diff options
Diffstat (limited to 'src/client/views/nodes')
36 files changed, 1716 insertions, 1038 deletions
diff --git a/src/client/views/nodes/AudioBox.tsx b/src/client/views/nodes/AudioBox.tsx index 1fb26c99b..0c671f7e3 100644 --- a/src/client/views/nodes/AudioBox.tsx +++ b/src/client/views/nodes/AudioBox.tsx @@ -1,14 +1,14 @@ import * as React from 'react'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { Tooltip } from '@mui/material'; -import { action, computed, IReactionDisposer, observable, runInAction } from 'mobx'; +import { action, computed, IReactionDisposer, makeObservable, observable, override, runInAction } from 'mobx'; import { observer } from 'mobx-react'; import { DateField } from '../../../fields/DateField'; import { Doc } from '../../../fields/Doc'; import { ComputedField } from '../../../fields/ScriptField'; import { Cast, DateCast, NumCast } from '../../../fields/Types'; import { AudioField, nullAudio } from '../../../fields/URLField'; -import { emptyFunction, formatTime, returnFalse, setupMoveUpEvents } from '../../../Utils'; +import { copyProps, emptyFunction, formatTime, returnFalse, setupMoveUpEvents } from '../../../Utils'; import { Docs, DocUtils } from '../../documents/Documents'; import { Networking } from '../../Network'; import { DragManager } from '../../util/DragManager'; @@ -49,10 +49,22 @@ export enum media_state { } @observer -export class AudioBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps & FieldViewProps>() { +export class AudioBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { public static LayoutString(fieldKey: string) { return FieldView.LayoutString(AudioBox, fieldKey); } + + _prevProps: React.PropsWithChildren<FieldViewProps>; + @override _props: React.PropsWithChildren<FieldViewProps>; + constructor(props: React.PropsWithChildren<FieldViewProps>) { + super(props); + this._props = this._prevProps = props; + makeObservable(this); + } + + componentDidUpdate() { + copyProps(this); + } public static Enabled = false; static topControlsHeight = 30; // height of upper controls above timeline @@ -68,7 +80,7 @@ export class AudioBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp _stream: MediaStream | undefined; // passed to MediaRecorder, records device input audio _play: any = null; // timeout for playback - @observable _stackedTimeline: CollectionStackedTimeline | null | undefined; // CollectionStackedTimeline ref + @observable _stackedTimeline: CollectionStackedTimeline | null | undefined = undefined; // CollectionStackedTimeline ref @observable _finished: boolean = false; // has playback reached end of clip @observable _volume: number = 1; @observable _muted: boolean = false; @@ -84,7 +96,7 @@ export class AudioBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp // if you get rid of it and set the value to 0 the timeline and waveform will set their bounds incorrectly @computed get miniPlayer() { - return this.props.PanelHeight() < 50; + return this._props.PanelHeight() < 50; } // used to collapse timeline when node is shrunk @computed get links() { return LinkManager.Links(this.dataDoc); @@ -94,7 +106,7 @@ export class AudioBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp } @computed get path() { // returns the path of the audio file - const path = Cast(this.props.Document[this.fieldKey], AudioField, null)?.url.href || ''; + const path = Cast(this.Document[this.fieldKey], AudioField, null)?.url.href || ''; return path === nullAudio ? '' : path; } set mediaState(value) { @@ -115,7 +127,7 @@ export class AudioBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp @action componentDidMount() { - this.props.setContentView?.(this); + this._props.setContentView?.(this); if (this.path) { this.mediaState = media_state.Paused; this.setPlayheadTime(NumCast(this.layoutDoc.clipStart)); @@ -142,7 +154,7 @@ export class AudioBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp this.Document, this.dataDoc, this.annotationKey, - this._ele?.currentTime || Cast(this.props.Document._layout_currentTimecode, 'number', null) || (this.mediaState === media_state.Recording ? (Date.now() - (this.recordingStart || 0)) / 1000 : undefined), + this._ele?.currentTime || Cast(this.Document._layout_currentTimecode, 'number', null) || (this.mediaState === media_state.Recording ? (Date.now() - (this.recordingStart || 0)) / 1000 : undefined), undefined, undefined, addAsAnnotation @@ -186,13 +198,16 @@ export class AudioBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp this._ele.play(); this.mediaState = media_state.Playing; this.addCurrentlyPlaying(); - this._play = setTimeout(() => { - // need to keep track of if end of clip is reached so on next play, clip restarts - if (fullPlay) this._finished = true; - // removes from currently playing if playback has reached end of range marker - else this.removeCurrentlyPlaying(); - this.Pause(); - }, (end - start) * 1000); + this._play = setTimeout( + () => { + // need to keep track of if end of clip is reached so on next play, clip restarts + if (fullPlay) this._finished = true; + // removes from currently playing if playback has reached end of range marker + else this.removeCurrentlyPlaying(); + this.Pause(); + }, + (end - start) * 1000 + ); } else { this.Pause(); } @@ -202,7 +217,7 @@ export class AudioBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp // removes from currently playing display @action removeCurrentlyPlaying = () => { - const docView = this.props.DocumentView?.(); + const docView = this._props.DocumentView?.(); if (CollectionStackedTimeline.CurrentlyPlaying && docView) { const index = CollectionStackedTimeline.CurrentlyPlaying.indexOf(docView); index !== -1 && CollectionStackedTimeline.CurrentlyPlaying.splice(index, 1); @@ -212,7 +227,7 @@ export class AudioBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp // adds doc to currently playing display @action addCurrentlyPlaying = () => { - const docView = this.props.DocumentView?.(); + const docView = this._props.DocumentView?.(); if (!CollectionStackedTimeline.CurrentlyPlaying) { CollectionStackedTimeline.CurrentlyPlaying = []; } @@ -240,7 +255,7 @@ export class AudioBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp this._recorder.ondataavailable = async (e: any) => { const [{ result }] = await Networking.UploadFilesToServer({ file: e.data }); if (!(result instanceof Error)) { - this.props.Document[this.fieldKey] = new AudioField(result.accessPaths.agnostic.client); + this.Document[this.fieldKey] = new AudioField(result.accessPaths.agnostic.client); } }; this._recordStart = new Date().getTime(); @@ -373,7 +388,7 @@ export class AudioBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp newDoc.overlayY = NumCast(this.Document.y) + NumCast(this.layoutDoc._height); Doc.AddToMyOverlay(newDoc); } else { - this.props.addDocument?.(newDoc); + this._props.addDocument?.(newDoc); } }), false @@ -449,9 +464,9 @@ export class AudioBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp }; @action - timelineWhenChildContentsActiveChanged = (isActive: boolean) => this.props.whenChildContentsActiveChanged((this._isAnyChildContentActive = isActive)); + timelineWhenChildContentsActiveChanged = (isActive: boolean) => this._props.whenChildContentsActiveChanged((this._isAnyChildContentActive = isActive)); - timelineScreenToLocal = () => this.props.ScreenToLocalTransform().translate(0, -AudioBox.topControlsHeight); + timelineScreenToLocal = () => this._props.ScreenToLocalTransform().translate(0, -AudioBox.topControlsHeight); setPlayheadTime = (time: number) => (this._ele!.currentTime /*= this.layoutDoc._layout_currentTimecode*/ = time); @@ -460,8 +475,8 @@ export class AudioBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp isActiveChild = () => this._isAnyChildContentActive; // timeline dimensions - timelineWidth = () => this.props.PanelWidth(); - timelineHeight = () => this.props.PanelHeight() - (AudioBox.topControlsHeight + AudioBox.bottomControlsHeight); + timelineWidth = () => this._props.PanelWidth(); + timelineHeight = () => this._props.PanelHeight() - (AudioBox.topControlsHeight + AudioBox.bottomControlsHeight); // ends trim, hides trim controls and displays new clip @undoBatch @@ -555,7 +570,7 @@ export class AudioBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp this._dropDisposer = DragManager.MakeDropTarget( r, (e, de) => { - const [xp, yp] = this.props.ScreenToLocalTransform().transformPoint(de.x, de.y); + const [xp, yp] = this._props.ScreenToLocalTransform().transformPoint(de.x, de.y); de.complete.docDragData && this.timeline?.internalDocDrop(e, de, de.complete.docDragData, xp); }, this.layoutDoc, @@ -597,7 +612,7 @@ export class AudioBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp <div className="audiobox-file" style={{ - pointerEvents: this._isAnyChildContentActive || this.props.isContentActive() ? 'all' : 'none', + pointerEvents: this._isAnyChildContentActive || this._props.isContentActive() ? 'all' : 'none', flexDirection: this.miniPlayer ? 'row' : 'column', justifyContent: this.miniPlayer ? 'flex-start' : 'space-between', }}> @@ -670,7 +685,7 @@ export class AudioBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp <div className="audiobox-timecodes"> <div className="timecode-current">{this.timeline && formatTime(Math.round(NumCast(this.layoutDoc._layout_currentTimecode) - NumCast(this.timeline.clipStart)))}</div> {this.miniPlayer ? ( - <div>/</div> + <div /> ) : ( <div className="bottom-controls-middle"> <FontAwesomeIcon icon="search-plus" /> @@ -699,13 +714,13 @@ export class AudioBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp return ( <CollectionStackedTimeline ref={action((r: CollectionStackedTimeline | null) => (this._stackedTimeline = r))} - {...this.props} + {...this._props} CollectionFreeFormDocumentView={undefined} dataFieldKey={this.fieldKey} fieldKey={this.annotationKey} dictationKey={this.fieldKey + '_dictation'} mediaPath={this.path} - renderDepth={this.props.renderDepth + 1} + renderDepth={this._props.renderDepth + 1} startTag={'_timecodeToShow' /* audioStart */} endTag={'_timecodeToHide' /* audioEnd */} bringToFront={emptyFunction} @@ -719,7 +734,7 @@ export class AudioBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp ScreenToLocalTransform={this.timelineScreenToLocal} Play={this.Play} Pause={this.Pause} - isContentActive={this.props.isContentActive} + isContentActive={this._props.isContentActive} isAnyChildContentActive={this.isAnyChildContentActive} playLink={this.playLink} PanelWidth={this.timelineWidth} @@ -734,7 +749,7 @@ export class AudioBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp return ( <audio ref={this.setRef} - className={`audiobox-control${this.props.isContentActive() ? '-interactive' : ''}`} + className={`audiobox-control${this._props.isContentActive() ? '-interactive' : ''}`} onLoadedData={action(e => this._ele?.duration && this._ele?.duration !== Infinity && (this.dataDoc[this.fieldKey + '_duration'] = this._ele.duration))}> <source src={this.path} type="audio/mpeg" /> Not supported. diff --git a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx index 624f28413..8e7a6914f 100644 --- a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx +++ b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx @@ -1,4 +1,4 @@ -import { action, computed, observable } from 'mobx'; +import { action, computed, observable, makeObservable, reaction, runInAction, override } from 'mobx'; import { observer } from 'mobx-react'; import { Doc, DocListCast, Opt } from '../../../fields/Doc'; import { List } from '../../../fields/List'; @@ -6,7 +6,7 @@ import { listSpec } from '../../../fields/Schema'; import { ComputedField } from '../../../fields/ScriptField'; import { Cast, NumCast, StrCast } from '../../../fields/Types'; import { TraceMobx } from '../../../fields/util'; -import { numberRange, OmitKeys } from '../../../Utils'; +import { copyProps, numberRange, OmitKeys } from '../../../Utils'; import { DocumentManager } from '../../util/DocumentManager'; import { SelectionManager } from '../../util/SelectionManager'; import { CollectionFreeFormView } from '../collections/collectionFreeForm/CollectionFreeFormView'; @@ -37,6 +37,13 @@ export interface CollectionFreeFormDocumentViewWrapperProps extends DocumentView } @observer export class CollectionFreeFormDocumentViewWrapper extends DocComponent<CollectionFreeFormDocumentViewWrapperProps & { fieldKey: string }>() implements CollectionFreeFormDocumentViewProps { + _prevProps: React.PropsWithChildren<CollectionFreeFormDocumentViewWrapperProps & { fieldKey: string }>; + @override _props: React.PropsWithChildren<CollectionFreeFormDocumentViewWrapperProps & { fieldKey: string }>; + constructor(props: React.PropsWithChildren<CollectionFreeFormDocumentViewWrapperProps & { fieldKey: string }>) { + super(props); + this._props = this._prevProps = props; + makeObservable(this); + } @observable X = this.props.x; @observable Y = this.props.y; @observable Z = this.props.z; @@ -64,11 +71,11 @@ export class CollectionFreeFormDocumentViewWrapper extends DocComponent<Collecti w_X = () => this.X; // prettier-ignore w_Y = () => this.Y; // prettier-ignore w_Z = () => this.Z; // prettier-ignore - w_ZIndex = () => this.ZIndex ?? NumCast(this.props.Document.zIndex); // prettier-ignore - w_Rotation = () => this.Rotation ?? NumCast(this.props.Document._rotation); // prettier-ignore + w_ZIndex = () => this.ZIndex ?? NumCast(this.Document.zIndex); // prettier-ignore + w_Rotation = () => this.Rotation ?? NumCast(this.Document._rotation); // prettier-ignore w_Opacity = () => this.Opacity; // prettier-ignore - w_BackgroundColor = () => this.BackgroundColor ?? Cast(this.props.Document._backgroundColor, 'string', null); // prettier-ignore - w_Color = () => this.Color ?? Cast(this.props.Document._color, 'string', null); // prettier-ignore + w_BackgroundColor = () => this.BackgroundColor ?? Cast(this.Document._backgroundColor, 'string', null); // prettier-ignore + w_Color = () => this.Color ?? Cast(this.Document._color, 'string', null); // prettier-ignore w_Highlight = () => this.Highlight; // prettier-ignore w_Width = () => this.Width; // prettier-ignore w_Height = () => this.Height; // prettier-ignore @@ -76,17 +83,18 @@ export class CollectionFreeFormDocumentViewWrapper extends DocComponent<Collecti w_Transition = () => this.Transition; // prettier-ignore w_DataTransition = () => this.DataTransition; // prettier-ignore - PanelWidth = () => this.props.autoDim ? this.props.PanelWidth?.() : this.Width; // prettier-ignore - PanelHeight = () => this.props.autoDim ? this.props.PanelHeight?.() : this.Height; // prettier-ignore - @action + PanelWidth = () => this._props.autoDim ? this._props.PanelWidth?.() : this.Width; // prettier-ignore + PanelHeight = () => this._props.autoDim ? this._props.PanelHeight?.() : this.Height; // prettier-ignore + componentDidUpdate() { - this.WrapperKeys.forEach(keys => ((this as any)[keys.upper] = (this.props as any)[keys.lower])); + copyProps(this); + this.WrapperKeys.forEach(action(keys => ((this as any)[keys.upper] = (this.props as any)[keys.lower]))); } render() { const layoutProps = this.WrapperKeys.reduce((val, keys) => [(val['w_' + keys.upper] = (this as any)['w_' + keys.upper]), val][1], {} as { [key: string]: Function }); return ( <CollectionFreeFormDocumentView - {...OmitKeys(this.props, this.WrapperKeys.map(keys => keys.lower) ).omit} // prettier-ignore + {...OmitKeys(this._props, this.WrapperKeys.map(keys => keys.lower) ).omit} // prettier-ignore {...layoutProps} PanelWidth={this.PanelWidth} PanelHeight={this.PanelHeight} @@ -115,7 +123,7 @@ export interface CollectionFreeFormDocumentViewProps { } @observer -export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeFormDocumentViewProps & DocumentViewProps & { fieldKey: string }>() { +export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeFormDocumentViewProps & DocumentViewProps>() { get displayName() { // this makes mobx trace() statements more descriptive return 'CollectionFreeFormDocumentView(' + this.Document.title + ')'; } // prettier-ignore @@ -135,33 +143,46 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF public static animStringFields = ['backgroundColor', 'color', 'fillColor']; // fields that are configured to be animatable using animation frames public static animDataFields = (doc: Doc) => (Doc.LayoutFieldKey(doc) ? [Doc.LayoutFieldKey(doc)] : []); // fields that are configured to be animatable using animation frames + _props: React.PropsWithChildren<CollectionFreeFormDocumentViewProps & DocumentViewProps>; + constructor(props: React.PropsWithChildren<CollectionFreeFormDocumentViewProps & DocumentViewProps>) { + super(props); + this._props = Object.assign({}, props); + makeObservable(this); + } + get CollectionFreeFormView() { - return this.props.CollectionFreeFormView; + return this._props.CollectionFreeFormView; } styleProvider = (doc: Doc | undefined, props: Opt<DocumentViewProps>, property: string) => { if (doc === this.layoutDoc) { switch (property) { - case StyleProp.Opacity: return this.props.w_Opacity(); // only change the opacity for this specific document, not its children - case StyleProp.BackgroundColor: return this.props.w_BackgroundColor(); - case StyleProp.Color: return this.props.w_Color(); + case StyleProp.Opacity: return this._props.w_Opacity(); // only change the opacity for this specific document, not its children + case StyleProp.BackgroundColor: return this._props.w_BackgroundColor(); + case StyleProp.Color: return this._props.w_Color(); } // prettier-ignore } - return this.props.styleProvider?.(doc, props, property); + return this._props.styleProvider?.(doc, props, property); }; public static getValues(doc: Doc, time: number, fillIn: boolean = true) { - return CollectionFreeFormDocumentView.animFields.reduce((p, val) => { - p[val.key] = Cast(doc[`${val.key}_indexed`], listSpec('number'), fillIn ? [NumCast(doc[val.key], val.val)] : []).reduce((p, v, i) => ((i <= Math.round(time) && v !== undefined) || p === undefined ? v : p), undefined as any as number); - return p; - }, {} as { [val: string]: Opt<number> }); + return CollectionFreeFormDocumentView.animFields.reduce( + (p, val) => { + p[val.key] = Cast(doc[`${val.key}_indexed`], listSpec('number'), fillIn ? [NumCast(doc[val.key], val.val)] : []).reduce((p, v, i) => ((i <= Math.round(time) && v !== undefined) || p === undefined ? v : p), undefined as any as number); + return p; + }, + {} as { [val: string]: Opt<number> } + ); } public static getStringValues(doc: Doc, time: number) { - return CollectionFreeFormDocumentView.animStringFields.reduce((p, val) => { - p[val] = Cast(doc[`${val}_indexed`], listSpec('string'), [StrCast(doc[val])]).reduce((p, v, i) => ((i <= Math.round(time) && v !== undefined) || p === undefined ? v : p), undefined as any as string); - return p; - }, {} as { [val: string]: Opt<string> }); + return CollectionFreeFormDocumentView.animStringFields.reduce( + (p, val) => { + p[val] = Cast(doc[`${val}_indexed`], listSpec('string'), [StrCast(doc[val])]).reduce((p, v, i) => ((i <= Math.round(time) && v !== undefined) || p === undefined ? v : p), undefined as any as string); + return p; + }, + {} as { [val: string]: Opt<string> } + ); } public static setStringValues(time: number, d: Doc, vals: { [val: string]: Opt<string> }) { @@ -213,7 +234,7 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF @action public float = () => { const topDoc = this.Document; - const containerDocView = this.props.docViewPath().lastElement(); + const containerDocView = this._props.docViewPath().lastElement(); const screenXf = containerDocView?.screenToLocalTransform(); if (screenXf) { SelectionManager.DeselectAll(); @@ -222,8 +243,8 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF topDoc.z = 0; topDoc.x = spt[0]; topDoc.y = spt[1]; - this.props.removeDocument?.(topDoc); - this.props.addDocTab(topDoc, OpenWhere.inParentFromScreen); + this._props.removeDocument?.(topDoc); + this._props.addDocTab(topDoc, OpenWhere.inParentFromScreen); } else { const spt = this.screenToLocalTransform().inverse().transformPoint(0, 0); const fpt = screenXf.transformPoint(spt[0], spt[1]); @@ -236,15 +257,15 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF }; nudge = (x: number, y: number) => { - const [locX, locY] = this.props.ScreenToLocalTransform().transformDirection(x, y); - this.props.Document.x = this.props.w_X() + locX; - this.props.Document.y = this.props.w_Y() + locY; + const [locX, locY] = this._props.ScreenToLocalTransform().transformDirection(x, y); + this._props.Document.x = this._props.w_X() + locX; + this._props.Document.y = this._props.w_Y() + locY; }; screenToLocalTransform = () => - this.props + this._props .ScreenToLocalTransform() - .translate(-this.props.w_X(), -this.props.w_Y()) - .rotateDeg(-(this.props.w_Rotation?.() || 0)); + .translate(-this._props.w_X(), -this._props.w_Y()) + .rotateDeg(-(this._props.w_Rotation?.() || 0)); returnThis = () => this; /// this indicates whether the doc view is activated because of its relationshop to a group @@ -255,25 +276,25 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF isGroupActive = () => { if (this.CollectionFreeFormView.isAnyChildContentActive()) return undefined; const isGroup = this.dataDoc.isGroup && (!this.layoutDoc.backgroundColor || this.layoutDoc.backgroundColor === 'transparent'); - return isGroup ? (this.props.isDocumentActive?.() ? 'group' : this.props.isGroupActive?.() ? 'child' : 'inactive') : this.props.isGroupActive?.() ? 'child' : undefined; + return isGroup ? (this._props.isDocumentActive?.() ? 'group' : this._props.isGroupActive?.() ? 'child' : 'inactive') : this._props.isGroupActive?.() ? 'child' : undefined; }; public static CollectionFreeFormDocViewClassName = 'collectionFreeFormDocumentView-container'; render() { TraceMobx(); - const passOnProps = OmitKeys(this.props, Object.keys(this.props).filter(key => key.startsWith('w_'))).omit; // prettier-ignore + const passOnProps = OmitKeys(this._props, Object.keys(this._props).filter(key => key.startsWith('w_'))).omit; // prettier-ignore return ( <div className={CollectionFreeFormDocumentView.CollectionFreeFormDocViewClassName} style={{ - width: this.props.PanelWidth(), - height: this.props.PanelHeight(), - transform: `translate(${this.props.w_X()}px, ${this.props.w_Y()}px) rotate(${NumCast(this.props.w_Rotation?.())}deg)`, - transition: this.props.w_Transition?.() ?? (this.props.w_DataTransition?.() || this.props.w_Transition?.()), - zIndex: this.props.w_ZIndex?.(), - display: this.props.w_Width?.() ? undefined : 'none', + width: this._props.PanelWidth(), + height: this._props.PanelHeight(), + transform: `translate(${this._props.w_X()}px, ${this._props.w_Y()}px) rotate(${NumCast(this._props.w_Rotation?.())}deg)`, + transition: this._props.w_Transition?.() ?? (this._props.w_DataTransition?.() || this._props.w_Transition?.()), + zIndex: this._props.w_ZIndex?.(), + display: this._props.w_Width?.() ? undefined : 'none', }}> - {this.props.RenderCutoffProvider(this.props.Document) ? ( - <div style={{ position: 'absolute', width: this.props.PanelWidth(), height: this.props.PanelHeight(), background: 'lightGreen' }} /> + {this._props.RenderCutoffProvider(this._props.Document) ? ( + <div style={{ position: 'absolute', width: this._props.PanelWidth(), height: this._props.PanelHeight(), background: 'lightGreen' }} /> ) : ( <DocumentView {...passOnProps} CollectionFreeFormDocumentView={this.returnThis} styleProvider={this.styleProvider} ScreenToLocalTransform={this.screenToLocalTransform} isGroupActive={this.isGroupActive} /> )} diff --git a/src/client/views/nodes/ColorBox.scss b/src/client/views/nodes/ColorBox.scss deleted file mode 100644 index d5f2a7ec7..000000000 --- a/src/client/views/nodes/ColorBox.scss +++ /dev/null @@ -1,22 +0,0 @@ -.colorBox-container, .colorBox-container-interactive { - width:100%; - height:100%; - position: relative; - pointer-events: none; - transform-origin: top left; - - .sketch-picker { - div { - cursor: crosshair; - } - .flexbox-fix { - cursor: pointer; - div { - cursor:pointer; - } - } - } -} -.colorBox-container-interactive { - pointer-events:all; -}
\ No newline at end of file diff --git a/src/client/views/nodes/ColorBox.tsx b/src/client/views/nodes/ColorBox.tsx deleted file mode 100644 index b4ba51814..000000000 --- a/src/client/views/nodes/ColorBox.tsx +++ /dev/null @@ -1,88 +0,0 @@ -import * as React from 'react'; -import { action } from 'mobx'; -import { observer } from 'mobx-react'; -import { ColorResult, SketchPicker } from 'react-color'; -import { Doc } from '../../../fields/Doc'; -import { InkTool } from '../../../fields/InkField'; -import { NumCast, StrCast } from '../../../fields/Types'; -import { DashColor } from '../../../Utils'; -import { DocumentType } from '../../documents/DocumentTypes'; -import { ScriptingGlobals } from '../../util/ScriptingGlobals'; -import { SelectionManager } from '../../util/SelectionManager'; -import { undoBatch } from '../../util/UndoManager'; -import { ViewBoxBaseComponent } from '../DocComponent'; -import { ActiveInkColor, ActiveInkWidth, SetActiveInkColor, SetActiveInkWidth } from '../InkingStroke'; -import './ColorBox.scss'; -import { FieldView, FieldViewProps } from './FieldView'; -import { RichTextMenu } from './formattedText/RichTextMenu'; - -@observer -export class ColorBox extends ViewBoxBaseComponent<FieldViewProps>() { - public static LayoutString(fieldKey: string) { - return FieldView.LayoutString(ColorBox, fieldKey); - } - - @undoBatch - @action - static switchColor(color: ColorResult) { - SetActiveInkColor(color.hex); - - SelectionManager.Views().map(view => { - const targetDoc = - view.props.Document.dragFactory instanceof Doc - ? view.props.Document.dragFactory - : view.props.Document.layout instanceof Doc - ? view.props.Document.layout - : view.props.Document.isTemplateForField - ? view.props.Document - : Doc.GetProto(view.props.Document); - if (targetDoc) { - if (view.props.LayoutTemplate?.() || view.props.LayoutTemplateString) { - // this situation typically occurs when you have a link dot - targetDoc.backgroundColor = color.hex; // bcz: don't know how to change the color of an inline template... - } else if (RichTextMenu.Instance?.TextViewFieldKey && window.getSelection()?.toString() !== '') { - Doc.Layout(view.props.Document)[RichTextMenu.Instance.TextViewFieldKey + '-color'] = color.hex; - } else { - Doc.Layout(view.props.Document)._backgroundColor = color.hex + (color.rgb.a ? Math.round(color.rgb.a * 255).toString(16) : ''); // '_backgroundColor' is template specific. 'backgroundColor' would apply to all templates, but has no UI at the moment - } - } - }); - } - - render() { - const scaling = Math.min(this.layoutDoc.layout_fitWidth ? 10000 : this.props.PanelHeight() / NumCast(this.Document._height), this.props.PanelWidth() / NumCast(this.Document._width)); - return ( - <div - className={`colorBox-container${this.props.isContentActive() ? '-interactive' : ''}`} - onPointerDown={e => e.button === 0 && !e.ctrlKey && e.stopPropagation()} - onClick={e => e.stopPropagation()} - style={{ transform: `scale(${scaling})`, width: `${100 * scaling}%`, height: `${100 * scaling}%` }}> - <SketchPicker - onChange={c => Doc.ActiveTool === InkTool.None && ColorBox.switchColor(c)} - color={StrCast(SelectionManager.Views()?.[0]?.Document?._backgroundColor, ActiveInkColor())} - presetColors={['#D0021B', '#F5A623', '#F8E71C', '#8B572A', '#7ED321', '#417505', '#9013FE', '#4A90E2', '#50E3C2', '#B8E986', '#000000', '#4A4A4A', '#9B9B9B', '#FFFFFF', '#f1efeb', 'transparent']} - /> - - <div style={{ width: this.props.PanelWidth() / scaling, display: 'flex', paddingTop: '10px' }}> - <div> {ActiveInkWidth()}</div> - <input - type="range" - defaultValue={ActiveInkWidth()} - min={1} - max={100} - onChange={(e: React.ChangeEvent<HTMLInputElement>) => { - SetActiveInkWidth(e.target.value); - SelectionManager.Views() - .filter(i => StrCast(i.Document.type) === DocumentType.INK) - .map(i => (i.Document.stroke_width = Number(e.target.value))); - }} - /> - </div> - </div> - ); - } -} - -ScriptingGlobals.add(function interpColors(c1: string, c2: string, weight = 0.5) { - return DashColor(c1).mix(DashColor(c2), weight); -}); diff --git a/src/client/views/nodes/DataVizBox/components/TableBox.tsx b/src/client/views/nodes/DataVizBox/components/TableBox.tsx index 0e766e5f0..33be87f46 100644 --- a/src/client/views/nodes/DataVizBox/components/TableBox.tsx +++ b/src/client/views/nodes/DataVizBox/components/TableBox.tsx @@ -77,7 +77,7 @@ export class TableBox extends React.Component<TableBoxProps> { }; @computed get viewScale() { - return this.props.docView?.()?.props.ScreenToLocalTransform().Scale || 1; + return this.props.docView?.()?._props.ScreenToLocalTransform().Scale || 1; } @computed get rowHeight() { console.log('scale = ' + this.viewScale + ' table = ' + this._tableHeight + ' ids = ' + this._tableDataIds.length); @@ -162,8 +162,6 @@ export class TableBox extends React.Component<TableBoxProps> { }; render() { - console.log(this.endID); - trace(); if (this._tableData.length > 0) { return ( <div diff --git a/src/client/views/nodes/DocumentContentsView.tsx b/src/client/views/nodes/DocumentContentsView.tsx index 54cfba506..82d346206 100644 --- a/src/client/views/nodes/DocumentContentsView.tsx +++ b/src/client/views/nodes/DocumentContentsView.tsx @@ -1,4 +1,4 @@ -import { computed } from 'mobx'; +import { computed, makeObservable, observable, runInAction, untracked } from 'mobx'; import { observer } from 'mobx-react'; import { Doc, Opt } from '../../../fields/Doc'; import { AclPrivate, DocData } from '../../../fields/DocSymbols'; @@ -18,7 +18,6 @@ import { SearchBox } from '../search/SearchBox'; import { DashWebRTCVideo } from '../webcam/DashWebRTCVideo'; import { YoutubeBox } from './../../apis/youtube/YoutubeBox'; import { AudioBox } from './AudioBox'; -import { ColorBox } from './ColorBox'; import { ComparisonBox } from './ComparisonBox'; import { DataVizBox } from './DataVizBox/DataVizBox'; import { DocumentViewProps } from './DocumentView'; @@ -46,7 +45,7 @@ import { PresBox } from './trails/PresBox'; import { VideoBox } from './VideoBox'; import { WebBox } from './WebBox'; import * as React from 'react'; -import XRegExp = require('xregexp'); +import * as XRegExp from 'xregexp'; const JsxParser = require('react-jsx-parser').default; //TODO Why does this need to be imported like this? @@ -123,30 +122,42 @@ export class DocumentContentsView extends React.Component< layout_fieldKey: string; } > { + @observable _props!: DocumentViewProps & + FieldViewProps & { + setHeight?: (height: number) => void; + layout_fieldKey: string; + }; + constructor(props: any) { + super(props); + this._props = props; + makeObservable(this); + } + + componentDidUpdate(prevProps: Readonly<DocumentViewProps & { setHeight?: ((height: number) => void) | undefined; layout_fieldKey: string }>, prevState: Readonly<{}>, snapshot?: any): void { + // untracked(() => (this._props = this.props)); + // Object.keys(prevProps).forEach(pkey => (prevProps as any)[pkey] !== (this._props as any)[pkey] && console.log(pkey + ' ' + (prevProps as any)[pkey] + ' ' + (this._props as any)[pkey])); + } + @computed get layout(): string { TraceMobx(); - if (this.props.LayoutTemplateString) return this.props.LayoutTemplateString; + if (this._props.LayoutTemplateString) return this._props.LayoutTemplateString; if (!this.layoutDoc) return '<p>awaiting layout</p>'; - if (this.props.layout_fieldKey === 'layout_keyValue') return StrCast(this.props.Document.layout_keyValue, KeyValueBox.LayoutString()); - const layout = Cast(this.layoutDoc[this.layoutDoc === this.props.Document && this.props.layout_fieldKey ? this.props.layout_fieldKey : StrCast(this.layoutDoc.layout_fieldKey, 'layout')], 'string'); - if (layout === undefined) return this.props.Document.data ? "<FieldView {...props} fieldKey='data' />" : KeyValueBox.LayoutString(); + if (this._props.layout_fieldKey === 'layout_keyValue') return StrCast(this._props.Document.layout_keyValue, KeyValueBox.LayoutString()); + const layout = Cast(this.layoutDoc[this.layoutDoc === this._props.Document && this._props.layout_fieldKey ? this._props.layout_fieldKey : StrCast(this.layoutDoc.layout_fieldKey, 'layout')], 'string'); + if (layout === undefined) return this._props.Document.data ? "<FieldView {...props} fieldKey='data' />" : KeyValueBox.LayoutString(); if (typeof layout === 'string') return layout; return '<p>Loading layout</p>'; } - componentDidUpdate(prevProps: Readonly<DocumentViewProps & { setHeight?: ((height: number) => void) | undefined; layout_fieldKey: string }>, prevState: Readonly<{}>, snapshot?: any): void { - Object.keys(prevProps).forEach(pkey => (prevProps as any)[pkey] !== (this.props as any)[pkey] && console.log(pkey + ' ' + (prevProps as any)[pkey] + ' ' + (this.props as any)[pkey])); - } - get layoutDoc() { // bcz: replaced this with below : is it correct? change was made to accommodate passing fieldKey's from a layout script - // const template: Doc = this.props.LayoutTemplate?.() || Doc.Layout(this.props.Document, this.props.layout_fieldKey ? Cast(this.props.Document[this.props.layout_fieldKey], Doc, null) : undefined); + // const template: Doc = this._props.LayoutTemplate?.() || Doc.Layout(this._props.Document, this._props.layout_fieldKey ? Cast(this._props.Document[this._props.layout_fieldKey], Doc, null) : undefined); const template: Doc = - this.props.LayoutTemplate?.() || - (this.props.LayoutTemplateString && this.props.Document) || - (this.props.layout_fieldKey && StrCast(this.props.Document[this.props.layout_fieldKey]) && this.props.Document) || - Doc.Layout(this.props.Document, this.props.layout_fieldKey ? Cast(this.props.Document[this.props.layout_fieldKey], Doc, null) : undefined); - return Doc.expandTemplateLayout(template, this.props.Document); + this._props.LayoutTemplate?.() || + (this._props.LayoutTemplateString && this._props.Document) || + (this._props.layout_fieldKey && StrCast(this._props.Document[this._props.layout_fieldKey]) && this._props.Document) || + Doc.Layout(this._props.Document, this._props.layout_fieldKey ? Cast(this._props.Document[this._props.layout_fieldKey], Doc, null) : undefined); + return Doc.expandTemplateLayout(template, this._props.Document); } CreateBindings(onClick: Opt<ScriptField>, onInput: Opt<ScriptField>): JsxBindings { @@ -165,10 +176,10 @@ export class DocumentContentsView extends React.Component< 'onPointerDown', 'onPointerUp', ]; - const templateDataDoc = this.props.TemplateDataDocument ?? (this.layoutDoc !== this.props.Document ? this.props.Document[DocData] : undefined); + const templateDataDoc = this._props.TemplateDataDocument ?? (this.layoutDoc !== this._props.Document ? this._props.Document[DocData] : undefined); const list: BindingProps & React.DetailedHTMLProps<React.HtmlHTMLAttributes<HTMLDivElement>, HTMLDivElement> = { - ...this.props, - Document: this.layoutDoc ?? this.props.Document, + ...this._props, + Document: this.layoutDoc ?? this._props.Document, TemplateDataDocument: templateDataDoc instanceof Promise ? undefined : templateDataDoc, onClick: onClick as any as React.MouseEventHandler, // pass onClick script as if it were a real function -- it will be interpreted properly in the HTMLtag onInput: onInput as any as React.FormEventHandler, @@ -181,7 +192,7 @@ export class DocumentContentsView extends React.Component< } // componentWillUpdate(oldProps: any, newState: any) { - // // console.log("willupdate", oldProps, this.props); // bcz: if you get a message saying something invalidated because reactive props changed, then this method allows you to figure out which prop changed + // // console.log("willupdate", oldProps, this._props); // bcz: if you get a message saying something invalidated because reactive props changed, then this method allows you to figure out which prop changed // } @computed get renderData() { @@ -190,13 +201,13 @@ export class DocumentContentsView extends React.Component< // replace code content with a script >{content}< as in <HTMLdiv>{this.title}</HTMLdiv> const replacer = (match: any, prefix: string, expr: string, postfix: string, offset: any, string: any) => { - return prefix + ((ScriptField.MakeFunction(expr, { self: Doc.name, this: Doc.name })?.script.run({ this: this.props.Document }).result as string) || '') + postfix; + return prefix + ((ScriptField.MakeFunction(expr, { self: Doc.name, this: Doc.name })?.script.run({ this: this._props.Document }).result as string) || '') + postfix; }; layoutFrame = layoutFrame.replace(/(>[^{]*)[^=]\{([^.'][^<}]+)\}([^}]*<)/g, replacer); // replace HTML<tag> with corresponding HTML tag as in: <HTMLdiv> becomes <HTMLtag Document={props.Document} htmltag='div'> const replacer2 = (match: any, p1: string, offset: any, string: any) => { - return `<HTMLtag Document={props.Document} scaling='${this.props.NativeDimScaling?.() || 1}' htmltag='${p1}'`; + return `<HTMLtag Document={props.Document} scaling='${this._props.NativeDimScaling?.() || 1}' htmltag='${p1}'`; }; layoutFrame = layoutFrame.replace(/<HTML([a-zA-Z0-9_-]+)/g, replacer2); @@ -228,7 +239,7 @@ export class DocumentContentsView extends React.Component< TraceMobx(); const { bindings, layoutFrame } = this.renderData; - return this.props.renderDepth > 12 || !layoutFrame || !this.layoutDoc || GetEffectiveAcl(this.layoutDoc) === AclPrivate ? null : ( + return this._props.renderDepth > 12 || !layoutFrame || !this.layoutDoc || GetEffectiveAcl(this.layoutDoc) === AclPrivate ? null : ( <ObserverJsxParser key={42} blacklistedAttrs={emptyPath} @@ -256,7 +267,6 @@ export class DocumentContentsView extends React.Component< PresElementBox, SearchBox, FunctionPlotBox, - ColorBox, DashWebRTCVideo, LinkAnchorBox, InkingStroke, diff --git a/src/client/views/nodes/DocumentLinksButton.tsx b/src/client/views/nodes/DocumentLinksButton.tsx index 90b044374..19b43fd52 100644 --- a/src/client/views/nodes/DocumentLinksButton.tsx +++ b/src/client/views/nodes/DocumentLinksButton.tsx @@ -1,10 +1,10 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { Tooltip } from '@mui/material'; -import { action, computed, observable, runInAction } from 'mobx'; +import { action, computed, makeObservable, observable, runInAction } from 'mobx'; import { observer } from 'mobx-react'; import { Doc } from '../../../fields/Doc'; import { StrCast } from '../../../fields/Types'; -import { emptyFunction, returnFalse, setupMoveUpEvents, StopEvent } from '../../../Utils'; +import { copyProps, emptyFunction, returnFalse, setupMoveUpEvents, StopEvent } from '../../../Utils'; import { DocUtils } from '../../documents/Documents'; import { DragManager } from '../../util/DragManager'; import { Hypothesis } from '../../util/HypothesisUtils'; @@ -16,7 +16,6 @@ import { LinkDescriptionPopup } from './LinkDescriptionPopup'; import { TaskCompletionBox } from './TaskCompletedBox'; import { PinProps } from './trails'; import * as React from 'react'; -import * as _ from 'lodash'; interface DocumentLinksButtonProps { View: DocumentView; @@ -29,25 +28,51 @@ interface DocumentLinksButtonProps { scaling?: () => number; // how uch doc is scaled so that link buttons can invert it hideCount?: () => boolean; } + +export class DocButtonState { + @observable public StartLink: Doc | undefined = undefined; //origin's Doc, if defined + @observable public StartLinkView: DocumentView | undefined = undefined; + @observable public AnnotationId: string | undefined = undefined; + @observable public AnnotationUri: string | undefined = undefined; + @observable public LinkEditorDocView: DocumentView | undefined = undefined; + public static _instance: DocButtonState | undefined; + public static get Instance() { + return DocButtonState._instance ?? (DocButtonState._instance = new DocButtonState()); + } + constructor() { + makeObservable(this); + } +} @observer export class DocumentLinksButton extends React.Component<DocumentLinksButtonProps, {}> { private _linkButton = React.createRef<HTMLDivElement>(); - @observable public static StartLink: Doc | undefined; //origin's Doc, if defined - @observable public static StartLinkView: DocumentView | undefined; - @observable public static AnnotationId: string | undefined; - @observable public static AnnotationUri: string | undefined; - @observable public static LinkEditorDocView: DocumentView | undefined; + public static get StartLink() { return DocButtonState.Instance.StartLink; } // prettier-ignore + public static set StartLink(value) { runInAction(() => (DocButtonState.Instance.StartLink = value)); } // prettier-ignore + @observable public static StartLinkView: DocumentView | undefined = undefined; + @observable public static AnnotationId: string | undefined = undefined; + @observable public static AnnotationUri: string | undefined = undefined; + + _prevProps: React.PropsWithChildren<DocumentLinksButtonProps>; + @observable _props: React.PropsWithChildren<DocumentLinksButtonProps>; + constructor(props: React.PropsWithChildren<DocumentLinksButtonProps>) { + super(props); + this._props = this._prevProps = props; + makeObservable(this); + } + + componentDidUpdate() { + copyProps(this); + } - @action @undoBatch onLinkButtonMoved = (e: PointerEvent) => { - if (this.props.InMenu && this.props.StartLink) { + if (this._props.InMenu && this._props.StartLink) { if (this._linkButton.current !== null) { const linkDrag = UndoManager.StartBatch('Drag Link'); - this.props.View && - DragManager.StartLinkDrag(this._linkButton.current, this.props.View, this.props.View.ComponentView?.getAnchor, e.pageX, e.pageY, { + this._props.View && + DragManager.StartLinkDrag(this._linkButton.current, this._props.View, this._props.View.ComponentView?.getAnchor, e.pageX, e.pageY, { dragComplete: dropEv => { - if (this.props.View && dropEv.linkDocument) { + if (this._props.View && dropEv.linkDocument) { // dropEv.linkDocument equivalent to !dropEve.aborted since linkDocument is only assigned on a completed drop !dropEv.linkDocument.link_relationship && (Doc.GetProto(dropEv.linkDocument).link_relationship = 'hyperlink'); } @@ -69,11 +94,11 @@ export class DocumentLinksButton extends React.Component<DocumentLinksButtonProp this.onLinkButtonMoved, emptyFunction, action((e, doubleTap) => { - doubleTap && DocumentView.showBackLinks(this.props.View.Document); + doubleTap && DocumentView.showBackLinks(this._props.View.Document); }), undefined, undefined, - action(() => (DocumentLinksButton.LinkEditorDocView = this.props.View)) + action(() => (DocButtonState.Instance.LinkEditorDocView = this._props.View)) ); }; @@ -84,33 +109,32 @@ export class DocumentLinksButton extends React.Component<DocumentLinksButtonProp this.onLinkButtonMoved, emptyFunction, action((e, doubleTap) => { - if (doubleTap && this.props.InMenu && this.props.StartLink) { - //action(() => Doc.BrushDoc(this.props.View.Document)); - if (DocumentLinksButton.StartLink === this.props.View.props.Document) { + if (doubleTap && this._props.InMenu && this._props.StartLink) { + //action(() => Doc.BrushDoc(this._props.View.Document)); + if (DocumentLinksButton.StartLink === this._props.View.Document) { DocumentLinksButton.StartLink = undefined; DocumentLinksButton.StartLinkView = undefined; } else { - DocumentLinksButton.StartLink = this.props.View.props.Document; - DocumentLinksButton.StartLinkView = this.props.View; + DocumentLinksButton.StartLink = this._props.View.Document; + DocumentLinksButton.StartLinkView = this._props.View; } } }) ); }; - @action @undoBatch onLinkClick = (e: React.MouseEvent): void => { - if (this.props.InMenu && this.props.StartLink) { + if (this._props.InMenu && this._props.StartLink) { DocumentLinksButton.AnnotationId = undefined; DocumentLinksButton.AnnotationUri = undefined; - if (DocumentLinksButton.StartLink === this.props.View.props.Document) { + if (DocumentLinksButton.StartLink === this._props.View.Document) { DocumentLinksButton.StartLink = undefined; DocumentLinksButton.StartLinkView = undefined; } else { //if this LinkButton's Document is undefined - DocumentLinksButton.StartLink = this.props.View.props.Document; - DocumentLinksButton.StartLinkView = this.props.View; + DocumentLinksButton.StartLink = this._props.View.Document; + DocumentLinksButton.StartLinkView = this._props.View; } } }; @@ -121,7 +145,7 @@ export class DocumentLinksButton extends React.Component<DocumentLinksButtonProp e, returnFalse, emptyFunction, - action(e => DocumentLinksButton.finishLinkClick(e.clientX, e.clientY, DocumentLinksButton.StartLink, this.props.View.props.Document, true, this.props.View)) + action(e => DocumentLinksButton.finishLinkClick(e.clientX, e.clientY, DocumentLinksButton.StartLink, this._props.View.Document, true, this._props.View)) ); }; @@ -133,7 +157,7 @@ export class DocumentLinksButton extends React.Component<DocumentLinksButtonProp DocumentLinksButton.StartLinkView = undefined; DocumentLinksButton.AnnotationId = undefined; DocumentLinksButton.AnnotationUri = undefined; - //!this.props.StartLink + //!this._props.StartLink } else if (startLink !== endLink) { endLink = endLinkView?.docView?._componentView?.getAnchor?.(true, pinProps) || endLink; startLink = DocumentLinksButton.StartLinkView?.docView?._componentView?.getAnchor?.(true) || startLink; @@ -190,8 +214,8 @@ export class DocumentLinksButton extends React.Component<DocumentLinksButtonProp @computed get filteredLinks() { const results = [] as Doc[]; - const filters = this.props.View.props.childFilters(); - Array.from(new Set<Doc>(this.props.View.allLinks)).forEach(link => { + const filters = this._props.View._props.childFilters(); + Array.from(new Set<Doc>(this._props.View.allLinks)).forEach(link => { if (DocUtils.FilterDocs([link], filters, []).length || DocUtils.FilterDocs([link.link_anchor_2 as Doc], filters, []).length || DocUtils.FilterDocs([link.link_anchor_1 as Doc], filters, []).length) { results.push(link); } @@ -206,8 +230,8 @@ export class DocumentLinksButton extends React.Component<DocumentLinksButtonProp */ @computed get linkButtonInner() { const btnDim = 30; - const isActive = DocumentLinksButton.StartLink === this.props.View.props.Document && this.props.StartLink; - const scaling = Math.min(1, this.props.scaling?.() || 1); + const isActive = DocumentLinksButton.StartLink === this._props.View.Document && this._props.StartLink; + const scaling = Math.min(1, this._props.scaling?.() || 1); const showLinkCount = (onHover?: boolean, offset?: boolean) => ( <div className="documentLinksButton-showCount" @@ -221,16 +245,16 @@ export class DocumentLinksButton extends React.Component<DocumentLinksButtonProp <span style={{ width: '100%', display: 'inline-block', textAlign: 'center' }}>{Array.from(this.filteredLinks).length}</span> </div> ); - return this.props.ShowCount ? ( - showLinkCount(this.props.OnHover, this.props.Bottom) + return this._props.ShowCount ? ( + showLinkCount(this._props.OnHover, this._props.Bottom) ) : ( <div className="documentLinksButton-menu"> - {this.props.StartLink ? ( //if link has been started from current node, then set behavior of link button to deactivate linking when clicked again + {this._props.StartLink ? ( //if link has been started from current node, then set behavior of link button to deactivate linking when clicked again <div className={`documentLinksButton ${isActive ? `startLink` : ``}`} ref={this._linkButton} onPointerDown={isActive ? StopEvent : this.onLinkButtonDown} onClick={isActive ? this.clearLinks : this.onLinkClick}> <FontAwesomeIcon className="documentdecorations-icon" icon="link" /> </div> ) : null} - {!this.props.StartLink && DocumentLinksButton.StartLink !== this.props.View.props.Document ? ( //if the origin node is not this node + {!this._props.StartLink && DocumentLinksButton.StartLink !== this._props.View.Document ? ( //if the origin node is not this node <div className={'documentLinksButton-endLink'} ref={this._linkButton} onPointerDown={DocumentLinksButton.StartLink && this.completeLink}> <FontAwesomeIcon className="documentdecorations-icon" icon="link" /> </div> @@ -240,21 +264,21 @@ export class DocumentLinksButton extends React.Component<DocumentLinksButtonProp } render() { - if (this.props.hideCount?.()) return null; - const menuTitle = this.props.StartLink ? 'Drag or tap to start link' : 'Tap to complete link'; + if (this._props.hideCount?.()) return null; + const menuTitle = this._props.StartLink ? 'Drag or tap to start link' : 'Tap to complete link'; const buttonTitle = 'Tap to view links; double tap to open link collection'; - const title = this.props.ShowCount ? buttonTitle : menuTitle; + const title = this._props.ShowCount ? buttonTitle : menuTitle; //render circular tooltip if it isn't set to invisible and show the number of doc links the node has, and render inner-menu link button for starting/stopping links if currently in menu - return !Array.from(this.filteredLinks).length && !this.props.AlwaysOn ? null : ( + return !Array.from(this.filteredLinks).length && !this._props.AlwaysOn ? null : ( <div className="documentLinksButton-wrapper" style={{ - position: this.props.InMenu ? 'relative' : 'absolute', + position: this._props.InMenu ? 'relative' : 'absolute', top: 0, pointerEvents: 'none', }}> - {DocumentLinksButton.LinkEditorDocView ? this.linkButtonInner : <Tooltip title={<div className="dash-tooltip">{title}</div>}>{this.linkButtonInner}</Tooltip>} + {DocButtonState.Instance.LinkEditorDocView ? this.linkButtonInner : <Tooltip title={<div className="dash-tooltip">{title}</div>}>{this.linkButtonInner}</Tooltip>} </div> ); } diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx index f2a910023..b3acb08e2 100644 --- a/src/client/views/nodes/DocumentView.tsx +++ b/src/client/views/nodes/DocumentView.tsx @@ -1,9 +1,9 @@ import { IconProp } from '@fortawesome/fontawesome-svg-core'; import { Dropdown, DropdownType, Type } from 'browndash-components'; -import { action, computed, IReactionDisposer, observable, reaction, runInAction } from 'mobx'; +import { action, computed, IReactionDisposer, makeObservable, observable, override, reaction, runInAction, untracked } from 'mobx'; import { observer } from 'mobx-react'; import { computedFn } from 'mobx-utils'; -import { Bounce, Fade, Flip, LightSpeed, Roll, Rotate, Zoom } from 'react-reveal'; +// import { Bounce, Fade, Flip, LightSpeed, Roll, Rotate, Zoom } from 'react-reveal'; import { Doc, DocListCast, Field, Opt, StrListCast } from '../../../fields/Doc'; import { AclPrivate, Animation, AudioPlay, DocViews } from '../../../fields/DocSymbols'; import { Id } from '../../../fields/FieldSymbols'; @@ -15,7 +15,7 @@ import { ScriptField } from '../../../fields/ScriptField'; import { BoolCast, Cast, DocCast, ImageCast, NumCast, ScriptCast, StrCast } from '../../../fields/Types'; import { AudioField } from '../../../fields/URLField'; import { GetEffectiveAcl, TraceMobx } from '../../../fields/util'; -import { emptyFunction, isTargetChildOf as isParentOf, lightOrDark, returnEmptyString, returnFalse, returnTrue, returnVal, simulateMouseClick, Utils } from '../../../Utils'; +import { copyProps, emptyFunction, isTargetChildOf as isParentOf, lightOrDark, returnEmptyString, returnFalse, returnTrue, returnVal, simulateMouseClick, Utils } from '../../../Utils'; import { GooglePhotos } from '../../apis/google_docs/GooglePhotosClientUtils'; import { DocServer } from '../../DocServer'; import { DocOptions, Docs, DocUtils, FInfo } from '../../documents/Documents'; @@ -254,6 +254,19 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps private _titleRef = React.createRef<EditableView>(); private _dropDisposer?: DragManager.DragDropDisposer; + @override _props: DocumentViewInternalProps; + _prevProps: DocumentViewInternalProps; + + constructor(props: DocumentViewInternalProps) { + super(props); + this._props = this._prevProps = props; + makeObservable(this); + } + + componentDidUpdate() { + // untracked(() => (this._props = this._props)); + } + @observable _componentView: Opt<DocComponentView>; // needs to be accessed from DocumentView wrapper class @observable _animateScaleTime: Opt<number>; // milliseconds for animating between views. defaults to 300 if not uset @observable _animateScalingTo = 0; @@ -262,7 +275,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps return this._animateScaleTime ?? 100; } public get displayName() { - return 'DocumentViewInternal(' + this.props.Document.title + ')'; + return 'DocumentViewInternal(' + this._props.Document.title + ')'; } // this makes mobx trace() statements more descriptive public get ContentDiv() { @@ -272,43 +285,43 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps return Doc.LayoutFieldKey(this.layoutDoc); } @computed get layout_showTitle() { - return this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.ShowTitle) as Opt<string>; + return this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.ShowTitle) as Opt<string>; } @computed get NativeDimScaling() { - return this.props.NativeDimScaling?.() || 1; + return this._props.NativeDimScaling?.() || 1; } @computed get thumb() { return ImageCast(this.layoutDoc['thumb-frozen'], ImageCast(this.layoutDoc.thumb))?.url?.href.replace('.png', '_m.png'); } @computed get opacity() { - return this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.Opacity); + return this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.Opacity); } @computed get boxShadow() { - return this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.BoxShadow); + return this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.BoxShadow); } @computed get borderRounding() { - return this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.BorderRounding); + return this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.BorderRounding); } @computed get widgetDecorations() { TraceMobx(); - return this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.Decorations); + return this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.Decorations); } @computed get backgroundBoxColor() { - return this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.BackgroundColor + ':box'); + return this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.BackgroundColor + ':box'); } @computed get docContents() { - return this.props.styleProvider?.(this.Document, this.props, StyleProp.DocContents); + return this._props.styleProvider?.(this.Document, this._props, StyleProp.DocContents); } @computed get headerMargin() { - return this.props?.styleProvider?.(this.layoutDoc, this.props, StyleProp.HeaderMargin) || 0; + return this._props?.styleProvider?.(this.layoutDoc, this._props, StyleProp.HeaderMargin) || 0; } @computed get layout_showCaption() { - return this.props?.styleProvider?.(this.layoutDoc, this.props, StyleProp.ShowCaption) || 0; + return this._props?.styleProvider?.(this.layoutDoc, this._props, StyleProp.ShowCaption) || 0; } @computed get titleHeight() { - return this.props?.styleProvider?.(this.layoutDoc, this.props, StyleProp.TitleHeight) || 0; + return this._props?.styleProvider?.(this.layoutDoc, this._props, StyleProp.TitleHeight) || 0; } - @observable _pointerEvents: 'none' | 'all' | 'visiblePainted' | undefined; + @observable _pointerEvents: 'none' | 'all' | 'visiblePainted' | undefined = undefined; @computed get pointerEvents(): 'none' | 'all' | 'visiblePainted' | undefined { return this._pointerEvents; } @@ -316,7 +329,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps return StrCast(this.Document.layout_fieldKey, 'layout'); } @computed get disableClickScriptFunc() { - const onScriptDisable = this.props.onClickScriptDisable ?? this._componentView?.onClickScriptDisable?.() ?? this.layoutDoc.onClickScriptDisable; + const onScriptDisable = this._props.onClickScriptDisable ?? this._componentView?.onClickScriptDisable?.() ?? this.layoutDoc.onClickScriptDisable; // prettier-ignore return ( DocumentView.LongPress || @@ -325,49 +338,49 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps ); } @computed get onClickHandler() { - return this.props.onClick?.() ?? this.props.onBrowseClick?.() ?? Cast(this.Document.onClick, ScriptField, Cast(this.layoutDoc.onClick, ScriptField, null)); + return this._props.onClick?.() ?? this._props.onBrowseClick?.() ?? Cast(this.Document.onClick, ScriptField, Cast(this.layoutDoc.onClick, ScriptField, null)); } @computed get onDoubleClickHandler() { - return this.props.onDoubleClick?.() ?? Cast(this.layoutDoc.onDoubleClick, ScriptField, null) ?? this.Document.onDoubleClick; + return this._props.onDoubleClick?.() ?? Cast(this.layoutDoc.onDoubleClick, ScriptField, null) ?? this.Document.onDoubleClick; } @computed get onPointerDownHandler() { - return this.props.onPointerDown?.() ?? ScriptCast(this.Document.onPointerDown); + return this._props.onPointerDown?.() ?? ScriptCast(this.Document.onPointerDown); } @computed get onPointerUpHandler() { - return this.props.onPointerUp?.() ?? ScriptCast(this.Document.onPointerUp); + return this._props.onPointerUp?.() ?? ScriptCast(this.Document.onPointerUp); } componentWillUnmount() { this.cleanupHandlers(true); } @observable _mounted = false; // turn off all pointer events if component isn't yet mounted (enables nested Docs in alternate UI textboxes that appear on hover which otherwise would grab focus from the text box, reverting to the original UI ) - @action + componentDidMount() { - this._mounted = true; + runInAction(() => (this._mounted = true)); this.setupHandlers(); this._disposers.contentActive = reaction( () => { // true - if the document has been activated directly or indirectly (by having its children selected) // false - if its pointer events are explicitly turned off or if it's container tells it that it's inactive // undefined - it is not active, but it should be responsive to actions that might activate it or its contents (eg clicking) - return this.props.isContentActive() === false || this.props.pointerEvents?.() === 'none' + return this._props.isContentActive() === false || this._props.pointerEvents?.() === 'none' ? false - : Doc.ActiveTool !== InkTool.None || SnappingManager.GetCanEmbed() || this.rootSelected() || this.Document.forceActive || this._componentView?.isAnyChildContentActive?.() || this.props.isContentActive() - ? true - : undefined; + : Doc.ActiveTool !== InkTool.None || SnappingManager.GetCanEmbed() || this.rootSelected() || this.Document.forceActive || this._componentView?.isAnyChildContentActive?.() || this._props.isContentActive() + ? true + : undefined; }, active => (this._isContentActive = active), { fireImmediately: true } ); this._disposers.pointerevents = reaction( - () => this.props.styleProvider?.(this.Document, this.props, StyleProp.PointerEvents), + () => this._props.styleProvider?.(this.Document, this._props, StyleProp.PointerEvents), pointerevents => (this._pointerEvents = pointerevents), { fireImmediately: true } ); } preDropFunc = (e: Event, de: DragManager.DropEvent) => { const dropAction = this.layoutDoc.dropAction as dropActionType; - if (de.complete.docDragData && this.isContentActive() && !this.props.treeViewDoc) { + if (de.complete.docDragData && this.isContentActive() && !this._props.treeViewDoc) { dropAction && (de.complete.docDragData.dropAction = dropAction); e.stopPropagation(); } @@ -375,38 +388,38 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps setupHandlers() { this.cleanupHandlers(false); if (this._mainCont.current) { - this._dropDisposer = DragManager.MakeDropTarget(this._mainCont.current, this.drop.bind(this), this.props.Document, this.preDropFunc); + this._dropDisposer = DragManager.MakeDropTarget(this._mainCont.current, this.drop.bind(this), this._props.Document, this.preDropFunc); } } - @action + cleanupHandlers(unbrush: boolean) { this._dropDisposer?.(); - unbrush && Doc.UnBrushDoc(this.props.Document); + unbrush && Doc.UnBrushDoc(this._props.Document); Object.values(this._disposers).forEach(disposer => disposer?.()); } startDragging(x: number, y: number, dropAction: dropActionType, hideSource = false) { if (this._mainCont.current) { const views = SelectionManager.Views().filter(dv => dv.docView?._mainCont.current); - const selected = views.length > 1 && views.some(dv => dv.Document === this.Document) ? views : [this.props.DocumentView()]; + const selected = views.length > 1 && views.some(dv => dv.Document === this.Document) ? views : [this._props.DocumentView()]; const dragData = new DragManager.DocumentDragData(selected.map(dv => dv.Document)); - const [left, top] = this.props.ScreenToLocalTransform().scale(this.NativeDimScaling).inverse().transformPoint(0, 0); - dragData.offset = this.props + const [left, top] = this._props.ScreenToLocalTransform().scale(this.NativeDimScaling).inverse().transformPoint(0, 0); + dragData.offset = this._props .ScreenToLocalTransform() .scale(this.NativeDimScaling) .transformDirection(x - left, y - top); dragData.dropAction = dropAction; - dragData.treeViewDoc = this.props.treeViewDoc; - dragData.removeDocument = this.props.removeDocument; - dragData.moveDocument = this.props.moveDocument; - dragData.draggedViews = [this.props.DocumentView()]; - dragData.canEmbed = this.Document.dragAction ?? this.props.dragAction ? true : false; + dragData.treeViewDoc = this._props.treeViewDoc; + dragData.removeDocument = this._props.removeDocument; + dragData.moveDocument = this._props.moveDocument; + dragData.draggedViews = [this._props.DocumentView()]; + dragData.canEmbed = this.Document.dragAction ?? this._props.dragAction ? true : false; DragManager.StartDocumentDrag( selected.map(dv => dv.docView!._mainCont.current!), dragData, x, y, - { hideSource: hideSource || (!dropAction && !this.layoutDoc.onDragStart && !this.props.dontHideOnDrag) } + { hideSource: hideSource || (!dropAction && !this.layoutDoc.onDragStart && !this._props.dontHideOnDrag) } ); // this needs to happen after the drop event is processed. } } @@ -429,28 +442,28 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps public static addDocTabFunc: (doc: Doc, location: OpenWhere) => boolean = returnFalse; onClick = action((e: React.MouseEvent | React.PointerEvent) => { - if (this.props.isGroupActive?.() === 'child' && !this.props.isDocumentActive?.()) return; - if (!this.Document.ignoreClick && this.props.renderDepth >= 0 && Utils.isClick(e.clientX, e.clientY, this._downX, this._downY, this._downTime)) { + if (this._props.isGroupActive?.() === 'child' && !this._props.isDocumentActive?.()) return; + if (!this.Document.ignoreClick && this._props.renderDepth >= 0 && Utils.isClick(e.clientX, e.clientY, this._downX, this._downY, this._downTime)) { let stopPropagate = true; let preventDefault = true; - !this.layoutDoc._keepZWhenDragged && this.props.bringToFront(this.Document); + !this.layoutDoc._keepZWhenDragged && this._props.bringToFront(this.Document); if (this._doubleTap) { - const defaultDblclick = this.props.defaultDoubleClick?.() || this.Document.defaultDoubleClick; + const defaultDblclick = this._props.defaultDoubleClick?.() || this.Document.defaultDoubleClick; if (this.onDoubleClickHandler?.script) { const { clientX, clientY, shiftKey, altKey, ctrlKey } = e; // or we could call e.persist() to capture variables // prettier-ignore const func = () => this.onDoubleClickHandler.script.run( { this: this.Document, - scriptContext: this.props.scriptContext, - documentView: this.props.DocumentView(), + scriptContext: this._props.scriptContext, + documentView: this._props.DocumentView(), clientX, clientY, altKey, shiftKey, ctrlKey, value: undefined, }, console.log ); - UndoManager.RunInBatch(() => (func().result?.select === true ? this.props.select(false) : ''), 'on double click'); + UndoManager.RunInBatch(() => (func().result?.select === true ? this._props.select(false) : ''), 'on double click'); } else if (!Doc.IsSystem(this.Document) && (defaultDblclick === undefined || defaultDblclick === 'default')) { UndoManager.RunInBatch(() => LightboxView.Instance.AddDocTab(this.Document, OpenWhere.lightbox), 'double tap'); SelectionManager.DeselectAll(); - Doc.UnBrushDoc(this.props.Document); + Doc.UnBrushDoc(this._props.Document); } else { this._singleClickFunc?.(); } @@ -467,13 +480,13 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps // e.g., if this document is part of a labeled 'lightbox' container, then documents will be shown in place // instead of in the global lightbox const oldFunc = DocumentViewInternal.addDocTabFunc; - DocumentViewInternal.addDocTabFunc = this.props.addDocTab; + DocumentViewInternal.addDocTabFunc = this._props.addDocTab; this.onClickHandler?.script.run( { this: this.Document, _readOnly_: false, - scriptContext: this.props.scriptContext, - documentView: this.props.DocumentView(), + scriptContext: this._props.scriptContext, + documentView: this._props.DocumentView(), clientX, clientY, shiftKey, @@ -482,14 +495,14 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps }, console.log ).result?.select === true - ? this.props.select(false) + ? this._props.select(false) : ''; DocumentViewInternal.addDocTabFunc = oldFunc; }; clickFunc = () => UndoManager.RunInBatch(func, 'click ' + this.Document.title); } else { // onDragStart implies a button doc that we don't want to select when clicking. RootDocument & isTemplateForField implies we're clicking on part of a template instance and we want to select the whole template, not the part - if ((this.layoutDoc.onDragStart || this.props.TemplateDataDocument) && !(e.ctrlKey || e.button > 0)) { + if ((this.layoutDoc.onDragStart || this._props.TemplateDataDocument) && !(e.ctrlKey || e.button > 0)) { stopPropagate = false; // don't stop propagation for field templates -- want the selection to propagate up to the root document of the template } preventDefault = false; @@ -497,10 +510,10 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps const sendToBack = e.altKey; this._singleClickFunc = // prettier-ignore - clickFunc ?? (() => (sendToBack ? this.props.DocumentView().props.bringToFront(this.Document, true) : + clickFunc ?? (() => (sendToBack ? this._props.DocumentView()._props.bringToFront(this.Document, true) : this._componentView?.select?.(e.ctrlKey || e.metaKey, e.shiftKey) ?? - this.props.select(e.ctrlKey||e.shiftKey, e.metaKey))); - const waitFordblclick = this.props.waitForDoubleClickToClick?.() ?? this.Document.waitForDoubleClickToClick; + this._props.select(e.ctrlKey||e.shiftKey, e.metaKey))); + const waitFordblclick = this._props.waitForDoubleClickToClick?.() ?? this.Document.waitForDoubleClickToClick; if ((clickFunc && waitFordblclick !== 'never') || waitFordblclick === 'always') { this._doubleClickTimeout && clearTimeout(this._doubleClickTimeout); this._doubleClickTimeout = setTimeout(this._singleClickFunc, 300); @@ -514,40 +527,39 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps } }); - @action onPointerDown = (e: React.PointerEvent): void => { - if (this.props.isGroupActive?.() === 'child' && !this.props.isDocumentActive?.()) return; + if (this._props.isGroupActive?.() === 'child' && !this._props.isDocumentActive?.()) return; this._longPressSelector = setTimeout(() => { if (DocumentView.LongPress) { if (this.Document.undoIgnoreFields) { runInAction(() => (UndoStack.HideInline = !UndoStack.HideInline)); } else { - this.props.select(false); + this._props.select(false); } } }, 1000); - if (!GestureOverlay.DownDocView) GestureOverlay.DownDocView = this.props.DocumentView(); + if (!GestureOverlay.DownDocView) GestureOverlay.DownDocView = this._props.DocumentView(); this._downX = e.clientX; this._downY = e.clientY; this._downTime = Date.now(); - if ((Doc.ActiveTool === InkTool.None || this.props.addDocTab === returnFalse) && !(this.props.TemplateDataDocument && !(e.ctrlKey || e.button > 0))) { + if ((Doc.ActiveTool === InkTool.None || this._props.addDocTab === returnFalse) && !(this._props.TemplateDataDocument && !(e.ctrlKey || e.button > 0))) { // click events stop here if the document is active and no modes are overriding it // if this is part of a template, let the event go up to the template root unless right/ctrl clicking if ( // prettier-ignore - (this.props.isDocumentActive?.() || this.props.isContentActive?.()) && - !this.props.onBrowseClick?.() && + (this._props.isDocumentActive?.() || this._props.isContentActive?.()) && + !this._props.onBrowseClick?.() && !this.Document.ignoreClick && e.button === 0 && !Doc.IsInMyOverlay(this.layoutDoc) ) { e.stopPropagation(); // don't preventDefault anymore. Goldenlayout, PDF text selection and RTF text selection all need it to go though - //if (this.props.isSelected(true) && this.Document.type !== DocumentType.PDF && this.layoutDoc._type_collection !== CollectionViewType.Docking) e.preventDefault(); + //if (this._props.isSelected(true) && this.Document.type !== DocumentType.PDF && this.layoutDoc._type_collection !== CollectionViewType.Docking) e.preventDefault(); // listen to move events if document content isn't active or document is draggable - if (!this.layoutDoc._lockedPosition && (!this.isContentActive() || BoolCast(this.layoutDoc._dragWhenActive, this.props.dragWhenActive))) { + if (!this.layoutDoc._lockedPosition && (!this.isContentActive() || BoolCast(this.layoutDoc._dragWhenActive, this._props.dragWhenActive))) { document.addEventListener('pointermove', this.onPointerMove); } } @@ -555,14 +567,13 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps } }; - @action onPointerMove = (e: PointerEvent): void => { if (e.buttons !== 1 || [InkTool.Highlighter, InkTool.Pen, InkTool.Write].includes(Doc.ActiveTool)) return; if (!Utils.isClick(e.clientX, e.clientY, this._downX, this._downY, Date.now())) { this.cleanupPointerEvents(); this._longPressSelector && clearTimeout(this._longPressSelector); - this.startDragging(this._downX, this._downY, ((e.ctrlKey || e.altKey) && 'embed') || ((this.Document.dragAction || this.props.dragAction || undefined) as dropActionType)); + this.startDragging(this._downX, this._downY, ((e.ctrlKey || e.altKey) && 'embed') || ((this.Document.dragAction || this._props.dragAction || undefined) as dropActionType)); } }; @@ -571,7 +582,6 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps document.removeEventListener('pointerup', this.onPointerUp); }; - @action onPointerUp = (e: PointerEvent): void => { this.cleanupPointerEvents(); this._longPressSelector && clearTimeout(this._longPressSelector); @@ -586,7 +596,6 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps }; @undoBatch - @action toggleFollowLink = (zoom?: boolean, setTargetToggle?: boolean): void => { const hadOnClick = this.Document.onClick; this.noOnClick(); @@ -594,8 +603,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps this.Document.waitForDoubleClickToClick = hadOnClick ? undefined : 'never'; }; @undoBatch - @action - followLinkOnClick = (): void => { + followLinkOnClick = () => { this.Document.ignoreClick = false; this.Document.onClick = FollowLinkScript(); this.Document.followLinkToggle = false; @@ -603,12 +611,12 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps this.Document.followLinkLocation = undefined; }; @undoBatch - noOnClick = (): void => { + noOnClick = () => { this.Document.ignoreClick = false; this.Document.onClick = Doc.GetProto(this.Document).onClick = undefined; }; - @undoBatch deleteClicked = () => this.props.removeDocument?.(this.props.Document); + @undoBatch deleteClicked = () => this._props.removeDocument?.(this._props.Document); @undoBatch setToggleDetail = () => (this.Document.onClick = ScriptField.MakeScript( `toggleDetail(documentView, "${StrCast(this.Document.layout_fieldKey) @@ -618,10 +626,9 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps )); @undoBatch - @action drop = (e: Event, de: DragManager.DropEvent) => { - if (this.props.dontRegisterView || this.props.LayoutTemplateString?.includes(LinkAnchorBox.name)) return false; - if (this.props.Document === Doc.ActiveDashboard) { + if (this._props.dontRegisterView || this._props.LayoutTemplateString?.includes(LinkAnchorBox.name)) return false; + if (this._props.Document === Doc.ActiveDashboard) { e.stopPropagation(); e.preventDefault(); alert( @@ -643,7 +650,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps de.complete.linkDocument = DocUtils.MakeLink(linkdrag.linkSourceDoc, dropDoc, {}, undefined, [de.x, de.y - 50]); if (de.complete.linkDocument) { de.complete.linkDocument.layout_isSvg = true; - this.props.DocumentView().CollectionFreeFormView?.addDocument(de.complete.linkDocument); + this._props.DocumentView().CollectionFreeFormView?.addDocument(de.complete.linkDocument); } } e.stopPropagation(); @@ -654,18 +661,17 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps }; @undoBatch - @action makeIntoPortal = () => { - const portalLink = this.allLinks.find(d => d.link_anchor_1 === this.props.Document && d.link_relationship === 'portal to:portal from'); + const portalLink = this.allLinks.find(d => d.link_anchor_1 === this._props.Document && d.link_relationship === 'portal to:portal from'); if (!portalLink) { DocUtils.MakeLink( - this.props.Document, + this._props.Document, Docs.Create.FreeformDocument([], { _width: NumCast(this.layoutDoc._width) + 10, _height: Math.max(NumCast(this.layoutDoc._height), NumCast(this.layoutDoc._width) + 10), _isLightbox: true, _layout_fitWidth: true, - title: StrCast(this.props.Document.title) + ' [Portal]', + title: StrCast(this._props.Document.title) + ' [Portal]', }), { link_relationship: 'portal to:portal from' } ); @@ -683,7 +689,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps const batch = UndoManager.StartBatch('importing'); Doc.importDocument(input.files[0]).then(doc => { if (doc instanceof Doc) { - this.props.addDocTab(doc, OpenWhere.addRight); + this._props.addDocTab(doc, OpenWhere.addRight); batch.end(); } }); @@ -692,12 +698,11 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps input.click(); }; - @action onContextMenu = (e?: React.MouseEvent, pageX?: number, pageY?: number) => { if (e && this.layoutDoc._layout_hideContextMenu && Doc.noviceMode) { e.preventDefault(); e.stopPropagation(); - //!this.props.isSelected(true) && SelectionManager.SelectView(this.props.DocumentView(), false); + //!this._props.isSelected(true) && SelectionManager.SelectView(this._props.DocumentView(), false); } // the touch onContextMenu is button 0, the pointer onContextMenu is button 2 if (e) { @@ -719,7 +724,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps if (e && !(e.nativeEvent as any).dash) { const onDisplay = () => { - if (this.Document.type !== DocumentType.MAP) DocumentViewInternal.SelectAfterContextMenu && !this.props.isSelected() && SelectionManager.SelectView(this.props.DocumentView(), false); // on a mac, the context menu is triggered on mouse down, but a YouTube video becaomes interactive when selected which means that the context menu won't show up. by delaying the selection until hopefully after the pointer up, the context menu will appear. + if (this.Document.type !== DocumentType.MAP) DocumentViewInternal.SelectAfterContextMenu && !this._props.isSelected() && SelectionManager.SelectView(this._props.DocumentView(), false); // on a mac, the context menu is triggered on mouse down, but a YouTube video becaomes interactive when selected which means that the context menu won't show up. by delaying the selection until hopefully after the pointer up, the context menu will appear. setTimeout(() => simulateMouseClick(document.elementFromPoint(e.clientX, e.clientY), e.clientX, e.clientY, e.screenX, e.screenY)); }; if (navigator.userAgent.includes('Macintosh')) { @@ -730,33 +735,33 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps return; } - const customScripts = Cast(this.props.Document.contextMenuScripts, listSpec(ScriptField), []); + const customScripts = Cast(this._props.Document.contextMenuScripts, listSpec(ScriptField), []); StrListCast(this.Document.contextMenuLabels).forEach((label, i) => - cm.addItem({ description: label, event: () => customScripts[i]?.script.run({ documentView: this, this: this.Document, scriptContext: this.props.scriptContext }), icon: 'sticky-note' }) + cm.addItem({ description: label, event: () => customScripts[i]?.script.run({ documentView: this, this: this.Document, scriptContext: this._props.scriptContext }), icon: 'sticky-note' }) ); - this.props.contextMenuItems?.().forEach(item => item.label && cm.addItem({ description: item.label, event: () => item.script.script.run({ this: this.Document, scriptContext: this.props.scriptContext }), icon: item.icon as IconProp })); + this._props.contextMenuItems?.().forEach(item => item.label && cm.addItem({ description: item.label, event: () => item.script.script.run({ this: this.Document, scriptContext: this._props.scriptContext }), icon: item.icon as IconProp })); - if (!this.props.Document.isFolder) { - const templateDoc = Cast(this.props.Document[StrCast(this.props.Document.layout_fieldKey)], Doc, null); + if (!this._props.Document.isFolder) { + const templateDoc = Cast(this._props.Document[StrCast(this._props.Document.layout_fieldKey)], Doc, null); const appearance = cm.findByDescription('Appearance...'); const appearanceItems: ContextMenuProps[] = appearance && 'subitems' in appearance ? appearance.subitems : []; - if (this.props.renderDepth === 0) { + if (this._props.renderDepth === 0) { appearanceItems.splice(0, 0, { description: 'Open in Lightbox', event: () => LightboxView.Instance.SetLightboxDoc(this.Document), icon: 'external-link-alt' }); } - appearanceItems.push({ description: 'Pin', event: () => this.props.pinToPres(this.Document, {}), icon: 'eye' }); - !Doc.noviceMode && templateDoc && appearanceItems.push({ description: 'Open Template ', event: () => this.props.addDocTab(templateDoc, OpenWhere.addRight), icon: 'eye' }); + appearanceItems.push({ description: 'Pin', event: () => this._props.pinToPres(this.Document, {}), icon: 'eye' }); + !Doc.noviceMode && templateDoc && appearanceItems.push({ description: 'Open Template ', event: () => this._props.addDocTab(templateDoc, OpenWhere.addRight), icon: 'eye' }); !appearance && appearanceItems.length && cm.addItem({ description: 'Appearance...', subitems: appearanceItems, icon: 'compass' }); if (!Doc.IsSystem(this.Document) && this.Document.type !== DocumentType.PRES && ![CollectionViewType.Docking, CollectionViewType.Tree].includes(this.Document._type_collection as any)) { const existingOnClick = cm.findByDescription('OnClick...'); const onClicks: ContextMenuProps[] = existingOnClick && 'subitems' in existingOnClick ? existingOnClick.subitems : []; - if (this.props.bringToFront !== emptyFunction) { + if (this._props.bringToFront !== emptyFunction) { const zorders = cm.findByDescription('ZOrder...'); const zorderItems: ContextMenuProps[] = zorders && 'subitems' in zorders ? zorders.subitems : []; - zorderItems.push({ description: 'Bring to Front', event: () => SelectionManager.Views().forEach(dv => dv.props.bringToFront(dv.Document, false)), icon: 'arrow-up' }); - zorderItems.push({ description: 'Send to Back', event: () => SelectionManager.Views().forEach(dv => dv.props.bringToFront(dv.Document, true)), icon: 'arrow-down' }); + zorderItems.push({ description: 'Bring to Front', event: () => SelectionManager.Views().forEach(dv => dv._props.bringToFront(dv.Document, false)), icon: 'arrow-up' }); + zorderItems.push({ description: 'Send to Back', event: () => SelectionManager.Views().forEach(dv => dv._props.bringToFront(dv.Document, true)), icon: 'arrow-down' }); zorderItems.push({ description: !this.layoutDoc._keepZDragged ? 'Keep ZIndex when dragged' : 'Allow ZIndex to change when dragged', event: undoBatch(action(() => (this.layoutDoc._keepZWhenDragged = !this.layoutDoc._keepZWhenDragged))), @@ -768,14 +773,14 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps onClicks.push({ description: 'Enter Portal', event: this.makeIntoPortal, icon: 'window-restore' }); !Doc.noviceMode && onClicks.push({ description: 'Toggle Detail', event: this.setToggleDetail, icon: 'concierge-bell' }); - if (!this.props.treeViewDoc) { + if (!this._props.treeViewDoc) { if (!this.Document.annotationOn) { const options = cm.findByDescription('Options...'); const optionItems: ContextMenuProps[] = options && 'subitems' in options ? options.subitems : []; !options && cm.addItem({ description: 'Options...', subitems: optionItems, icon: 'compass' }); onClicks.push({ description: this.onClickHandler ? 'Remove Click Behavior' : 'Follow Link', event: () => this.toggleFollowLink(false, false), icon: 'link' }); - !Doc.noviceMode && onClicks.push({ description: 'Edit onClick Script', event: () => UndoManager.RunInBatch(() => DocUtils.makeCustomViewClicked(this.props.Document, undefined, 'onClick'), 'edit onClick'), icon: 'terminal' }); + !Doc.noviceMode && onClicks.push({ description: 'Edit onClick Script', event: () => UndoManager.RunInBatch(() => DocUtils.makeCustomViewClicked(this._props.Document, undefined, 'onClick'), 'edit onClick'), icon: 'terminal' }); !existingOnClick && cm.addItem({ description: 'OnClick...', noexpand: true, subitems: onClicks, icon: 'mouse-pointer' }); } else if (LinkManager.Links(this.Document).length) { onClicks.push({ description: 'Restore On Click default', event: () => this.noOnClick(), icon: 'link' }); @@ -797,15 +802,15 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps const moreItems = more && 'subitems' in more ? more.subitems : []; if (!Doc.IsSystem(this.Document)) { if (!Doc.noviceMode) { - moreItems.push({ description: 'Make View of Metadata Field', event: () => Doc.MakeMetadataFieldTemplate(this.props.Document, this.props.TemplateDataDocument), icon: 'concierge-bell' }); + moreItems.push({ description: 'Make View of Metadata Field', event: () => Doc.MakeMetadataFieldTemplate(this._props.Document, this._props.TemplateDataDocument), icon: 'concierge-bell' }); moreItems.push({ description: `${this.Document._chromeHidden ? 'Show' : 'Hide'} Chrome`, event: () => (this.Document._chromeHidden = !this.Document._chromeHidden), icon: 'project-diagram' }); - if (Cast(Doc.GetProto(this.props.Document).data, listSpec(Doc))) { - moreItems.push({ description: 'Export to Google Photos Album', event: () => GooglePhotos.Export.CollectionToAlbum({ collection: this.props.Document }).then(console.log), icon: 'caret-square-right' }); - moreItems.push({ description: 'Tag Child Images via Google Photos', event: () => GooglePhotos.Query.TagChildImages(this.props.Document), icon: 'caret-square-right' }); - moreItems.push({ description: 'Write Back Link to Album', event: () => GooglePhotos.Transactions.AddTextEnrichment(this.props.Document), icon: 'caret-square-right' }); + if (Cast(Doc.GetProto(this._props.Document).data, listSpec(Doc))) { + moreItems.push({ description: 'Export to Google Photos Album', event: () => GooglePhotos.Export.CollectionToAlbum({ collection: this._props.Document }).then(console.log), icon: 'caret-square-right' }); + moreItems.push({ description: 'Tag Child Images via Google Photos', event: () => GooglePhotos.Query.TagChildImages(this._props.Document), icon: 'caret-square-right' }); + moreItems.push({ description: 'Write Back Link to Album', event: () => GooglePhotos.Transactions.AddTextEnrichment(this._props.Document), icon: 'caret-square-right' }); } - moreItems.push({ description: 'Copy ID', event: () => Utils.CopyText(Doc.globalServerPath(this.props.Document)), icon: 'fingerprint' }); + moreItems.push({ description: 'Copy ID', event: () => Utils.CopyText(Doc.globalServerPath(this._props.Document)), icon: 'fingerprint' }); } } @@ -813,25 +818,25 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps } const constantItems: ContextMenuProps[] = []; if (!Doc.IsSystem(this.Document) && this.Document._type_collection !== CollectionViewType.Docking) { - constantItems.push({ description: 'Zip Export', icon: 'download', event: async () => Doc.Zip(this.props.Document) }); - (this.Document._type_collection !== CollectionViewType.Docking || !Doc.noviceMode) && constantItems.push({ description: 'Share', event: () => SharingManager.Instance.open(this.props.DocumentView()), icon: 'users' }); - if (this.props.removeDocument && Doc.ActiveDashboard !== this.props.Document) { + constantItems.push({ description: 'Zip Export', icon: 'download', event: async () => Doc.Zip(this._props.Document) }); + (this.Document._type_collection !== CollectionViewType.Docking || !Doc.noviceMode) && constantItems.push({ description: 'Share', event: () => SharingManager.Instance.open(this._props.DocumentView()), icon: 'users' }); + if (this._props.removeDocument && Doc.ActiveDashboard !== this._props.Document) { // need option to gray out menu items ... preferably with a '?' that explains why they're grayed out (eg., no permissions) constantItems.push({ description: 'Close', event: this.deleteClicked, icon: 'times' }); } } - constantItems.push({ description: 'Show Metadata', event: () => this.props.addDocTab(this.props.Document, OpenWhere.addRightKeyvalue), icon: 'table-columns' }); + constantItems.push({ description: 'Show Metadata', event: () => this._props.addDocTab(this._props.Document, OpenWhere.addRightKeyvalue), icon: 'table-columns' }); cm.addItem({ description: 'General...', noexpand: false, subitems: constantItems, icon: 'question' }); const help = cm.findByDescription('Help...'); const helpItems: ContextMenuProps[] = help && 'subitems' in help ? help.subitems : []; - !Doc.noviceMode && helpItems.push({ description: 'Text Shortcuts Ctrl+/', event: () => this.props.addDocTab(Docs.Create.PdfDocument('/assets/cheat-sheet.pdf', { _width: 300, _height: 300 }), OpenWhere.addRight), icon: 'keyboard' }); + !Doc.noviceMode && helpItems.push({ description: 'Text Shortcuts Ctrl+/', event: () => this._props.addDocTab(Docs.Create.PdfDocument('/assets/cheat-sheet.pdf', { _width: 300, _height: 300 }), OpenWhere.addRight), icon: 'keyboard' }); !Doc.noviceMode && helpItems.push({ description: 'Print Document in Console', event: () => console.log(this.Document), icon: 'hand-point-right' }); !Doc.noviceMode && helpItems.push({ description: 'Print DataDoc in Console', event: () => console.log(this.dataDoc), icon: 'hand-point-right' }); let documentationDescription: string | undefined = undefined; let documentationLink: string | undefined = undefined; - switch (this.props.Document.type) { + switch (this._props.Document.type) { case DocumentType.COL: documentationDescription = 'See collection documentation'; documentationLink = 'https://brown-dash.github.io/Dash-Documentation/views/'; @@ -866,7 +871,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps break; } // Add link to help documentation - if (!this.props.treeViewDoc && documentationDescription && documentationLink) { + if (!this._props.treeViewDoc && documentationDescription && documentationLink) { helpItems.push({ description: documentationDescription, event: () => window.open(documentationLink, '_blank'), @@ -881,26 +886,26 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps }; @computed get _rootSelected() { - return this.props.isSelected() || BoolCast(this.props.TemplateDataDocument && this.props.rootSelected?.()); + return this._props.isSelected() || BoolCast(this._props.TemplateDataDocument && this._props.rootSelected?.()); } rootSelected = () => this._rootSelected; - panelHeight = () => this.props.PanelHeight() - this.headerMargin; - screenToLocal = () => this.props.ScreenToLocalTransform().translate(0, -this.headerMargin); + panelHeight = () => this._props.PanelHeight() - this.headerMargin; + screenToLocal = () => this._props.ScreenToLocalTransform().translate(0, -this.headerMargin); onClickFunc: any = () => (this.disableClickScriptFunc ? undefined : this.onClickHandler); - setHeight = (height: number) => !this.props.suppressSetHeight && (this.layoutDoc._height = height); + setHeight = (height: number) => !this._props.suppressSetHeight && (this.layoutDoc._height = height); setContentView = action((view: { getAnchor?: (addAsAnnotation: boolean) => Doc; forward?: () => boolean; back?: () => boolean }) => (this._componentView = view)); - @observable _isContentActive: boolean | undefined; + @observable _isContentActive: boolean | undefined = undefined; isContentActive = (): boolean | undefined => this._isContentActive; - childFilters = () => [...this.props.childFilters(), ...StrListCast(this.layoutDoc.childFilters)]; + childFilters = () => [...this._props.childFilters(), ...StrListCast(this.layoutDoc.childFilters)]; /// disable pointer events on content when there's an enabled onClick script (but not the browse script) and the contents aren't forced active, or if contents are marked inactive @computed get _contentPointerEvents() { TraceMobx(); - return this.props.contentPointerEvents ?? + return this._props.contentPointerEvents ?? ((!this.disableClickScriptFunc && // this.onClickHandler && - !this.props.onBrowseClick?.() && + !this._props.onBrowseClick?.() && this.isContentActive() !== true) || this.isContentActive() === false) ? 'none' @@ -909,8 +914,8 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps contentPointerEvents = () => this._contentPointerEvents; @computed get contents() { TraceMobx(); - const isInk = this.layoutDoc._layout_isSvg && !this.props.LayoutTemplateString; - const noBackground = this.Document.isGroup && !this.props.LayoutTemplateString?.includes(KeyValueBox.name) && (!this.layoutDoc.backgroundColor || this.layoutDoc.backgroundColor === 'transparent'); + const isInk = this.layoutDoc._layout_isSvg && !this._props.LayoutTemplateString; + const noBackground = this.Document.isGroup && !this._props.LayoutTemplateString?.includes(KeyValueBox.name) && (!this.layoutDoc.backgroundColor || this.layoutDoc.backgroundColor === 'transparent'); return ( <div className="documentView-contentsView" @@ -920,10 +925,10 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps }}> <DocumentContentsView key={1} - {...this.props} + {...this._props} fieldKey="" pointerEvents={this.contentPointerEvents} - docViewPath={this.props.viewPath} + docViewPath={this._props.viewPath} setContentView={this.setContentView} childFilters={this.childFilters} PanelHeight={this.panelHeight} @@ -940,8 +945,8 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps ); } - anchorPanelWidth = () => this.props.PanelWidth() || 1; - anchorPanelHeight = () => this.props.PanelHeight() || 1; + anchorPanelWidth = () => this._props.PanelWidth() || 1; + anchorPanelHeight = () => this._props.PanelHeight() || 1; anchorStyleProvider = (doc: Opt<Doc>, props: Opt<DocumentViewProps>, property: string): any => { // prettier-ignore switch (property.split(':')[0]) { @@ -949,11 +954,11 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps case StyleProp.PointerEvents: return 'none'; case StyleProp.Highlighting: return undefined; case StyleProp.Opacity: { - const filtered = DocUtils.FilterDocs(this.directLinks, this.props.childFilters?.() ?? [], []).filter(d => d.link_displayLine || Doc.UserDoc().showLinkLines); + const filtered = DocUtils.FilterDocs(this.directLinks, this._props.childFilters?.() ?? [], []).filter(d => d.link_displayLine || Doc.UserDoc().showLinkLines); return filtered.some(link => link._link_displayArrow) ? 0 : undefined; } } - return this.props.styleProvider?.(doc, props, property); + return this._props.styleProvider?.(doc, props, property); }; // We need to use allrelatedLinks to get not just links to the document as a whole, but links to // anchors that are not rendered as DocumentViews (marked as 'layout_unrendered' with their 'annotationOn' set to this document). e.g., @@ -979,15 +984,15 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps @computed get allLinkEndpoints() { // the small blue dots that mark the endpoints of links TraceMobx(); - if (this._componentView instanceof KeyValueBox || this.props.hideLinkAnchors || this.layoutDoc.layout_hideLinkAnchors || this.props.dontRegisterView || this.layoutDoc.layout_unrendered) return null; - const filtered = DocUtils.FilterDocs(this.directLinks, this.props.childFilters?.() ?? [], []).filter(d => d.link_displayLine || Doc.UserDoc().showLinkLines); + if (this._componentView instanceof KeyValueBox || this._props.hideLinkAnchors || this.layoutDoc.layout_hideLinkAnchors || this._props.dontRegisterView || this.layoutDoc.layout_unrendered) return null; + const filtered = DocUtils.FilterDocs(this.directLinks, this._props.childFilters?.() ?? [], []).filter(d => d.link_displayLine || Doc.UserDoc().showLinkLines); return filtered.map(link => ( <div className="documentView-anchorCont" key={link[Id]}> <DocumentView - {...this.props} + {...this._props} isContentActive={returnFalse} Document={link} - docViewPath={this.props.viewPath} + docViewPath={this._props.viewPath} PanelWidth={this.anchorPanelWidth} PanelHeight={this.anchorPanelHeight} dontRegisterView={false} @@ -1077,7 +1082,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps } }; - captionStyleProvider = (doc: Opt<Doc>, props: Opt<DocumentViewProps>, property: string) => this.props?.styleProvider?.(doc, props, property + ':caption'); + captionStyleProvider = (doc: Opt<Doc>, props: Opt<DocumentViewProps>, property: string) => this._props?.styleProvider?.(doc, props, property + ':caption'); @observable _changingTitleField = false; @observable _dropDownInnerWidth = 0; fieldsDropdown = (inputOptions: string[], dropdownWidth: number, placeholder: string, onChange: (val: string | number) => void, onClose: () => void) => { @@ -1121,12 +1126,12 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps <div className="documentView-captionWrapper" style={{ - pointerEvents: this.Document.ignoreClick ? 'none' : this.isContentActive() || this.props.isDocumentActive?.() ? 'all' : undefined, + pointerEvents: this.Document.ignoreClick ? 'none' : this.isContentActive() || this._props.isDocumentActive?.() ? 'all' : undefined, background: StrCast(this.layoutDoc._backgroundColor, 'rgba(0,0,0,0.2)'), color: lightOrDark(StrCast(this.layoutDoc._backgroundColor, 'black')), }}> <FormattedTextBox - {...this.props} + {...this._props} yPadding={10} xPadding={10} fieldKey={this.layout_showCaption} @@ -1134,7 +1139,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps dontRegisterView={true} noSidebar={true} dontScale={true} - renderDepth={this.props.renderDepth} + renderDepth={this._props.renderDepth} isContentActive={this.isContentActive} /> </div> @@ -1160,7 +1165,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps width: 100 - sidebarWidthPercent + '%', color: background === 'transparent' ? SettingsManager.userColor : lightOrDark(background), background, - pointerEvents: (!this.disableClickScriptFunc && this.onClickHandler) || this.Document.ignoreClick ? 'none' : this.isContentActive() || this.props.isDocumentActive?.() ? 'all' : undefined, + pointerEvents: (!this.disableClickScriptFunc && this.onClickHandler) || this.Document.ignoreClick ? 'none' : this.isContentActive() || this._props.isDocumentActive?.() ? 'all' : undefined, }}> {!dropdownWidth ? null @@ -1171,7 +1176,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps action((field: string | number) => { if (this.layoutDoc.layout_showTitle) { this.layoutDoc._layout_showTitle = field; - } else if (!this.props.layout_showTitle) { + } else if (!this._props.layout_showTitle) { Doc.UserDoc().layout_showTitle = field; } this._changingTitleField = false; @@ -1199,7 +1204,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps if (input?.startsWith('#')) { if (this.layoutDoc.layout_showTitle) { this.layoutDoc._layout_showTitle = input?.substring(1); - } else if (!this.props.layout_showTitle) { + } else if (!this._props.layout_showTitle) { Doc.UserDoc().layout_showTitle = input?.substring(1) ?? 'author_date'; } } else if (showTitle && !showTitle.includes('Date') && showTitle !== 'author') { @@ -1211,7 +1216,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps </div> </div> ); - return this.props.hideTitle || (!showTitle && !this.layout_showCaption) ? ( + return this._props.hideTitle || (!showTitle && !this.layout_showCaption) ? ( this.contents ) : ( <div className="documentView-styleWrapper"> @@ -1267,31 +1272,31 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps switch (StrCast(presEffectDoc?.presentation_effect, StrCast(presEffectDoc?.followLinkAnimEffect))) { default: case PresEffect.None: return renderDoc; - case PresEffect.Zoom: return <Zoom {...effectProps}>{renderDoc}</Zoom>; - case PresEffect.Fade: return <Fade {...effectProps}>{renderDoc}</Fade>; - case PresEffect.Flip: return <Flip {...effectProps}>{renderDoc}</Flip>; - case PresEffect.Rotate: return <Rotate {...effectProps}>{renderDoc}</Rotate>; - case PresEffect.Bounce: return <Bounce {...effectProps}>{renderDoc}</Bounce>; - case PresEffect.Roll: return <Roll {...effectProps}>{renderDoc}</Roll>; - case PresEffect.Lightspeed: return <LightSpeed {...effectProps}>{renderDoc}</LightSpeed>; + // case PresEffect.Zoom: return <Zoom {...effectProps}>{renderDoc}</Zoom>; + // case PresEffect.Fade: return <Fade {...effectProps}>{renderDoc}</Fade>; + // case PresEffect.Flip: return <Flip {...effectProps}>{renderDoc}</Flip>; + // case PresEffect.Rotate: return <Rotate {...effectProps}>{renderDoc}</Rotate>; + // case PresEffect.Bounce: return <Bounce {...effectProps}>{renderDoc}</Bounce>; + // case PresEffect.Roll: return <Roll {...effectProps}>{renderDoc}</Roll>; + // case PresEffect.Lightspeed: return <LightSpeed {...effectProps}>{renderDoc}</LightSpeed>; } } @computed get highlighting() { - return this.props.styleProvider?.(this.Document, this.props, StyleProp.Highlighting); + return this._props.styleProvider?.(this.Document, this._props, StyleProp.Highlighting); } @computed get borderPath() { - return this.props.styleProvider?.(this.Document, this.props, StyleProp.BorderPath); + return this._props.styleProvider?.(this.Document, this._props, StyleProp.BorderPath); } render() { TraceMobx(); const highlighting = this.highlighting; const borderPath = this.borderPath; const boxShadow = - this.props.treeViewDoc || !highlighting + this._props.treeViewDoc || !highlighting ? this.boxShadow : highlighting && this.borderRounding && highlighting.highlightStyle !== 'dashed' - ? `0 0 0 ${highlighting.highlightIndex}px ${highlighting.highlightColor}` - : this.boxShadow || (this.Document.isTemplateForField ? 'black 0.2vw 0.2vw 0.8vw' : undefined); + ? `0 0 0 ${highlighting.highlightIndex}px ${highlighting.highlightColor}` + : this.boxShadow || (this.Document.isTemplateForField ? 'black 0.2vw 0.2vw 0.8vw' : undefined); const renderDoc = this.renderDoc({ borderRadius: this.borderRounding, outline: highlighting && !this.borderRounding && !highlighting.highlightStroke ? `${highlighting.highlightColor} ${highlighting.highlightStyle} ${highlighting.highlightIndex}px` : 'solid 0px', @@ -1326,6 +1331,18 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps @observer export class DocumentView extends React.Component<DocumentViewProps> { public static ROOT_DIV = 'documentView-effectsWrapper'; + + @observable _props: DocumentViewProps; + _prevProps: DocumentViewProps; + constructor(props: any) { + super(props); + this._props = this._prevProps = props; + makeObservable(this); + } + componentDidUpdate() { + copyProps(this); + } + @observable _selected = false; public get SELECTED() { return this._selected; @@ -1340,14 +1357,14 @@ export class DocumentView extends React.Component<DocumentViewProps> { @computed public static get exploreMode() { return () => (DocumentView.ExploreMode ? ScriptField.MakeScript('CollectionBrowseClick(documentView, clientX, clientY)', { documentView: 'any', clientX: 'number', clientY: 'number' })! : undefined); } - @observable public docView: DocumentViewInternal | undefined | null; + @observable public docView: DocumentViewInternal | undefined | null = undefined; @observable public textHtmlOverlay: Opt<string>; @observable public textHtmlOverlayTime: Opt<number>; @observable private _isHovering = false; public htmlOverlayEffect: Opt<Doc>; public get displayName() { - return 'DocumentView(' + this.props.Document?.title + ')'; + return 'DocumentView(' + this._props.Document?.title + ')'; } // this makes mobx trace() statements more descriptive public ContentRef = React.createRef<HTMLDivElement>(); public ViewTimer: NodeJS.Timeout | undefined; // timer for res @@ -1405,10 +1422,10 @@ export class DocumentView extends React.Component<DocumentViewProps> { } get Document() { - return this.props.Document; + return this._props.Document; } get topMost() { - return this.props.renderDepth === 0; + return this._props.renderDepth === 0; } get dataDoc() { return this.docView?.dataDoc ?? this.Document; @@ -1426,32 +1443,32 @@ export class DocumentView extends React.Component<DocumentViewProps> { return this.docView?.LayoutFieldKey || 'layout'; } @computed get layout_fitWidth() { - return this.props.layout_fitWidth?.(this.layoutDoc) ?? this.layoutDoc?.layout_fitWidth; + return this._props.layout_fitWidth?.(this.layoutDoc) ?? this.layoutDoc?.layout_fitWidth; } @computed get anchorViewDoc() { - return this.props.LayoutTemplateString?.includes('link_anchor_2') ? DocCast(this.Document['link_anchor_2']) : this.props.LayoutTemplateString?.includes('link_anchor_1') ? DocCast(this.Document['link_anchor_1']) : undefined; + return this._props.LayoutTemplateString?.includes('link_anchor_2') ? DocCast(this.Document['link_anchor_2']) : this._props.LayoutTemplateString?.includes('link_anchor_1') ? DocCast(this.Document['link_anchor_1']) : undefined; } @computed get hideLinkButton() { - return this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.HideLinkBtn + (this.SELECTED ? ':selected' : '')); + return this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.HideLinkBtn + (this.SELECTED ? ':selected' : '')); } - hideLinkCount = () => this.props.renderDepth === -1 || (this.SELECTED && this.props.renderDepth) || !this._isHovering || this.hideLinkButton; + hideLinkCount = () => this._props.renderDepth === -1 || (this.SELECTED && this._props.renderDepth) || !this._isHovering || this.hideLinkButton; @computed get linkCountView() { return <DocumentLinksButton hideCount={this.hideLinkCount} View={this} scaling={this.scaleToScreenSpace} OnHover={true} Bottom={this.topMost} ShowCount={true} />; } @computed get docViewPath(): DocumentView[] { - return this.props.docViewPath ? [...this.props.docViewPath(), this] : [this]; + return this._props.docViewPath ? [...this._props.docViewPath(), this] : [this]; } @computed get layoutDoc() { - return Doc.Layout(this.Document, this.props.LayoutTemplate?.()); + return Doc.Layout(this.Document, this._props.LayoutTemplate?.()); } @computed get nativeWidth() { - return this.props.LayoutTemplateString?.includes(KeyValueBox.name) ? 0 : returnVal(this.props.NativeWidth?.(), Doc.NativeWidth(this.layoutDoc, this.props.TemplateDataDocument, !this.layout_fitWidth)); + return this._props.LayoutTemplateString?.includes(KeyValueBox.name) ? 0 : returnVal(this._props.NativeWidth?.(), Doc.NativeWidth(this.layoutDoc, this._props.TemplateDataDocument, !this.layout_fitWidth)); } @computed get nativeHeight() { - return this.props.LayoutTemplateString?.includes(KeyValueBox.name) ? 0 : returnVal(this.props.NativeHeight?.(), Doc.NativeHeight(this.layoutDoc, this.props.TemplateDataDocument, !this.layout_fitWidth)); + return this._props.LayoutTemplateString?.includes(KeyValueBox.name) ? 0 : returnVal(this._props.NativeHeight?.(), Doc.NativeHeight(this.layoutDoc, this._props.TemplateDataDocument, !this.layout_fitWidth)); } @computed get shouldNotScale() { - return (this.layout_fitWidth && !this.nativeWidth) || this.props.LayoutTemplateString?.includes(KeyValueBox.name) || [CollectionViewType.Docking].includes(this.Document._type_collection as any); + return (this.layout_fitWidth && !this.nativeWidth) || this._props.LayoutTemplateString?.includes(KeyValueBox.name) || [CollectionViewType.Docking].includes(this.Document._type_collection as any); } @computed get effectiveNativeWidth() { return this.shouldNotScale ? 0 : this.nativeWidth || NumCast(this.layoutDoc.width); @@ -1462,57 +1479,57 @@ export class DocumentView extends React.Component<DocumentViewProps> { @computed get nativeScaling() { if (this.shouldNotScale) return 1; const minTextScale = this.Document.type === DocumentType.RTF ? 0.1 : 0; - if (this.layout_fitWidth || this.props.PanelHeight() / (this.effectiveNativeHeight || 1) > this.props.PanelWidth() / (this.effectiveNativeWidth || 1)) { - return Math.max(minTextScale, this.props.PanelWidth() / (this.effectiveNativeWidth || 1)); // width-limited or layout_fitWidth + if (this.layout_fitWidth || this._props.PanelHeight() / (this.effectiveNativeHeight || 1) > this._props.PanelWidth() / (this.effectiveNativeWidth || 1)) { + return Math.max(minTextScale, this._props.PanelWidth() / (this.effectiveNativeWidth || 1)); // width-limited or layout_fitWidth } - return Math.max(minTextScale, this.props.PanelHeight() / (this.effectiveNativeHeight || 1)); // height-limited or unscaled + return Math.max(minTextScale, this._props.PanelHeight() / (this.effectiveNativeHeight || 1)); // height-limited or unscaled } @computed get panelWidth() { - return this.effectiveNativeWidth ? this.effectiveNativeWidth * this.nativeScaling : this.props.PanelWidth(); + return this.effectiveNativeWidth ? this.effectiveNativeWidth * this.nativeScaling : this._props.PanelWidth(); } @computed get panelHeight() { if (this.effectiveNativeHeight && (!this.layout_fitWidth || !this.layoutDoc.layout_reflowVertical)) { - return Math.min(this.props.PanelHeight(), this.effectiveNativeHeight * this.nativeScaling); + return Math.min(this._props.PanelHeight(), this.effectiveNativeHeight * this.nativeScaling); } - return this.props.PanelHeight(); + return this._props.PanelHeight(); } @computed get Xshift() { - return this.effectiveNativeWidth ? Math.max(0, (this.props.PanelWidth() - this.effectiveNativeWidth * this.nativeScaling) / 2) : 0; + return this.effectiveNativeWidth ? Math.max(0, (this._props.PanelWidth() - this.effectiveNativeWidth * this.nativeScaling) / 2) : 0; } @computed get Yshift() { return this.effectiveNativeWidth && this.effectiveNativeHeight && Math.abs(this.Xshift) < 0.001 && - (!this.layoutDoc.layout_reflowVertical || (!this.layout_fitWidth && this.effectiveNativeHeight * this.nativeScaling <= this.props.PanelHeight())) - ? Math.max(0, (this.props.PanelHeight() - this.effectiveNativeHeight * this.nativeScaling) / 2) + (!this.layoutDoc.layout_reflowVertical || (!this.layout_fitWidth && this.effectiveNativeHeight * this.nativeScaling <= this._props.PanelHeight())) + ? Math.max(0, (this._props.PanelHeight() - this.effectiveNativeHeight * this.nativeScaling) / 2) : 0; } @computed get centeringX() { - return this.props.dontCenter?.includes('x') ? 0 : this.Xshift; + return this._props.dontCenter?.includes('x') ? 0 : this.Xshift; } @computed get centeringY() { - return this.props.dontCenter?.includes('y') ? 0 : this.Yshift; + return this._props.dontCenter?.includes('y') ? 0 : this.Yshift; } @computed get CollectionFreeFormView() { return this.CollectionFreeFormDocumentView?.CollectionFreeFormView; } @computed get CollectionFreeFormDocumentView() { - return this.props.CollectionFreeFormDocumentView?.(); + return this._props.CollectionFreeFormDocumentView?.(); } - public toggleNativeDimensions = () => this.docView && this.Document.type !== DocumentType.INK && Doc.toggleNativeDimensions(this.layoutDoc, this.docView.NativeDimScaling, this.props.PanelWidth(), this.props.PanelHeight()); + public toggleNativeDimensions = () => this.docView && this.Document.type !== DocumentType.INK && Doc.toggleNativeDimensions(this.layoutDoc, this.docView.NativeDimScaling, this._props.PanelWidth(), this._props.PanelHeight()); public getBounds = () => { - if (!this.docView?.ContentDiv || this.props.treeViewDoc || Doc.AreProtosEqual(this.props.Document, Doc.UserDoc())) { + if (!this.docView?.ContentDiv || this._props.treeViewDoc || Doc.AreProtosEqual(this._props.Document, Doc.UserDoc())) { return undefined; } if (this.docView._componentView?.screenBounds?.()) { return this.docView._componentView.screenBounds(); } - const xf = this.docView.props.ScreenToLocalTransform().scale(this.nativeScaling).inverse(); + const xf = this.docView._props.ScreenToLocalTransform().scale(this.nativeScaling).inverse(); const [[left, top], [right, bottom]] = [xf.transformPoint(0, 0), xf.transformPoint(this.panelWidth, this.panelHeight)]; - if (this.docView.props.LayoutTemplateString?.includes(LinkAnchorBox.name)) { + if (this.docView._props.LayoutTemplateString?.includes(LinkAnchorBox.name)) { const docuBox = this.docView.ContentDiv.getElementsByClassName('linkAnchorBox-cont'); if (docuBox.length) return { ...docuBox[0].getBoundingClientRect(), center: undefined }; } @@ -1535,18 +1552,17 @@ export class DocumentView extends React.Component<DocumentViewProps> { const deiconifyLayout = Cast(this.Document.deiconifyLayout, 'string', null); this.switchViews(deiconifyLayout ? true : false, deiconifyLayout, finalFinished); this.Document.deiconifyLayout = undefined; - this.props.bringToFront(this.Document); + this._props.bringToFront(this.Document); } } @undoBatch - @action setCustomView = (custom: boolean, layout: string): void => { - Doc.setNativeView(this.props.Document); - custom && DocUtils.makeCustomViewClicked(this.props.Document, Docs.Create.StackingDocument, layout, undefined); + Doc.setNativeView(this._props.Document); + custom && DocUtils.makeCustomViewClicked(this._props.Document, Docs.Create.StackingDocument, layout, undefined); }; - @action + switchViews = (custom: boolean, view: string, finished?: () => void, useExistingLayout = false) => { - this.docView && (this.docView._animateScalingTo = 0.1); // shrink doc + runInAction(() => this.docView && (this.docView._animateScalingTo = 0.1)); // shrink doc setTimeout( action(() => { if (useExistingLayout && custom && this.Document['layout_' + view]) { @@ -1568,7 +1584,7 @@ export class DocumentView extends React.Component<DocumentViewProps> { }; layout_fitWidthFunc = (doc: Doc) => BoolCast(this.layout_fitWidth); - scaleToScreenSpace = () => (1 / (this.props.NativeDimScaling?.() || 1)) * this.screenToLocalTransform().Scale; + scaleToScreenSpace = () => (1 / (this._props.NativeDimScaling?.() || 1)) * this.screenToLocalTransform().Scale; docViewPathFunc = () => this.docViewPath; isSelected = () => this.SELECTED; select = (extendSelection: boolean, focusSelection?: boolean) => { @@ -1592,14 +1608,13 @@ export class DocumentView extends React.Component<DocumentViewProps> { NativeDimScaling = () => this.nativeScaling; selfView = () => this; screenToLocalTransform = () => - this.props + this._props .ScreenToLocalTransform() .translate(-this.centeringX, -this.centeringY) .scale(1 / this.nativeScaling); - @action componentDidMount() { - this.Document[DocViews].add(this); + runInAction(() => this.Document[DocViews].add(this)); this._disposers.updateContentsScript = reaction(() => ScriptCast(this.Document.updateContentsScript)?.script?.run({ this: this.Document }).result, emptyFunction); this._disposers.height = reaction( // increase max auto height if document has been resized to be greater than current max @@ -1609,13 +1624,13 @@ export class DocumentView extends React.Component<DocumentViewProps> { if (docMax && docMax < height) this.layoutDoc.layout_maxAutoHeight = height; }) ); - !BoolCast(this.props.Document.dontRegisterView, this.props.dontRegisterView) && DocumentManager.Instance.AddView(this); + !BoolCast(this._props.Document.dontRegisterView, this._props.dontRegisterView) && DocumentManager.Instance.AddView(this); } - @action + componentWillUnmount() { this.Document[DocViews].delete(this); Object.values(this._disposers).forEach(disposer => disposer?.()); - !BoolCast(this.props.Document.dontRegisterView, this.props.dontRegisterView) && DocumentManager.Instance.RemoveView(this); + !BoolCast(this._props.Document.dontRegisterView, this._props.dontRegisterView) && DocumentManager.Instance.RemoveView(this); } // want the htmloverlay to be able to fade in but we also want it to be display 'none' until it is needed. // unfortunately, CSS can't transition animate any properties for something that is display 'none'. @@ -1652,23 +1667,23 @@ export class DocumentView extends React.Component<DocumentViewProps> { render() { TraceMobx(); - const xshift = Math.abs(this.Xshift) <= 0.001 ? this.props.PanelWidth() : undefined; - const yshift = Math.abs(this.Yshift) <= 0.001 ? this.props.PanelHeight() : undefined; + const xshift = Math.abs(this.Xshift) <= 0.001 ? this._props.PanelWidth() : undefined; + const yshift = Math.abs(this.Yshift) <= 0.001 ? this._props.PanelHeight() : undefined; return ( <div className="contentFittingDocumentView" onPointerEnter={action(() => (this._isHovering = true))} onPointerLeave={action(() => (this._isHovering = false))}> - {!this.props.Document || !this.props.PanelWidth() ? null : ( + {!this._props.Document || !this._props.PanelWidth() ? null : ( <div className="contentFittingDocumentView-previewDoc" ref={this.ContentRef} style={{ - transition: 'inherit', // this.props.dataTransition, + transition: 'inherit', // this._props.dataTransition, transform: `translate(${this.centeringX}px, ${this.centeringY}px)`, - width: xshift ?? `${this.props.PanelWidth() - this.Xshift * 2}px`, - height: this.props.forceAutoHeight ? undefined : yshift ?? (this.layout_fitWidth ? `${this.panelHeight}px` : `${(this.effectiveNativeHeight / this.effectiveNativeWidth) * this.props.PanelWidth()}px`), + width: xshift ?? `${this._props.PanelWidth() - this.Xshift * 2}px`, + height: this._props.forceAutoHeight ? undefined : yshift ?? (this.layout_fitWidth ? `${this.panelHeight}px` : `${(this.effectiveNativeHeight / this.effectiveNativeWidth) * this._props.PanelWidth()}px`), }}> <DocumentViewInternal - {...this.props} + {...this._props} DocumentView={this.selfView} viewPath={this.docViewPathFunc} PanelWidth={this.PanelWidth} @@ -1680,7 +1695,7 @@ export class DocumentView extends React.Component<DocumentViewProps> { select={this.select} layout_fitWidth={this.layout_fitWidthFunc} ScreenToLocalTransform={this.screenToLocalTransform} - focus={this.props.focus || emptyFunction} + focus={this._props.focus || emptyFunction} ref={action((r: DocumentViewInternal | null) => r && (this.docView = r))} /> {this.htmlOverlay} diff --git a/src/client/views/nodes/EquationBox.tsx b/src/client/views/nodes/EquationBox.tsx index 23413b9d1..02ed56333 100644 --- a/src/client/views/nodes/EquationBox.tsx +++ b/src/client/views/nodes/EquationBox.tsx @@ -1,5 +1,5 @@ import EquationEditor from './formattedText/EquationEditor'; -import { action, reaction } from 'mobx'; +import { action, makeObservable, override, reaction } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; import { Id } from '../../../fields/FieldSymbols'; @@ -11,6 +11,7 @@ import { ViewBoxBaseComponent } from '../DocComponent'; import { LightboxView } from '../LightboxView'; import './EquationBox.scss'; import { FieldView, FieldViewProps } from './FieldView'; +import { copyProps } from '../../../Utils'; @observer export class EquationBox extends ViewBoxBaseComponent<FieldViewProps>() { @@ -19,10 +20,23 @@ export class EquationBox extends ViewBoxBaseComponent<FieldViewProps>() { } public static SelectOnLoad: string = ''; _ref: React.RefObject<EquationEditor> = React.createRef(); + + _prevProps: React.PropsWithChildren<FieldViewProps>; + @override _props: React.PropsWithChildren<FieldViewProps>; + constructor(props: React.PropsWithChildren<FieldViewProps>) { + super(props); + this._props = this._prevProps = props; + makeObservable(this); + } + + componentDidUpdate() { + copyProps(this); + } + componentDidMount() { - this.props.setContentView?.(this); - if (EquationBox.SelectOnLoad === this.Document[Id] && (!LightboxView.LightboxDoc || LightboxView.IsLightboxDocView(this.props.docViewPath()))) { - this.props.select(false); + this._props.setContentView?.(this); + if (EquationBox.SelectOnLoad === this.Document[Id] && (!LightboxView.LightboxDoc || LightboxView.IsLightboxDocView(this._props.docViewPath()))) { + this._props.select(false); this._ref.current!.mathField.focus(); this.dataDoc.text === 'x' && this._ref.current!.mathField.select(); @@ -38,7 +52,7 @@ export class EquationBox extends ViewBoxBaseComponent<FieldViewProps>() { //{ fireImmediately: true } ); reaction( - () => this.props.isSelected(), + () => this._props.isSelected(), selected => { if (this._ref.current) { if (selected) this._ref.current.element.current.children[0].addEventListener('keydown', this.keyPressed, true); @@ -62,7 +76,7 @@ export class EquationBox extends ViewBoxBaseComponent<FieldViewProps>() { y: NumCast(this.layoutDoc.y) + _height + 10, }); EquationBox.SelectOnLoad = nextEq[Id]; - this.props.addDocument?.(nextEq); + this._props.addDocument?.(nextEq); e.stopPropagation(); } if (e.key === 'Tab') { @@ -73,10 +87,10 @@ export class EquationBox extends ViewBoxBaseComponent<FieldViewProps>() { _height: 300, backgroundColor: 'white', }); - this.props.addDocument?.(graph); + this._props.addDocument?.(graph); e.stopPropagation(); } - if (e.key === 'Backspace' && !this.dataDoc.text) this.props.removeDocument?.(this.Document); + if (e.key === 'Backspace' && !this.dataDoc.text) this._props.removeDocument?.(this.Document); }; @undoBatch onChange = (str: string) => (this.dataDoc.text = str); @@ -100,7 +114,7 @@ export class EquationBox extends ViewBoxBaseComponent<FieldViewProps>() { }; render() { TraceMobx(); - const scale = (this.props.NativeDimScaling?.() || 1) * NumCast(this.layoutDoc._freeform_scale, 1); + const scale = (this._props.NativeDimScaling?.() || 1) * NumCast(this.layoutDoc._freeform_scale, 1); return ( <div ref={r => this.updateSize()} @@ -110,7 +124,7 @@ export class EquationBox extends ViewBoxBaseComponent<FieldViewProps>() { transform: `scale(${scale})`, width: 'fit-content', // `${100 / scale}%`, height: `${100 / scale}%`, - pointerEvents: !this.props.isSelected() ? 'none' : undefined, + pointerEvents: !this._props.isSelected() ? 'none' : undefined, fontSize: StrCast(this.layoutDoc._text_fontSize), }} onKeyDown={e => e.stopPropagation()}> diff --git a/src/client/views/nodes/FontIconBox/FontIconBox.tsx b/src/client/views/nodes/FontIconBox/FontIconBox.tsx index 8f6550663..22339907f 100644 --- a/src/client/views/nodes/FontIconBox/FontIconBox.tsx +++ b/src/client/views/nodes/FontIconBox/FontIconBox.tsx @@ -1,13 +1,13 @@ import { IconProp } from '@fortawesome/fontawesome-svg-core'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { Button, ColorPicker, Dropdown, DropdownType, EditableText, IconButton, IListItemProps, MultiToggle, NumberDropdown, NumberDropdownType, Popup, Size, Toggle, ToggleType, Type } from 'browndash-components'; -import { action, computed, observable } from 'mobx'; +import { computed, makeObservable, observable, override } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; import { Doc, DocListCast, StrListCast } from '../../../../fields/Doc'; import { ScriptField } from '../../../../fields/ScriptField'; import { BoolCast, Cast, DocCast, NumCast, ScriptCast, StrCast } from '../../../../fields/Types'; -import { emptyFunction, setupMoveUpEvents, Utils } from '../../../../Utils'; +import { copyProps, emptyFunction, setupMoveUpEvents, Utils } from '../../../../Utils'; import { CollectionViewType, DocumentType } from '../../../documents/DocumentTypes'; import { SelectionManager } from '../../../util/SelectionManager'; import { SettingsManager } from '../../../util/SettingsManager'; @@ -45,6 +45,16 @@ export class FontIconBox extends ViewBoxBaseComponent<ButtonProps>() { public static LayoutString(fieldKey: string) { return FieldView.LayoutString(FontIconBox, fieldKey); } + _prevProps: React.PropsWithChildren<ButtonProps>; + @override _props: React.PropsWithChildren<ButtonProps>; + constructor(props: React.PropsWithChildren<ButtonProps>) { + super(props); + this._props = this._prevProps = props; + makeObservable(this); + } + componentDidUpdate() { + copyProps(this); + } // // This controls whether fontIconButtons will display labels under their icons or not // @@ -57,7 +67,7 @@ export class FontIconBox extends ViewBoxBaseComponent<ButtonProps>() { @observable noTooltip = false; showTemplate = (): void => { const dragFactory = Cast(this.layoutDoc.dragFactory, Doc, null); - dragFactory && this.props.addDocTab(dragFactory, OpenWhere.addRight); + dragFactory && this._props.addDocTab(dragFactory, OpenWhere.addRight); }; dragAsTemplate = (): void => { this.layoutDoc.onDragStart = ScriptField.MakeFunction('getCopy(this.dragFactory, true)'); @@ -131,7 +141,7 @@ export class FontIconBox extends ViewBoxBaseComponent<ButtonProps>() { break; } const numScript = (value?: number) => ScriptCast(this.Document.script).script.run({ this: this.Document, self: this.Document, value, _readOnly_: value === undefined }); - const color = this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.Color); + const color = this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.Color); // Script for checking the outcome of the toggle const checkResult = Number(Number(numScript().result ?? 0).toPrecision(NumCast(this.dataDoc.numPrecision, 3))); @@ -246,7 +256,7 @@ export class FontIconBox extends ViewBoxBaseComponent<ButtonProps>() { * Color button */ @computed get colorButton() { - const color = this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.Color); + const color = this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.Color); const curColor = this.colorScript?.script.run({ this: this.Document, self: this.Document, value: undefined, _readOnly_: true }).result ?? 'transparent'; const tooltip: string = StrCast(this.Document.toolTip); @@ -279,7 +289,7 @@ export class FontIconBox extends ViewBoxBaseComponent<ButtonProps>() { const script = ScriptCast(this.Document.onClick); const toggleStatus = script ? script.script.run({ this: this.Document, self: this.Document, value: undefined, _readOnly_: true }).result : false; // Colors - const color = this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.Color); + const color = this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.Color); const items = DocListCast(this.dataDoc.data); return ( <MultiToggle @@ -310,8 +320,8 @@ export class FontIconBox extends ViewBoxBaseComponent<ButtonProps>() { const script = ScriptCast(this.Document.onClick); const toggleStatus = script ? script.script.run({ this: this.Document, self: this.Document, value: undefined, _readOnly_: true }).result : false; // Colors - const color = this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.Color); - const backgroundColor = this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.BackgroundColor); + const color = this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.Color); + const backgroundColor = this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.BackgroundColor); return ( <Toggle @@ -333,8 +343,8 @@ export class FontIconBox extends ViewBoxBaseComponent<ButtonProps>() { * Default */ @computed get defaultButton() { - const color = this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.Color); - const backgroundColor = this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.BackgroundColor); + const color = this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.Color); + const backgroundColor = this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.BackgroundColor); const tooltip: string = StrCast(this.Document.toolTip); return <IconButton tooltip={tooltip} icon={this.Icon(color)!} label={this.label} />; @@ -361,7 +371,7 @@ export class FontIconBox extends ViewBoxBaseComponent<ButtonProps>() { } render() { - const color = this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.Color); + const color = this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.Color); const tooltip = StrCast(this.Document.toolTip); const scriptFunc = () => ScriptCast(this.Document.onClick)?.script.run({ this: this.Document, self: this.Document, _readOnly_: false }); const btnProps = { tooltip, icon: this.Icon(color)!, label: this.label }; diff --git a/src/client/views/nodes/FunctionPlotBox.tsx b/src/client/views/nodes/FunctionPlotBox.tsx index 5ed5fa8fd..a04c27e01 100644 --- a/src/client/views/nodes/FunctionPlotBox.tsx +++ b/src/client/views/nodes/FunctionPlotBox.tsx @@ -1,5 +1,5 @@ import functionPlot from 'function-plot'; -import { action, computed, reaction } from 'mobx'; +import { action, computed, makeObservable, override, reaction } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; import { Doc, DocListCast } from '../../../fields/Doc'; @@ -9,19 +9,14 @@ import { List } from '../../../fields/List'; import { createSchema, listSpec, makeInterface } from '../../../fields/Schema'; import { Cast, StrCast } from '../../../fields/Types'; import { TraceMobx } from '../../../fields/util'; +import { copyProps } from '../../../Utils'; import { Docs } from '../../documents/Documents'; import { DragManager } from '../../util/DragManager'; import { undoBatch } from '../../util/UndoManager'; import { ViewBoxAnnotatableComponent } from '../DocComponent'; -import { DocFocusOptions, DocumentView } from './DocumentView'; import { FieldView, FieldViewProps } from './FieldView'; import { PinProps, PresBox } from './trails'; -const EquationSchema = createSchema({}); - -type EquationDocument = makeInterface<[typeof EquationSchema, typeof documentSchema]>; -const EquationDocument = makeInterface(EquationSchema, documentSchema); - @observer export class FunctionPlotBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { public static LayoutString(fieldKey: string) { @@ -31,12 +26,22 @@ export class FunctionPlotBox extends ViewBoxAnnotatableComponent<FieldViewProps> _plot: any; _plotId = ''; _plotEle: any; - constructor(props: any) { + + _prevProps: React.PropsWithChildren<FieldViewProps>; + @override _props: React.PropsWithChildren<FieldViewProps>; + constructor(props: React.PropsWithChildren<FieldViewProps>) { super(props); + this._props = this._prevProps = props; + makeObservable(this); this._plotId = 'graph' + FunctionPlotBox.GraphCount++; } + + componentDidUpdate() { + copyProps(this); + } + componentDidMount() { - this.props.setContentView?.(this); + this._props.setContentView?.(this); reaction( () => [DocListCast(this.dataDoc[this.fieldKey]).map(doc => doc?.text), this.layoutDoc.width, this.layoutDoc.height, this.layoutDoc.xRange, this.layoutDoc.yRange], () => this.createGraph() @@ -52,8 +57,8 @@ export class FunctionPlotBox extends ViewBoxAnnotatableComponent<FieldViewProps> }; createGraph = (ele?: HTMLDivElement) => { this._plotEle = ele || this._plotEle; - const width = this.props.PanelWidth(); - const height = this.props.PanelHeight(); + const width = this._props.PanelWidth(); + const height = this._props.PanelHeight(); const fns = DocListCast(this.dataDoc.data).map(doc => StrCast(doc.text, 'x^2').replace(/\\frac\{(.*)\}\{(.*)\}/, '($1/$2)')); try { this._plotEle.children.length && this._plotEle.removeChild(this._plotEle.children[0]); @@ -77,7 +82,7 @@ export class FunctionPlotBox extends ViewBoxAnnotatableComponent<FieldViewProps> @undoBatch drop = (e: Event, de: DragManager.DropEvent) => { if (de.complete.docDragData?.droppedDocuments.length) { - const added = de.complete.docDragData.droppedDocuments.reduce((res, doc) => res && Doc.AddDocToList(this.dataDoc, this.props.fieldKey, doc), true); + const added = de.complete.docDragData.droppedDocuments.reduce((res, doc) => res && Doc.AddDocToList(this.dataDoc, this._props.fieldKey, doc), true); !added && e.preventDefault(); e.stopPropagation(); // prevent parent Doc from registering new position so that it snaps back into place return added; @@ -102,14 +107,14 @@ export class FunctionPlotBox extends ViewBoxAnnotatableComponent<FieldViewProps> <div ref={this.createDropTarget} style={{ - pointerEvents: !this.props.isContentActive() ? 'all' : undefined, - width: this.props.PanelWidth(), - height: this.props.PanelHeight(), + pointerEvents: !this._props.isContentActive() ? 'all' : undefined, + width: this._props.PanelWidth(), + height: this._props.PanelHeight(), }}> {this.theGraph} <div style={{ - display: this.props.isSelected() ? 'none' : undefined, + display: this._props.isSelected() ? 'none' : undefined, position: 'absolute', width: '100%', height: '100%', diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx index dbfeec1c3..091c1a32f 100644 --- a/src/client/views/nodes/ImageBox.tsx +++ b/src/client/views/nodes/ImageBox.tsx @@ -1,25 +1,20 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { Tooltip } from '@mui/material'; -import { action, computed, IReactionDisposer, observable, ObservableMap, reaction, runInAction } from 'mobx'; +import { action, computed, IReactionDisposer, makeObservable, observable, ObservableMap, override, reaction } from 'mobx'; import { observer } from 'mobx-react'; import { extname } from 'path'; +import * as React from 'react'; import { Doc, DocListCast, Opt } from '../../../fields/Doc'; import { DocData } from '../../../fields/DocSymbols'; import { Id } from '../../../fields/FieldSymbols'; import { InkTool } from '../../../fields/InkField'; -import { List } from '../../../fields/List'; import { ObjectField } from '../../../fields/ObjectField'; -import { createSchema } from '../../../fields/Schema'; -import { ComputedField } from '../../../fields/ScriptField'; import { Cast, NumCast, StrCast } from '../../../fields/Types'; import { ImageField } from '../../../fields/URLField'; import { TraceMobx } from '../../../fields/util'; -import { DashColor, emptyFunction, returnEmptyString, returnFalse, returnOne, returnZero, setupMoveUpEvents, Utils } from '../../../Utils'; -import { GooglePhotos } from '../../apis/google_docs/GooglePhotosClientUtils'; -import { CognitiveServices, Confidence, Service, Tag } from '../../cognitive_services/CognitiveServices'; +import { copyProps, DashColor, emptyFunction, returnEmptyString, returnFalse, returnOne, returnZero, setupMoveUpEvents, Utils } from '../../../Utils'; import { Docs, DocUtils } from '../../documents/Documents'; import { DocumentType } from '../../documents/DocumentTypes'; -import { Networking } from '../../Network'; import { DocumentManager } from '../../util/DocumentManager'; import { DragManager } from '../../util/DragManager'; import { undoBatch } from '../../util/UndoManager'; @@ -31,22 +26,9 @@ import { MarqueeAnnotator } from '../MarqueeAnnotator'; import { AnchorMenu } from '../pdf/AnchorMenu'; import { StyleProp } from '../StyleProvider'; import { DocFocusOptions, OpenWhere } from './DocumentView'; -import { FaceRectangles } from './FaceRectangles'; import { FieldView, FieldViewProps } from './FieldView'; import './ImageBox.scss'; import { PinProps, PresBox } from './trails'; -import * as React from 'react'; - -export const pageSchema = createSchema({ - googlePhotosUrl: 'string', - googlePhotosTags: 'string', -}); -const uploadIcons = { - idle: 'downarrow.png', - loading: 'loading.gif', - success: 'greencheck.png', - failure: 'redx.png', -}; @observer export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps & FieldViewProps>() { @@ -54,10 +36,10 @@ export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp return FieldView.LayoutString(ImageBox, fieldKey); } - @observable public static imageRootDoc: Doc | undefined; + @observable public static imageRootDoc: Doc | undefined = undefined; @observable public static imageEditorOpen: boolean = false; @observable public static imageEditorSource: string = ''; - @observable public static addDoc: ((doc: Doc | Doc[], annotationKey?: string) => boolean) | undefined; + @observable public static addDoc: ((doc: Doc | Doc[], annotationKey?: string) => boolean) | undefined = undefined; @action public static setImageEditorOpen(open: boolean) { ImageBox.imageEditorOpen = open; } @@ -72,16 +54,23 @@ export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp private _overlayIconRef = React.createRef<HTMLDivElement>(); private _marqueeref = React.createRef<MarqueeAnnotator>(); @observable _curSuffix = ''; - @observable _uploadIcon = uploadIcons.idle; - constructor(props: any) { + _prevProps: ViewBoxAnnotatableProps & FieldViewProps; + @override _props: ViewBoxAnnotatableProps & FieldViewProps; + constructor(props: ViewBoxAnnotatableProps & FieldViewProps) { super(props); - this.props.setContentView?.(this); + this._props = this._prevProps = props; + makeObservable(this); + this._props.setContentView?.(this); + } + + componentDidUpdate() { + copyProps(this); } protected createDropTarget = (ele: HTMLDivElement) => { this._dropDisposer?.(); - ele && (this._dropDisposer = DragManager.MakeDropTarget(ele, this.drop.bind(this), this.props.Document)); + ele && (this._dropDisposer = DragManager.MakeDropTarget(ele, this.drop.bind(this), this.Document)); }; getAnchor = (addAsAnnotation: boolean, pinProps?: PinProps) => { @@ -107,9 +96,9 @@ export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp componentDidMount() { this._disposers.sizer = reaction( () => ({ - forceFull: this.props.renderDepth < 1 || this.layoutDoc._showFullRes, - scrSize: (this.props.ScreenToLocalTransform().inverse().transformDirection(this.nativeSize.nativeWidth, this.nativeSize.nativeHeight)[0] / this.nativeSize.nativeWidth) * NumCast(this.layoutDoc._freeform_scale, 1), - selected: this.props.isSelected(), + forceFull: this._props.renderDepth < 1 || this.layoutDoc._showFullRes, + scrSize: (this._props.ScreenToLocalTransform().inverse().transformDirection(this.nativeSize.nativeWidth, this.nativeSize.nativeHeight)[0] / this.nativeSize.nativeWidth) * NumCast(this.layoutDoc._freeform_scale, 1), + selected: this._props.isSelected(), }), ({ forceFull, scrSize, selected }) => (this._curSuffix = selected ? '_o' : this.fieldKey === 'icon' ? '_m' : forceFull ? '_o' : scrSize < 0.25 ? '_s' : scrSize < 0.5 ? '_m' : scrSize < 0.8 ? '_l' : '_o'), { fireImmediately: true, delay: 1000 } @@ -141,7 +130,6 @@ export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp } @undoBatch - @action drop = (e: Event, de: DragManager.DropEvent) => { if (de.complete.docDragData) { let added: boolean | undefined = undefined; @@ -177,8 +165,8 @@ export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp @undoBatch setNativeSize = action(() => { - const scaling = (this.props.DocumentView?.().props.ScreenToLocalTransform().Scale || 1) / NumCast(this.layoutDoc._freeform_scale, 1); - const nscale = NumCast(this.props.PanelWidth()) / scaling; + const scaling = (this._props.DocumentView?.()._props.ScreenToLocalTransform().Scale || 1) / NumCast(this.layoutDoc._freeform_scale, 1); + const nscale = NumCast(this._props.PanelWidth()) / scaling; const nw = nscale / NumCast(this.dataDoc[this.fieldKey + '_nativeWidth']); this.dataDoc[this.fieldKey + '_nativeHeight'] = NumCast(this.dataDoc[this.fieldKey + '_nativeHeight']) * nw; this.dataDoc[this.fieldKey + '_nativeWidth'] = NumCast(this.dataDoc[this.fieldKey + '_nativeWidth']) * nw; @@ -217,8 +205,8 @@ export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp cropping.title = 'crop: ' + this.Document.title; cropping.x = NumCast(this.Document.x) + NumCast(this.layoutDoc._width); cropping.y = NumCast(this.Document.y); - cropping._width = anchw * (this.props.NativeDimScaling?.() || 1); - cropping._height = anchh * (this.props.NativeDimScaling?.() || 1); + cropping._width = anchw * (this._props.NativeDimScaling?.() || 1); + cropping._height = anchh * (this._props.NativeDimScaling?.() || 1); cropping.onClick = undefined; const croppingProto = Doc.GetProto(cropping); croppingProto.annotationOn = undefined; @@ -242,10 +230,10 @@ export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp DocUtils.MakeLink(region, cropping, { link_relationship: 'cropped image' }); cropping.x = NumCast(this.Document.x) + NumCast(this.layoutDoc._width); cropping.y = NumCast(this.Document.y); - this.props.addDocTab(cropping, OpenWhere.inParent); + this._props.addDocTab(cropping, OpenWhere.inParent); } DocumentManager.Instance.AddViewRenderedCb(cropping, dv => setTimeout(() => (dv.ComponentView as ImageBox).setNativeSize(), 200)); - this.props.bringToFront(cropping); + this._props.bringToFront(cropping); return cropping; }; @@ -262,55 +250,15 @@ export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp event: action(() => { ImageBox.setImageEditorOpen(true); ImageBox.setImageEditorSource(this.choosePath(field.url)); - ImageBox.addDoc = this.props.addDocument; + ImageBox.addDoc = this._props.addDocument; ImageBox.imageRootDoc = this.Document; }), icon: 'pencil-alt', }); - if (!Doc.noviceMode) { - funcs.push({ description: 'Export to Google Photos', event: () => GooglePhotos.Transactions.UploadImages([this.props.Document]), icon: 'caret-square-right' }); - - const existingAnalyze = ContextMenu.Instance?.findByDescription('Analyzers...'); - const modes: ContextMenuProps[] = existingAnalyze && 'subitems' in existingAnalyze ? existingAnalyze.subitems : []; - modes.push({ description: 'Generate Tags', event: this.generateMetadata, icon: 'tag' }); - modes.push({ description: 'Find Faces', event: this.extractFaces, icon: 'camera' }); - //modes.push({ description: "Recommend", event: this.extractText, icon: "brain" }); - !existingAnalyze && ContextMenu.Instance?.addItem({ description: 'Analyzers...', subitems: modes, icon: 'hand-point-right' }); - } - ContextMenu.Instance?.addItem({ description: 'Options...', subitems: funcs, icon: 'asterisk' }); } }; - extractFaces = () => { - const converter = (results: any) => { - return results.map((face: CognitiveServices.Image.Face) => Doc.Get.FromJson({ data: face, title: `Face: ${face.faceId}` })!); - }; - this.url && CognitiveServices.Image.Appliers.ProcessImage(this.dataDoc, [this.fieldKey + '-faces'], this.url, Service.Face, converter); - }; - - generateMetadata = (threshold: Confidence = Confidence.Excellent) => { - const converter = (results: any) => { - const tagDoc = new Doc(); - const tagsList = new List(); - results.tags.map((tag: Tag) => { - tagsList.push(tag.name); - const sanitized = tag.name.replace(' ', '_'); - tagDoc[sanitized] = ComputedField.MakeFunction(`(${tag.confidence} >= this.confidence) ? ${tag.confidence} : "${ComputedField.undefined}"`); - }); - this.dataDoc[this.fieldKey + '-generatedTags'] = tagsList; - tagDoc.title = 'Generated Tags Doc'; - tagDoc.confidence = threshold; - return tagDoc; - }; - this.url && CognitiveServices.Image.Appliers.ProcessImage(this.dataDoc, [this.fieldKey + '-generatedTagsDoc'], this.url, Service.ComputerVision, converter); - }; - - @computed private get url() { - const data = Cast(this.dataDoc[this.fieldKey], ImageField); - return data ? data.url?.href : undefined; - } - choosePath(url: URL) { if (!url?.href) return ''; const lower = url.href.toLowerCase(); @@ -321,60 +269,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp const ext = extname(url.href); return url.href.replace(ext, (this._error ? '_o' : this._curSuffix) + ext); } - - considerGooglePhotosLink = () => { - const remoteUrl = StrCast(this.dataDoc.googlePhotosUrl); // bcz: StrCast or URLCast??? - return !remoteUrl ? null : <img draggable={false} style={{ transformOrigin: 'bottom right' }} id={'google-photos'} src={'/assets/google_photos.png'} onClick={() => window.open(remoteUrl)} />; - }; - - considerGooglePhotosTags = () => { - const tags = this.dataDoc.googlePhotosTags; - return !tags ? null : <img id={'google-tags'} src={'/assets/google_tags.png'} />; - }; - - getScrollHeight = () => (this.props.layout_fitWidth?.(this.Document) !== false && NumCast(this.layoutDoc._freeform_scale, 1) === NumCast(this.dataDoc._freeform_scaleMin, 1) ? this.nativeSize.nativeHeight : undefined); - - @computed - private get considerDownloadIcon() { - const data = this.dataDoc[this.fieldKey]; - if (!(data instanceof ImageField)) { - return null; - } - const primary = data.url?.href; - if (primary.includes(window.location.origin)) { - return null; - } - return ( - <img - id={'upload-icon'} - draggable={false} - style={{ transformOrigin: 'bottom right' }} - src={`/assets/${this._uploadIcon}`} - onClick={async () => { - const { dataDoc } = this; - const { success, failure, idle, loading } = uploadIcons; - runInAction(() => (this._uploadIcon = loading)); - const [{ accessPaths }] = await Networking.PostToServer('/uploadRemoteImage', { sources: [primary] }); - dataDoc[this.props.fieldKey + '-originalUrl'] = primary; - let succeeded = true; - let data: ImageField | undefined; - try { - data = new ImageField(accessPaths.agnostic.client); - } catch { - succeeded = false; - } - runInAction(() => (this._uploadIcon = succeeded ? success : failure)); - setTimeout( - action(() => { - this._uploadIcon = idle; - data && (dataDoc[this.fieldKey] = data); - }), - 2000 - ); - }} - /> - ); - } + getScrollHeight = () => (this._props.layout_fitWidth?.(this.Document) !== false && NumCast(this.layoutDoc._freeform_scale, 1) === NumCast(this.dataDoc._freeform_scaleMin, 1) ? this.nativeSize.nativeHeight : undefined); @computed get nativeSize() { TraceMobx(); @@ -407,7 +302,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp ref={this._overlayIconRef} onPointerDown={e => setupMoveUpEvents(e.target, e, returnFalse, emptyFunction, e => (this.layoutDoc[`_${this.fieldKey}_usePath`] = usePath === undefined ? 'alternate' : usePath === 'alternate' ? 'alternate:hover' : undefined))} style={{ - display: (this.props.isContentActive() !== false && DragManager.DocDragData?.canEmbed) || DocListCast(this.dataDoc[this.fieldKey + '-alternates']).length ? 'block' : 'none', + display: (this._props.isContentActive() !== false && DragManager.DocDragData?.canEmbed) || DocListCast(this.dataDoc[this.fieldKey + '-alternates']).length ? 'block' : 'none', width: 'min(10%, 25px)', height: 'min(10%, 25px)', background: usePath === undefined ? 'white' : usePath === 'alternate' ? 'black' : 'gray', @@ -436,7 +331,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp @computed get content() { TraceMobx(); - const backColor = DashColor(this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.BackgroundColor)); + const backColor = DashColor(this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.BackgroundColor)); const backAlpha = backColor.red() === 0 && backColor.green() === 0 && backColor.blue() === 0 ? backColor.alpha() : 1; const srcpath = this.layoutDoc.hideImage ? '' : this.paths[0]; const fadepath = this.layoutDoc.hideImage ? '' : this.paths.lastElement(); @@ -466,9 +361,6 @@ export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp </div> )} </div> - {!Doc.noviceMode && this.considerDownloadIcon} - {this.considerGooglePhotosLink()} - <FaceRectangles document={this.dataDoc} color={'#0000FF'} backgroundColor={'#0000FF'} /> {this.overlayImageIcon} </div> ); @@ -479,11 +371,11 @@ export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp @observable _savedAnnotations = new ObservableMap<number, HTMLDivElement[]>(); @computed get annotationLayer() { TraceMobx(); - return <div className="imageBox-annotationLayer" style={{ height: this.props.PanelHeight() }} ref={this._annotationLayer} />; + return <div className="imageBox-annotationLayer" style={{ height: this._props.PanelHeight() }} ref={this._annotationLayer} />; } - screenToLocalTransform = () => this.props.ScreenToLocalTransform().translate(0, NumCast(this.layoutDoc._layout_scrollTop) * this.props.ScreenToLocalTransform().Scale); + screenToLocalTransform = () => this._props.ScreenToLocalTransform().translate(0, NumCast(this.layoutDoc._layout_scrollTop) * this._props.ScreenToLocalTransform().Scale); marqueeDown = (e: React.PointerEvent) => { - if (!e.altKey && e.button === 0 && NumCast(this.layoutDoc._freeform_scale, 1) <= NumCast(this.dataDoc.freeform_scaleMin, 1) && this.props.isContentActive(true) && ![InkTool.Highlighter, InkTool.Pen, InkTool.Write].includes(Doc.ActiveTool)) { + if (!e.altKey && e.button === 0 && NumCast(this.layoutDoc._freeform_scale, 1) <= NumCast(this.dataDoc.freeform_scaleMin, 1) && this._props.isContentActive(true) && ![InkTool.Highlighter, InkTool.Pen, InkTool.Write].includes(Doc.ActiveTool)) { setupMoveUpEvents( this, e, @@ -502,7 +394,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp finishMarquee = () => { this._getAnchor = AnchorMenu.Instance?.GetAnchor; this._marqueeref.current?.onTerminateSelection(); - this.props.select(false); + this._props.select(false); }; focus = (anchor: Doc, options: DocFocusOptions) => (anchor.type === DocumentType.CONFIG ? undefined : this._ffref.current?.focus(anchor, options)); @@ -510,8 +402,8 @@ export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp savedAnnotations = () => this._savedAnnotations; render() { TraceMobx(); - const borderRad = this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.BorderRounding); - const borderRadius = borderRad?.includes('px') ? `${Number(borderRad.split('px')[0]) / (this.props.NativeDimScaling?.() || 1)}px` : borderRad; + const borderRad = this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.BorderRounding); + const borderRadius = borderRad?.includes('px') ? `${Number(borderRad.split('px')[0]) / (this._props.NativeDimScaling?.() || 1)}px` : borderRad; return ( <div className="imageBox" @@ -527,25 +419,25 @@ export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp } })} style={{ - width: this.props.PanelWidth() ? undefined : `100%`, - height: this.props.PanelWidth() ? undefined : `100%`, + width: this._props.PanelWidth() ? undefined : `100%`, + height: this._props.PanelWidth() ? undefined : `100%`, pointerEvents: this.layoutDoc._lockedPosition ? 'none' : undefined, borderRadius, - overflow: this.layoutDoc.layout_fitWidth || this.props.layout_fitWidth?.(this.Document) ? 'auto' : undefined, + overflow: this.layoutDoc.layout_fitWidth || this._props.layout_fitWidth?.(this.Document) ? 'auto' : undefined, }}> <CollectionFreeFormView ref={this._ffref} - {...this.props} + {...this._props} setContentView={emptyFunction} NativeWidth={returnZero} NativeHeight={returnZero} - renderDepth={this.props.renderDepth + 1} + renderDepth={this._props.renderDepth + 1} fieldKey={this.annotationKey} - styleProvider={this.props.styleProvider} + styleProvider={this._props.styleProvider} isAnnotationOverlay={true} annotationLayerHostsContent={true} - PanelWidth={this.props.PanelWidth} - PanelHeight={this.props.PanelHeight} + PanelWidth={this._props.PanelWidth} + PanelHeight={this._props.PanelHeight} ScreenToLocalTransform={this.screenToLocalTransform} select={emptyFunction} focus={this.focus} @@ -566,8 +458,8 @@ export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp scrollTop={0} annotationLayerScrollTop={0} scaling={returnOne} - annotationLayerScaling={this.props.NativeDimScaling} - docView={this.props.DocumentView!} + annotationLayerScaling={this._props.NativeDimScaling} + docView={this._props.DocumentView!} addDocument={this.addDocument} finishMarquee={this.finishMarquee} savedAnnotations={this.savedAnnotations} diff --git a/src/client/views/nodes/KeyValueBox.tsx b/src/client/views/nodes/KeyValueBox.tsx index d2325a807..9aab53daf 100644 --- a/src/client/views/nodes/KeyValueBox.tsx +++ b/src/client/views/nodes/KeyValueBox.tsx @@ -1,4 +1,4 @@ -import { action, computed, observable } from 'mobx'; +import { action, computed, makeObservable, observable } from 'mobx'; import { observer } from 'mobx-react'; import { Doc, Field, FieldResult } from '../../../fields/Doc'; import { List } from '../../../fields/List'; @@ -6,7 +6,7 @@ import { RichTextField } from '../../../fields/RichTextField'; import { ComputedField, ScriptField } from '../../../fields/ScriptField'; import { DocCast } from '../../../fields/Types'; import { ImageField } from '../../../fields/URLField'; -import { returnAll, returnAlways, returnTrue } from '../../../Utils'; +import { copyProps, returnAll, returnAlways, returnTrue } from '../../../Utils'; import { Docs } from '../../documents/Documents'; import { SetupDrag } from '../../util/DragManager'; import { CompiledScript, CompileScript, ScriptOptions } from '../../util/Scripting'; @@ -38,8 +38,20 @@ export class KeyValueBox extends React.Component<FieldViewProps> { private _keyInput = React.createRef<HTMLInputElement>(); private _valInput = React.createRef<HTMLInputElement>(); + _prevProps: FieldViewProps; + @observable _props: FieldViewProps; + constructor(props: FieldViewProps) { + super(props); + this._props = this._prevProps = props; + makeObservable(this); + } + + componentDidUpdate() { + copyProps(this); + } + componentDidMount() { - this.props.setContentView?.(this); + this._props.setContentView?.(this); } isKeyValueBox = returnTrue; able = returnAlways; @@ -50,7 +62,7 @@ export class KeyValueBox extends React.Component<FieldViewProps> { @observable _splitPercentage = 50; get fieldDocToLayout() { - return this.props.fieldKey ? DocCast(this.props.Document[this.props.fieldKey], DocCast(this.props.Document)) : this.props.Document; + return this._props.fieldKey ? DocCast(this._props.Document[this._props.fieldKey], DocCast(this._props.Document)) : this._props.Document; } @action @@ -109,7 +121,7 @@ export class KeyValueBox extends React.Component<FieldViewProps> { } onPointerDown = (e: React.PointerEvent): void => { - if (e.buttons === 1 && this.props.isSelected()) { + if (e.buttons === 1 && this._props.isSelected()) { e.stopPropagation(); } }; @@ -155,8 +167,8 @@ export class KeyValueBox extends React.Component<FieldViewProps> { rows.push( <KeyValuePair doc={realDoc} - addDocTab={this.props.addDocTab} - PanelWidth={this.props.PanelWidth} + addDocTab={this._props.addDocTab} + PanelWidth={this._props.PanelWidth} PanelHeight={this.rowHeight} ref={(function () { let oldEl: KeyValuePair | undefined; @@ -220,19 +232,19 @@ export class KeyValueBox extends React.Component<FieldViewProps> { getFieldView = () => { const rows = this.rows.filter(row => row.isChecked); if (rows.length > 1) { - const parent = Docs.Create.StackingDocument([], { _layout_autoHeight: true, _width: 300, title: `field views for ${DocCast(this.props.Document).title}`, _chromeHidden: true }); + const parent = Docs.Create.StackingDocument([], { _layout_autoHeight: true, _width: 300, title: `field views for ${DocCast(this._props.Document).title}`, _chromeHidden: true }); for (const row of rows) { - const field = this.createFieldView(DocCast(this.props.Document), row); + const field = this.createFieldView(DocCast(this._props.Document), row); field && Doc.AddDocToList(parent, 'data', field); row.uncheck(); } return parent; } - return rows.length ? this.createFieldView(DocCast(this.props.Document), rows.lastElement()) : undefined; + return rows.length ? this.createFieldView(DocCast(this._props.Document), rows.lastElement()) : undefined; }; createFieldView = (templateDoc: Doc, row: KeyValuePair) => { - const metaKey = row.props.keyName; + const metaKey = row._props.keyName; const fieldTemplate = Doc.IsDelegateField(templateDoc, metaKey) ? Doc.MakeDelegate(templateDoc) : Doc.MakeEmbedding(templateDoc); fieldTemplate.title = metaKey; fieldTemplate.layout_fitWidth = true; @@ -278,8 +290,8 @@ export class KeyValueBox extends React.Component<FieldViewProps> { openItems.push({ description: 'Default Perspective', event: () => { - this.props.addDocTab(this.props.Document, OpenWhere.close); - this.props.addDocTab(this.fieldDocToLayout, OpenWhere.addRight); + this._props.addDocTab(this._props.Document, OpenWhere.close); + this._props.addDocTab(this.fieldDocToLayout, OpenWhere.addRight); }, icon: 'image', }); diff --git a/src/client/views/nodes/KeyValuePair.tsx b/src/client/views/nodes/KeyValuePair.tsx index 3cda70648..40991f371 100644 --- a/src/client/views/nodes/KeyValuePair.tsx +++ b/src/client/views/nodes/KeyValuePair.tsx @@ -1,7 +1,7 @@ -import { action, observable } from 'mobx'; +import { action, makeObservable, observable, toJS } from 'mobx'; import { observer } from 'mobx-react'; import { Doc, Field } from '../../../fields/Doc'; -import { emptyFunction, returnEmptyDoclist, returnEmptyFilter, returnFalse, returnZero } from '../../../Utils'; +import { copyProps, emptyFunction, returnEmptyDoclist, returnEmptyFilter, returnFalse, returnZero } from '../../../Utils'; import { Transform } from '../../util/Transform'; import { undoBatch } from '../../util/UndoManager'; import { ContextMenu } from '../ContextMenu'; @@ -34,6 +34,18 @@ export class KeyValuePair extends React.Component<KeyValuePairProps> { @observable public isChecked = false; private checkbox = React.createRef<HTMLInputElement>(); + _prevProps: KeyValuePairProps; + @observable _props: KeyValuePairProps; + constructor(props: KeyValuePairProps) { + super(props); + this._props = this._prevProps = props; + makeObservable(this); + } + + componentDidUpdate() { + copyProps(this); + } + @action handleCheck = (e: React.ChangeEvent<HTMLInputElement>) => { this.isChecked = e.currentTarget.checked; @@ -46,24 +58,24 @@ export class KeyValuePair extends React.Component<KeyValuePairProps> { }; onContextMenu = (e: React.MouseEvent) => { - const value = this.props.doc[this.props.keyName]; + const value = this._props.doc[this._props.keyName]; if (value instanceof Doc) { e.stopPropagation(); e.preventDefault(); - ContextMenu.Instance.addItem({ description: 'Open Fields', event: () => this.props.addDocTab(value, OpenWhere.addRightKeyvalue), icon: 'layer-group' }); + ContextMenu.Instance.addItem({ description: 'Open Fields', event: () => this._props.addDocTab(value, OpenWhere.addRightKeyvalue), icon: 'layer-group' }); ContextMenu.Instance.displayMenu(e.clientX, e.clientY); } }; render() { const props: FieldViewProps = { - Document: this.props.doc, + Document: this._props.doc, childFilters: returnEmptyFilter, childFiltersByRanges: returnEmptyFilter, searchFilterDocs: returnEmptyDoclist, styleProvider: DefaultStyleProvider, docViewPath: returnEmptyDoclist, - fieldKey: this.props.keyName, + fieldKey: this._props.keyName, isSelected: returnFalse, setHeight: returnFalse, select: emptyFunction, @@ -73,12 +85,11 @@ export class KeyValuePair extends React.Component<KeyValuePairProps> { whenChildContentsActiveChanged: emptyFunction, ScreenToLocalTransform: Transform.Identity, focus: emptyFunction, - PanelWidth: this.props.PanelWidth, - PanelHeight: this.props.PanelHeight, + PanelWidth: this._props.PanelWidth, + PanelHeight: this._props.PanelHeight, addDocTab: returnFalse, pinToPres: returnZero, }; - const contents = <FieldView {...props} />; // let fieldKey = Object.keys(props.Document).indexOf(props.fieldKey) !== -1 ? props.fieldKey : "(" + props.fieldKey + ")"; let protoCount = 0; let doc: Doc | undefined = props.Document; @@ -95,8 +106,8 @@ export class KeyValuePair extends React.Component<KeyValuePairProps> { const hover = { transition: '0.3s ease opacity', opacity: this.isPointerOver || this.isChecked ? 1 : 0 }; return ( - <tr className={this.props.rowStyle} onPointerEnter={action(() => (this.isPointerOver = true))} onPointerLeave={action(() => (this.isPointerOver = false))}> - <td className="keyValuePair-td-key" style={{ width: `${this.props.keyWidth}%` }}> + <tr className={this._props.rowStyle} onPointerEnter={action(() => (this.isPointerOver = true))} onPointerLeave={action(() => (this.isPointerOver = false))}> + <td className="keyValuePair-td-key" style={{ width: `${this._props.keyWidth}%` }}> <div className="keyValuePair-td-key-container"> <button style={hover} @@ -118,9 +129,9 @@ export class KeyValuePair extends React.Component<KeyValuePairProps> { </Tooltip> </div> </td> - <td className="keyValuePair-td-value" style={{ width: `${100 - this.props.keyWidth}%` }} onContextMenu={this.onContextMenu}> + <td className="keyValuePair-td-value" style={{ width: `${100 - this._props.keyWidth}%` }} onContextMenu={this.onContextMenu}> <div className="keyValuePair-td-value-container"> - <EditableView contents={contents} GetValue={() => Field.toKeyValueString(props.Document, props.fieldKey)} SetValue={(value: string) => KeyValueBox.SetField(props.Document, props.fieldKey, value)} /> + <EditableView contents={undefined} fieldContents={props} GetValue={() => Field.toKeyValueString(props.Document, props.fieldKey)} SetValue={(value: string) => KeyValueBox.SetField(props.Document, props.fieldKey, value)} /> </div> </td> </tr> diff --git a/src/client/views/nodes/LabelBox.tsx b/src/client/views/nodes/LabelBox.tsx index f8140af93..52ca8b5b1 100644 --- a/src/client/views/nodes/LabelBox.tsx +++ b/src/client/views/nodes/LabelBox.tsx @@ -1,10 +1,11 @@ -import { action, computed, observable } from 'mobx'; +import { action, computed, makeObservable, observable, override } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; import { Doc, DocListCast } from '../../../fields/Doc'; import { List } from '../../../fields/List'; import { listSpec } from '../../../fields/Schema'; import { Cast, StrCast, NumCast, BoolCast } from '../../../fields/Types'; +import { copyProps } from '../../../Utils'; import { DragManager } from '../../util/DragManager'; import { undoBatch } from '../../util/UndoManager'; import { ContextMenu } from '../ContextMenu'; @@ -29,21 +30,32 @@ export class LabelBox extends ViewBoxBaseComponent<FieldViewProps & LabelBoxProp } private dropDisposer?: DragManager.DragDropDisposer; private _timeout: any; + + _prevProps: React.PropsWithChildren<FieldViewProps & LabelBoxProps>; + @override _props: React.PropsWithChildren<FieldViewProps & LabelBoxProps>; + constructor(props: React.PropsWithChildren<FieldViewProps & LabelBoxProps>) { + super(props); + this._props = this._prevProps = props; + makeObservable(this); + } + componentDidUpdate() { + copyProps(this); + } componentDidMount() { - this.props.setContentView?.(this); + this._props.setContentView?.(this); } componentWillUnMount() { this._timeout && clearTimeout(this._timeout); } @computed get Title() { - return this.dataDoc.title_custom ? StrCast(this.Document.title) : this.props.label ? this.props.label : typeof this.dataDoc[this.fieldKey] === 'string' ? StrCast(this.dataDoc[this.fieldKey]) : StrCast(this.Document.title); + return this.dataDoc.title_custom ? StrCast(this.Document.title) : this._props.label ? this._props.label : typeof this.dataDoc[this.fieldKey] === 'string' ? StrCast(this.dataDoc[this.fieldKey]) : StrCast(this.Document.title); } protected createDropTarget = (ele: HTMLDivElement) => { this.dropDisposer?.(); if (ele) { - this.dropDisposer = DragManager.MakeDropTarget(ele, this.drop.bind(this), this.props.Document); + this.dropDisposer = DragManager.MakeDropTarget(ele, this.drop.bind(this), this.Document); } }; @@ -66,7 +78,6 @@ export class LabelBox extends ViewBoxBaseComponent<FieldViewProps & LabelBoxProp }; @undoBatch - @action drop = (e: Event, de: DragManager.DropEvent) => { const docDragData = de.complete.docDragData; const params = Cast(this.paramsDoc['onClick-paramFieldKeys'], listSpec('string'), []); @@ -81,7 +92,7 @@ export class LabelBox extends ViewBoxBaseComponent<FieldViewProps & LabelBoxProp @observable _mouseOver = false; @computed get hoverColor() { - return this._mouseOver ? StrCast(this.layoutDoc._hoverBackgroundColor) : this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.BackgroundColor); + return this._mouseOver ? StrCast(this.layoutDoc._hoverBackgroundColor) : this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.BackgroundColor); } fitTextToBox = ( @@ -142,7 +153,7 @@ export class LabelBox extends ViewBoxBaseComponent<FieldViewProps & LabelBoxProp onMouseOver={action(() => (this._mouseOver = true))} ref={this.createDropTarget} onContextMenu={this.specificContextMenu} - style={{ boxShadow: this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.BoxShadow) }}> + style={{ boxShadow: this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.BoxShadow) }}> <div className="labelBox-mainButton" style={{ @@ -156,8 +167,8 @@ export class LabelBox extends ViewBoxBaseComponent<FieldViewProps & LabelBoxProp paddingRight: NumCast(this.layoutDoc._xPadding), paddingTop: NumCast(this.layoutDoc._yPadding), paddingBottom: NumCast(this.layoutDoc._yPadding), - width: this.props.PanelWidth(), - height: this.props.PanelHeight(), + width: this._props.PanelWidth(), + height: this._props.PanelHeight(), whiteSpace: 'singleLine' in boxParams && boxParams.singleLine ? 'pre' : 'pre-wrap', }}> <span style={{ width: 'singleLine' in boxParams ? '' : '100%' }} ref={action((r: any) => this.fitTextToBox(r))}> diff --git a/src/client/views/nodes/LinkBox.tsx b/src/client/views/nodes/LinkBox.tsx index ed448ecfb..ff2597fb4 100644 --- a/src/client/views/nodes/LinkBox.tsx +++ b/src/client/views/nodes/LinkBox.tsx @@ -103,7 +103,7 @@ export class LinkBox extends ViewBoxBaseComponent<FieldViewProps>() { componentWillUnmount(): void { this.disposer?.(); } - @observable renderProps: { lx: number; rx: number; ty: number; by: number; pts: number[][] } | undefined; + @observable renderProps: { lx: number; rx: number; ty: number; by: number; pts: number[][] } | undefined = undefined; render() { if (this.renderProps) { const highlight = this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.Highlighting); diff --git a/src/client/views/nodes/LinkDocPreview.tsx b/src/client/views/nodes/LinkDocPreview.tsx index 4df8e792e..dd102edef 100644 --- a/src/client/views/nodes/LinkDocPreview.tsx +++ b/src/client/views/nodes/LinkDocPreview.tsx @@ -144,8 +144,8 @@ export class LinkDocPreview extends React.Component<LinkDocPreviewProps> { LinkManager.currentLink = this._linkDoc; LinkManager.currentLinkAnchor = this._linkSrc; this.props.docProps.DocumentView?.().select(false); - if ((SettingsManager.propertiesWidth ?? 0) < 100) { - SettingsManager.propertiesWidth = 250; + if ((SettingsManager.Instance.propertiesWidth ?? 0) < 100) { + SettingsManager.Instance.propertiesWidth = 250; } }) ); diff --git a/src/client/views/nodes/LoadingBox.tsx b/src/client/views/nodes/LoadingBox.tsx index e554cb8ad..bbb725a3d 100644 --- a/src/client/views/nodes/LoadingBox.tsx +++ b/src/client/views/nodes/LoadingBox.tsx @@ -6,6 +6,7 @@ import { Doc } from '../../../fields/Doc'; import { Id } from '../../../fields/FieldSymbols'; import { StrCast } from '../../../fields/Types'; import { Networking } from '../../Network'; +import { DocumentManager } from '../../util/DocumentManager'; import { ViewBoxAnnotatableComponent } from '../DocComponent'; import { FieldView, FieldViewProps } from './FieldView'; import './LoadingBox.scss'; @@ -36,28 +37,27 @@ export class LoadingBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { public static LayoutString(fieldKey: string) { return FieldView.LayoutString(LoadingBox, fieldKey); } - @observable public static CurrentlyLoading: Doc[] = []; // this assignment doesn't work. the actual assignment happens in DocumentManager's constructor // removes from currently loading display @action public static removeCurrentlyLoading(doc: Doc) { - if (LoadingBox.CurrentlyLoading) { - const index = LoadingBox.CurrentlyLoading.indexOf(doc); - index !== -1 && LoadingBox.CurrentlyLoading.splice(index, 1); + if (DocumentManager.Instance.CurrentlyLoading) { + const index = DocumentManager.Instance.CurrentlyLoading.indexOf(doc); + index !== -1 && DocumentManager.Instance.CurrentlyLoading.splice(index, 1); } } // adds doc to currently loading display @action public static addCurrentlyLoading(doc: Doc) { - if (LoadingBox.CurrentlyLoading.indexOf(doc) === -1) { - LoadingBox.CurrentlyLoading.push(doc); + if (DocumentManager.Instance.CurrentlyLoading.indexOf(doc) === -1) { + DocumentManager.Instance.CurrentlyLoading.push(doc); } } _timer: any; @observable progress = ''; componentDidMount() { - if (!LoadingBox.CurrentlyLoading?.includes(this.Document)) { + if (!DocumentManager.Instance.CurrentlyLoading?.includes(this.Document)) { this.Document.loadingError = 'Upload interrupted, please try again'; } else { const updateFunc = async () => { diff --git a/src/client/views/nodes/MapBox/MapBox.tsx b/src/client/views/nodes/MapBox/MapBox.tsx index 69723b171..98a302834 100644 --- a/src/client/views/nodes/MapBox/MapBox.tsx +++ b/src/client/views/nodes/MapBox/MapBox.tsx @@ -1,20 +1,18 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import BingMapsReact from 'bingmaps-react'; import { Button, EditableText, IconButton, Type } from 'browndash-components'; -import { action, computed, IReactionDisposer, observable, ObservableMap, reaction, runInAction } from 'mobx'; +import { action, computed, IReactionDisposer, makeObservable, observable, ObservableMap, override, reaction, runInAction } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; import { Doc, DocListCast, Field, LinkedTo, Opt } from '../../../../fields/Doc'; import { DocCss, Highlight } from '../../../../fields/DocSymbols'; -import { Id } from '../../../../fields/FieldSymbols'; import { DocCast, NumCast, StrCast } from '../../../../fields/Types'; -import { emptyFunction, returnEmptyDoclist, returnEmptyFilter, returnFalse, returnOne, setupMoveUpEvents, Utils } from '../../../../Utils'; +import { copyProps, emptyFunction, returnEmptyDoclist, returnEmptyFilter, returnFalse, returnOne, setupMoveUpEvents, Utils } from '../../../../Utils'; import { Docs, DocUtils } from '../../../documents/Documents'; import { DocumentType } from '../../../documents/DocumentTypes'; import { DocumentManager } from '../../../util/DocumentManager'; import { DragManager } from '../../../util/DragManager'; import { LinkManager } from '../../../util/LinkManager'; -import { SnappingManager } from '../../../util/SnappingManager'; import { Transform } from '../../../util/Transform'; import { undoable, UndoManager } from '../../../util/UndoManager'; import { MarqueeOptionsMenu } from '../../collections/collectionFreeForm'; @@ -68,7 +66,19 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps private _sidebarRef = React.createRef<SidebarAnnos>(); private _ref: React.RefObject<HTMLDivElement> = React.createRef(); private _disposers: { [key: string]: IReactionDisposer } = {}; - private _setPreviewCursor: undefined | ((x: number, y: number, drag: boolean, hide: boolean, doc: Opt<Doc>) => void); + + _unmounting = false; + _prevProps: ViewBoxAnnotatableProps & FieldViewProps; + @override _props: ViewBoxAnnotatableProps & FieldViewProps; + constructor(props: ViewBoxAnnotatableProps & FieldViewProps) { + super(props); + this._props = this._prevProps = props; + makeObservable(this); + } + + componentDidUpdate() { + copyProps(this); + } @observable private _savedAnnotations = new ObservableMap<number, HTMLDivElement[]>(); @computed get allSidebarDocs() { @@ -88,7 +98,7 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps return StrCast(this.layoutDoc._layout_sidebarWidthPercent, '0%'); } @computed get sidebarColor() { - return StrCast(this.layoutDoc.sidebar_color, StrCast(this.layoutDoc[this.props.fieldKey + '_backgroundColor'], '#e4e4e4')); + return StrCast(this.layoutDoc.sidebar_color, StrCast(this.layoutDoc[this._props.fieldKey + '_backgroundColor'], '#e4e4e4')); } @computed get SidebarKey() { return this.fieldKey + '_sidebar'; @@ -96,10 +106,9 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps componentDidMount() { this._unmounting = false; - this.props.setContentView?.(this); + this._props.setContentView?.(this); } - _unmounting = false; componentWillUnmount(): void { this._unmounting = true; this.deselectPin(); @@ -161,9 +170,9 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps e, (e, down, delta) => runInAction(() => { - const localDelta = this.props + const localDelta = this._props .ScreenToLocalTransform() - .scale(this.props.NativeDimScaling?.() || 1) + .scale(this._props.NativeDimScaling?.() || 1) .transformDirection(delta[0], delta[1]); const fullWidth = NumCast(this.layoutDoc._width); const mapWidth = fullWidth - this.sidebarWidth(); @@ -182,7 +191,7 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps () => UndoManager.RunInBatch(this.toggleSidebar, 'toggle sidebar map') ); }; - sidebarWidth = () => (Number(this.sidebarWidthPercent.substring(0, this.sidebarWidthPercent.length - 1)) / 100) * this.props.PanelWidth(); + sidebarWidth = () => (Number(this.sidebarWidthPercent.substring(0, this.sidebarWidthPercent.length - 1)) / 100) * this._props.PanelWidth(); /** * Handles toggle of sidebar on click the little comment button @@ -194,7 +203,7 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps key="sidebar" title="Toggle Sidebar" style={{ - display: !this.props.isContentActive() ? 'none' : undefined, + display: !this._props.isContentActive() ? 'none' : undefined, top: StrCast(this.layoutDoc._layout_showTitle) === 'title' ? 20 : 5, backgroundColor: this.SidebarShown ? Colors.MEDIUM_BLUE : Colors.BLACK, }} @@ -231,12 +240,12 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps FormattedTextBox.SetSelectOnLoad(target); return target; }; - const docView = this.props.DocumentView?.(); + const docView = this._props.DocumentView?.(); docView && DragManager.StartAnchorAnnoDrag([ele], new DragManager.AnchorAnnoDragData(docView, sourceAnchorCreator, targetCreator), e.pageX, e.pageY, { dragComplete: e => { if (!e.aborted && e.annoDragData && e.annoDragData.linkSourceDoc && e.annoDragData.dropDocument && e.linkDocument) { - e.annoDragData.linkSourceDoc.followLinkToggle = e.annoDragData.dropDocument.annotationOn === this.props.Document; + e.annoDragData.linkSourceDoc.followLinkToggle = e.annoDragData.dropDocument.annotationOn === this.Document; e.annoDragData.linkSourceDoc.followLinkZoom = false; } }, @@ -271,19 +280,17 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps return false; }; - setPreviewCursor = (func?: (x: number, y: number, drag: boolean, hide: boolean, doc: Opt<Doc>) => void) => (this._setPreviewCursor = func); - addDocumentWrapper = (doc: Doc | Doc[], annotationKey?: string) => this.addDocument(doc, annotationKey); - pointerEvents = () => (this.props.isContentActive() && !MarqueeOptionsMenu.Instance.isShown() ? 'all' : 'none'); + pointerEvents = () => (this._props.isContentActive() && !MarqueeOptionsMenu.Instance.isShown() ? 'all' : 'none'); - panelWidth = () => this.props.PanelWidth() / (this.props.NativeDimScaling?.() || 1) - this.sidebarWidth(); - panelHeight = () => this.props.PanelHeight() / (this.props.NativeDimScaling?.() || 1); - scrollXf = () => this.props.ScreenToLocalTransform().translate(0, NumCast(this.layoutDoc._layout_scrollTop)); - transparentFilter = () => [...this.props.childFilters(), Utils.TransparentBackgroundFilter]; - opaqueFilter = () => [...this.props.childFilters(), Utils.OpaqueBackgroundFilter]; - infoWidth = () => this.props.PanelWidth() / 5; - infoHeight = () => this.props.PanelHeight() / 5; + panelWidth = () => this._props.PanelWidth() / (this._props.NativeDimScaling?.() || 1) - this.sidebarWidth(); + panelHeight = () => this._props.PanelHeight() / (this._props.NativeDimScaling?.() || 1); + scrollXf = () => this._props.ScreenToLocalTransform().translate(0, NumCast(this.layoutDoc._layout_scrollTop)); + transparentFilter = () => [...this._props.childFilters(), Utils.TransparentBackgroundFilter]; + opaqueFilter = () => [...this._props.childFilters(), Utils.OpaqueBackgroundFilter]; + infoWidth = () => this._props.PanelWidth() / 5; + infoHeight = () => this._props.PanelHeight() / 5; anchorMenuClick = () => this._sidebarRef.current?.anchorMenuClick; savedAnnotations = () => this._savedAnnotations; @@ -348,7 +355,7 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps }, 'createpin'); // The pin that is selected - @observable selectedPin: Doc | undefined; + @observable selectedPin: Doc | undefined = undefined; @action deselectPin = () => { @@ -398,9 +405,9 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps MapAnchorMenu.Instance.StartDrag = this.startAnchorDrag; const point = this._bingMap.current.tryLocationToPixel(new this.MicrosoftMaps.Location(this.selectedPin.latitude, this.selectedPin.longitude)); - const x = point.x + (this.props.PanelWidth() - this.sidebarWidth()) / 2; - const y = point.y + this.props.PanelHeight() / 2 + 32; - const cpt = this.props.ScreenToLocalTransform().inverse().transformPoint(x, y); + const x = point.x + (this._props.PanelWidth() - this.sidebarWidth()) / 2; + const y = point.y + this._props.PanelHeight() / 2 + 32; + const cpt = this._props.ScreenToLocalTransform().inverse().transformPoint(x, y); MapAnchorMenu.Instance.jumpTo(cpt[0], cpt[1], true); document.addEventListener('pointerdown', this.tryHideMapAnchorMenu, true); @@ -411,7 +418,7 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps */ @action mapOnClick = (e: { location: { latitude: any; longitude: any } }) => { - this.props.select(false); + this._props.select(false); this.deselectPin(); }; /* @@ -454,6 +461,7 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps /* * Returns doc w/ relevant info */ + getAnchor = (addAsAnnotation: boolean, pinProps?: PinProps, existingPin?: Doc) => { /// this should use SELECTED pushpin for lat/long if there is a selection, otherwise CENTER const anchor = Docs.Create.ConfigDocument({ @@ -599,6 +607,7 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps * Initializes starting values */ @observable _mapReady = false; + @action bingMapReady = (map: any) => { this._mapReady = true; @@ -676,9 +685,9 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps let target = document.elementFromPoint(e.x, e.y); while (target) { if (target === this._ref.current) { - const cpt = this.props.ScreenToLocalTransform().transformPoint(e.clientX, e.clientY); - const x = cpt[0] - (this.props.PanelWidth() - this.sidebarWidth()) / 2; - const y = cpt[1] - 32 /* height of search bar */ - this.props.PanelHeight() / 2; + const cpt = this._props.ScreenToLocalTransform().transformPoint(e.clientX, e.clientY); + const x = cpt[0] - (this._props.PanelWidth() - this.sidebarWidth()) / 2; + const y = cpt[1] - 32 /* height of search bar */ - this._props.PanelHeight() / 2; const location = this._bingMap.current.tryPixelToLocation(new this.MicrosoftMaps.Point(x, y)); this.createPushpin(location.latitude, location.longitude); break; @@ -717,7 +726,6 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps return null; } - const renderAnnotations = (childFilters?: () => string[]) => null; return ( <div className="mapBox" ref={this._ref}> <div @@ -727,10 +735,6 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps e.button === 0 && !e.ctrlKey && e.stopPropagation(); }} style={{ width: `calc(100% - ${this.sidebarWidthPercent})`, pointerEvents: this.pointerEvents() }}> - <div style={{ mixBlendMode: 'multiply' }}>{renderAnnotations(this.transparentFilter)}</div> - {renderAnnotations(this.opaqueFilter)} - {SnappingManager.GetIsDragging() ? null : renderAnnotations()} - <div className="mapBox-searchbar"> <EditableText // editing @@ -771,8 +775,8 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps .map((pushpin, i) => ( <DocumentView key={i} - {...this.props} - renderDepth={this.props.renderDepth + 1} + {...this._props} + renderDepth={this._props.renderDepth + 1} Document={pushpin} TemplateDataDocument={undefined} PanelWidth={returnOne} @@ -799,7 +803,7 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps <div className="mapBox-sidebar" style={{ width: `${this.sidebarWidthPercent}`, backgroundColor: `${this.sidebarColor}` }}> <SidebarAnnos ref={this._sidebarRef} - {...this.props} + {...this._props} fieldKey={this.fieldKey} Document={this.Document} layoutDoc={this.layoutDoc} diff --git a/src/client/views/nodes/MapBox/MapBox2.tsx b/src/client/views/nodes/MapBox/MapBox2.tsx index 39ed6a47e..8a6d68a71 100644 --- a/src/client/views/nodes/MapBox/MapBox2.tsx +++ b/src/client/views/nodes/MapBox/MapBox2.tsx @@ -101,7 +101,7 @@ // } // @observable private _map: google.maps.Map = null as unknown as google.maps.Map; -// @observable private selectedPlace: Doc | undefined; +// @observable private selectedPlace: Doc | undefined = undefined; // @observable private markerMap: { [id: string]: google.maps.Marker } = {}; // @observable private center = navigator.geolocation ? navigator.geolocation.getCurrentPosition : defaultCenter; // @observable private inputRef = React.createRef<HTMLInputElement>(); diff --git a/src/client/views/nodes/MapBox/MapPushpinBox.tsx b/src/client/views/nodes/MapBox/MapPushpinBox.tsx index 590f8735c..8760c8600 100644 --- a/src/client/views/nodes/MapBox/MapPushpinBox.tsx +++ b/src/client/views/nodes/MapBox/MapPushpinBox.tsx @@ -1,15 +1,11 @@ -import { observer } from 'mobx-react'; -// import { SettingsManager } from '../../../util/SettingsManager'; +import * as React from 'react'; import { ViewBoxBaseComponent } from '../../DocComponent'; import { FieldView, FieldViewProps } from '../FieldView'; -import * as React from 'react'; -import { computed } from 'mobx'; import { MapBox } from './MapBox'; /** * Map Pushpin doc class */ -@observer export class MapPushpinBox extends ViewBoxBaseComponent<FieldViewProps>() { public static LayoutString(fieldKey: string) { return FieldView.LayoutString(MapPushpinBox, fieldKey); @@ -21,11 +17,11 @@ export class MapPushpinBox extends ViewBoxBaseComponent<FieldViewProps>() { this.mapBoxView.deletePushpin(this.Document); } - @computed get mapBoxView() { - return this.props.DocumentView?.()?.props.docViewPath().lastElement()?.ComponentView as MapBox; + get mapBoxView() { + return this.props.DocumentView?.()?._props.docViewPath().lastElement()?.ComponentView as MapBox; } - @computed get mapBox() { - return this.props.DocumentView?.().props.docViewPath().lastElement()?.Document; + get mapBox() { + return this.props.DocumentView?.()._props.docViewPath().lastElement()?.Document; } render() { diff --git a/src/client/views/nodes/PDFBox.tsx b/src/client/views/nodes/PDFBox.tsx index 8de8498d7..2a884cef8 100644 --- a/src/client/views/nodes/PDFBox.tsx +++ b/src/client/views/nodes/PDFBox.tsx @@ -1,5 +1,5 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { action, computed, IReactionDisposer, observable, reaction, runInAction } from 'mobx'; +import { action, computed, IReactionDisposer, makeObservable, observable, override, reaction, runInAction } from 'mobx'; import { observer } from 'mobx-react'; import * as Pdfjs from 'pdfjs-dist'; import 'pdfjs-dist/web/pdf_viewer.css'; @@ -10,7 +10,7 @@ import { ComputedField } from '../../../fields/ScriptField'; import { Cast, FieldValue, ImageCast, NumCast, StrCast } from '../../../fields/Types'; import { ImageField, PdfField } from '../../../fields/URLField'; import { TraceMobx } from '../../../fields/util'; -import { emptyFunction, returnFalse, setupMoveUpEvents, Utils } from '../../../Utils'; +import { copyProps, emptyFunction, returnFalse, setupMoveUpEvents, Utils } from '../../../Utils'; import { Docs, DocUtils } from '../../documents/Documents'; import { CollectionViewType, DocumentType } from '../../documents/DocumentTypes'; import { DocumentManager } from '../../util/DocumentManager'; @@ -52,14 +52,18 @@ export class PDFBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps @observable private _pageControls = false; @computed get pdfUrl() { - return Cast(this.dataDoc[this.props.fieldKey], PdfField); + return Cast(this.dataDoc[this._props.fieldKey], PdfField); } @computed get pdfThumb() { return ImageCast(this.layoutDoc['thumb-frozen'], ImageCast(this.layoutDoc.thumb))?.url; } - constructor(props: any) { + _prevProps: ViewBoxAnnotatableProps & FieldViewProps; + @override _props: ViewBoxAnnotatableProps & FieldViewProps; + constructor(props: ViewBoxAnnotatableProps & FieldViewProps) { super(props); + this._props = this._prevProps = props; + makeObservable(this); const nw = Doc.NativeWidth(this.Document, this.dataDoc) || 927; const nh = Doc.NativeHeight(this.Document, this.dataDoc) || 1200; !this.Document._layout_fitWidth && (this.Document._height = NumCast(this.Document._width) * (nh / nw)); @@ -68,6 +72,9 @@ export class PDFBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps else if (PDFBox.pdfpromise.get(this.pdfUrl.url.href)) PDFBox.pdfpromise.get(this.pdfUrl.url.href)?.then(action((pdf: any) => (this._pdf = pdf))); } } + componentDidUpdate() { + copyProps(this); + } replaceCanvases = (oldDiv: HTMLElement, newDiv: HTMLElement) => { if (oldDiv.childNodes) { @@ -101,7 +108,7 @@ export class PDFBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps Doc.GetProto(region).followLinkToggle = true; this.addDocument(region); - const docViewContent = this.props.docViewPath().lastElement().ContentDiv!; + const docViewContent = this._props.docViewPath().lastElement().ContentDiv!; const newDiv = docViewContent.cloneNode(true) as HTMLDivElement; newDiv.style.width = NumCast(this.layoutDoc._width).toString(); newDiv.style.height = NumCast(this.layoutDoc._height).toString(); @@ -110,8 +117,8 @@ export class PDFBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps const anchx = NumCast(cropping.x); const anchy = NumCast(cropping.y); - const anchw = NumCast(cropping._width) * (this.props.NativeDimScaling?.() || 1); - const anchh = NumCast(cropping._height) * (this.props.NativeDimScaling?.() || 1); + const anchw = NumCast(cropping._width) * (this._props.NativeDimScaling?.() || 1); + const anchh = NumCast(cropping._height) * (this._props.NativeDimScaling?.() || 1); const viewScale = 1; cropping.title = 'crop: ' + this.Document.title; cropping.x = NumCast(this.Document.x) + NumCast(this.layoutDoc._width); @@ -131,7 +138,7 @@ export class PDFBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps if (addCrop) { DocUtils.MakeLink(region, cropping, { link_relationship: 'cropped image' }); } - this.props.bringToFront(cropping); + this._props.bringToFront(cropping); CreateImage( '', @@ -139,8 +146,8 @@ export class PDFBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps htmlString, anchw, anchh, - (NumCast(region.y) * this.props.PanelWidth()) / NumCast(this.dataDoc[this.fieldKey + '_nativeWidth']), - (NumCast(region.x) * this.props.PanelWidth()) / NumCast(this.dataDoc[this.fieldKey + '_nativeWidth']), + (NumCast(region.y) * this._props.PanelWidth()) / NumCast(this.dataDoc[this.fieldKey + '_nativeWidth']), + (NumCast(region.x) * this._props.PanelWidth()) / NumCast(this.dataDoc[this.fieldKey + '_nativeWidth']), 4 ) .then((data_url: any) => { @@ -162,7 +169,7 @@ export class PDFBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps updateIcon = () => { // currently we render pdf icons as text labels - const docViewContent = this.props.docViewPath().lastElement().ContentDiv!; + const docViewContent = this._props.docViewPath().lastElement().ContentDiv!; const filename = this.layoutDoc[Id] + '-icon' + new Date().getTime(); this._pdfViewer?._mainCont.current && CollectionFreeFormView.UpdateIcon( @@ -170,8 +177,8 @@ export class PDFBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps docViewContent, NumCast(this.layoutDoc._width), NumCast(this.layoutDoc._height), - this.props.PanelWidth(), - this.props.PanelHeight(), + this._props.PanelWidth(), + this._props.PanelHeight(), NumCast(this.layoutDoc._layout_scrollTop), NumCast(this.dataDoc[this.fieldKey + '_nativeHeight'], 1), true, @@ -190,20 +197,20 @@ export class PDFBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps Object.values(this._disposers).forEach(disposer => disposer?.()); } componentDidMount() { - this.props.setContentView?.(this); + this._props.setContentView?.(this); this._disposers.select = reaction( - () => this.props.isSelected(), + () => this._props.isSelected(), () => { document.removeEventListener('keydown', this.onKeyDown); - this.props.isSelected() && document.addEventListener('keydown', this.onKeyDown); + this._props.isSelected() && document.addEventListener('keydown', this.onKeyDown); }, { fireImmediately: true } ); this._disposers.scroll = reaction( () => this.layoutDoc.layout_scrollTop, () => { - if (!(ComputedField.WithoutComputed(() => FieldValue(this.props.Document[this.SidebarKey + '_panY'])) instanceof ComputedField)) { - this.props.Document[this.SidebarKey + '_panY'] = ComputedField.MakeFunction('this.layout_scrollTop'); + if (!(ComputedField.WithoutComputed(() => FieldValue(this.Document[this.SidebarKey + '_panY'])) instanceof ComputedField)) { + this.Document[this.SidebarKey + '_panY'] = ComputedField.MakeFunction('this.layout_scrollTop'); } this.layoutDoc[this.SidebarKey + '_freeform_scale'] = 1; this.layoutDoc[this.SidebarKey + '_freeform_panX'] = 0; @@ -212,11 +219,11 @@ export class PDFBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps } sidebarAddDocTab = (doc: Doc, where: OpenWhere) => { - if (DocListCast(this.props.Document[this.props.fieldKey + '_sidebar']).includes(doc) && !this.SidebarShown) { + if (DocListCast(this.Document[this._props.fieldKey + '_sidebar']).includes(doc) && !this.SidebarShown) { this.toggleSidebar(false); return true; } - return this.props.addDocTab(doc, where); + return this._props.addDocTab(doc, where); }; focus = (anchor: Doc, options: DocFocusOptions) => { this._initialScrollTarget = anchor; @@ -251,7 +258,7 @@ export class PDFBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps @action loaded = (nw: number, nh: number, np: number) => { - this.dataDoc[this.props.fieldKey + '_numPages'] = np; + this.dataDoc[this._props.fieldKey + '_numPages'] = np; Doc.SetNativeWidth(this.dataDoc, Math.max(Doc.NativeWidth(this.dataDoc), (nw * 96) / 72)); Doc.SetNativeHeight(this.dataDoc, (nh * 96) / 72); this.layoutDoc._height = NumCast(this.layoutDoc._width) / (Doc.NativeAspect(this.dataDoc) || 1); @@ -276,7 +283,7 @@ export class PDFBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps return true; }; public forwardPage = () => { - this.Document._layout_curPage = Math.min(NumCast(this.dataDoc[this.props.fieldKey + '_numPages']), (NumCast(this.Document._layout_curPage) || 1) + 1); + this.Document._layout_curPage = Math.min(NumCast(this.dataDoc[this._props.fieldKey + '_numPages']), (NumCast(this.Document._layout_curPage) || 1) + 1); return true; }; public gotoPage = (p: number) => (this.Document._layout_curPage = p); @@ -300,7 +307,7 @@ export class PDFBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps setPdfViewer = (pdfViewer: PDFViewer) => { this._pdfViewer = pdfViewer; - const docView = this.props.DocumentView?.(); + const docView = this._props.DocumentView?.(); if (this._initialScrollTarget && docView) { this.focus(this._initialScrollTarget, { instant: true }); this._initialScrollTarget = undefined; @@ -321,13 +328,13 @@ export class PDFBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps this, e, (e, down, delta) => { - const localDelta = this.props + const localDelta = this._props .ScreenToLocalTransform() - .scale(this.props.NativeDimScaling?.() || 1) + .scale(this._props.NativeDimScaling?.() || 1) .transformDirection(delta[0], delta[1]); const nativeWidth = NumCast(this.layoutDoc[this.fieldKey + '_nativeWidth']); const curNativeWidth = NumCast(this.layoutDoc.nativeWidth, nativeWidth); - const ratio = (curNativeWidth + ((onButton ? 1 : -1) * localDelta[0]) / (this.props.NativeDimScaling?.() || 1)) / nativeWidth; + const ratio = (curNativeWidth + ((onButton ? 1 : -1) * localDelta[0]) / (this._props.NativeDimScaling?.() || 1)) / nativeWidth; if (ratio >= 1) { this.layoutDoc.nativeWidth = nativeWidth * ratio; onButton && (this.layoutDoc._width = NumCast(this.layoutDoc._width) + localDelta[0]); @@ -372,12 +379,12 @@ export class PDFBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps ); const searchTitle = `${!this._searching ? 'Open' : 'Close'} Search Bar`; const curPage = NumCast(this.Document._layout_curPage) || 1; - return !this.props.isContentActive() || this._pdfViewer?.isAnnotating ? null : ( + return !this._props.isContentActive() || this._pdfViewer?.isAnnotating ? null : ( <div className="pdfBox-ui" onKeyDown={e => ([KeyCodes.BACKSPACE, KeyCodes.DELETE].includes(e.keyCode) ? e.stopPropagation() : true)} onPointerDown={e => e.stopPropagation()} - style={{ display: this.props.isContentActive() ? 'flex' : 'none' }}> + style={{ display: this._props.isContentActive() ? 'flex' : 'none' }}> <div className="pdfBox-overlayCont" onPointerDown={e => e.stopPropagation()} style={{ left: `${this._searching ? 0 : 100}%` }}> <button className="pdfBox-overlayButton" title={searchTitle} /> <input @@ -431,7 +438,7 @@ export class PDFBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps if (!this.SidebarShown) return 0; if (this._previewWidth) return PDFBox.sidebarResizerWidth + PDFBox.openSidebarWidth; // return default sidebar if previewing (as in viewing a link target) const nativeDiff = NumCast(this.layoutDoc.nativeWidth) - Doc.NativeWidth(this.dataDoc); - return PDFBox.sidebarResizerWidth + nativeDiff * (this.props.NativeDimScaling?.() || 1); + return PDFBox.sidebarResizerWidth + nativeDiff * (this._props.NativeDimScaling?.() || 1); }; @undoBatch toggleSidebarType = () => (this.dataDoc[this.SidebarKey + '_type_collection'] = this.dataDoc[this.SidebarKey + '_type_collection'] === CollectionViewType.Freeform ? CollectionViewType.Stacking : CollectionViewType.Freeform); @@ -450,11 +457,11 @@ export class PDFBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps }; @computed get renderTitleBox() { - const classname = 'pdfBox' + (this.props.isContentActive() ? '-interactive' : ''); + const classname = 'pdfBox' + (this._props.isContentActive() ? '-interactive' : ''); return ( <div className={classname}> <div className="pdfBox-title-outer"> - <strong className="pdfBox-title">{StrCast(this.props.Document.title)}</strong> + <strong className="pdfBox-title">{StrCast(this.Document.title)}</strong> </div> </div> ); @@ -472,7 +479,7 @@ export class PDFBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps key="sidebar" title="Toggle Sidebar" style={{ - display: !this.props.isContentActive() ? 'none' : undefined, + display: !this._props.isContentActive() ? 'none' : undefined, top: StrCast(this.layoutDoc._layout_showTitle) === 'title' ? 20 : 5, backgroundColor: this.SidebarShown ? Colors.MEDIUM_BLUE : Colors.BLACK, }} @@ -489,26 +496,26 @@ export class PDFBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps const pdfNativeWidth = NumCast(this.layoutDoc[this.fieldKey + '_nativeWidth']); const nativeWidth = NumCast(this.layoutDoc.nativeWidth, pdfNativeWidth); const pdfRatio = pdfNativeWidth / nativeWidth; - return (pdfRatio * this.props.PanelWidth()) / pdfNativeWidth; + return (pdfRatio * this._props.PanelWidth()) / pdfNativeWidth; } @computed get sidebarNativeWidth() { return this.sidebarWidth() / this.pdfScale; } @computed get sidebarNativeHeight() { - return this.props.PanelHeight() / this.pdfScale; + return this._props.PanelHeight() / this.pdfScale; } sidebarNativeWidthFunc = () => this.sidebarNativeWidth; sidebarNativeHeightFunc = () => this.sidebarNativeHeight; sidebarMoveDocument = (doc: Doc | Doc[], targetCollection: Doc | undefined, addDocument: (doc: Doc | Doc[]) => boolean) => this.moveDocument(doc, targetCollection, addDocument, this.SidebarKey); sidebarRemDocument = (doc: Doc | Doc[]) => this.removeDocument(doc, this.SidebarKey); - sidebarScreenToLocal = () => this.props.ScreenToLocalTransform().translate((this.sidebarWidth() - this.props.PanelWidth()) / this.pdfScale, 0); + sidebarScreenToLocal = () => this._props.ScreenToLocalTransform().translate((this.sidebarWidth() - this._props.PanelWidth()) / this.pdfScale, 0); @computed get sidebarCollection() { const renderComponent = (tag: string) => { const ComponentTag = tag === CollectionViewType.Freeform ? CollectionFreeFormView : CollectionStackingView; return ComponentTag === CollectionStackingView ? ( <SidebarAnnos ref={this._sidebarRef} - {...this.props} + {...this._props} Document={this.Document} layoutDoc={this.layoutDoc} dataDoc={this.dataDoc} @@ -521,13 +528,13 @@ export class PDFBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps removeDocument={this.removeDocument} /> ) : ( - <div onPointerDown={e => setupMoveUpEvents(this, e, returnFalse, emptyFunction, () => SelectionManager.SelectView(this.props.DocumentView?.()!, false), true)}> + <div onPointerDown={e => setupMoveUpEvents(this, e, returnFalse, emptyFunction, () => SelectionManager.SelectView(this._props.DocumentView?.()!, false), true)}> <ComponentTag - {...this.props} + {...this._props} setContentView={emptyFunction} // override setContentView to do nothing NativeWidth={this.sidebarNativeWidthFunc} NativeHeight={this.sidebarNativeHeightFunc} - PanelHeight={this.props.PanelHeight} + PanelHeight={this._props.PanelHeight} PanelWidth={this.sidebarWidth} xPadding={0} yPadding={0} @@ -541,7 +548,7 @@ export class PDFBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps moveDocument={this.sidebarMoveDocument} addDocument={this.sidebarAddDocument} ScreenToLocalTransform={this.sidebarScreenToLocal} - renderDepth={this.props.renderDepth + 1} + renderDepth={this._props.renderDepth + 1} noSidebar={true} fieldKey={this.SidebarKey} /> @@ -557,13 +564,13 @@ export class PDFBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps @computed get renderPdfView() { TraceMobx(); const previewScale = this._previewNativeWidth ? 1 - this.sidebarWidth() / this._previewNativeWidth : 1; - const scale = previewScale * (this.props.NativeDimScaling?.() || 1); + const scale = previewScale * (this._props.NativeDimScaling?.() || 1); return !this._pdf ? null : ( <div className="pdfBox" onContextMenu={this.specificContextMenu} style={{ - height: this.props.Document._layout_scrollTop && !this.Document._layout_fitWidth && window.screen.width > 600 ? (NumCast(this.Document._height) * this.props.PanelWidth()) / NumCast(this.Document._width) : undefined, + height: this.Document._layout_scrollTop && !this.Document._layout_fitWidth && window.screen.width > 600 ? (NumCast(this.Document._height) * this._props.PanelWidth()) / NumCast(this.Document._width) : undefined, }}> <div className="pdfBox-background" onPointerDown={e => this.sidebarBtnDown(e, false)} /> <div @@ -576,7 +583,7 @@ export class PDFBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps top: 0, }}> <PDFViewer - {...this.props} + {...this._props} sidebarAddDoc={this.sidebarAddDocument} addDocTab={this.sidebarAddDocTab} layoutDoc={this.layoutDoc} @@ -594,7 +601,7 @@ export class PDFBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps crop={this.crop} /> </div> - <div style={{ position: 'absolute', height: '100%', right: 0, top: 0, width: `calc(100 * ${this.sidebarWidth() / this.props.PanelWidth()}%` }}>{this.sidebarCollection}</div> + <div style={{ position: 'absolute', height: '100%', right: 0, top: 0, width: `calc(100 * ${this.sidebarWidth() / this._props.PanelWidth()}%` }}>{this.sidebarCollection}</div> {this.settingsPanel()} </div> ); diff --git a/src/client/views/nodes/ScreenshotBox.tsx b/src/client/views/nodes/ScreenshotBox.tsx index 7c21b1893..a7ff8ff8f 100644 --- a/src/client/views/nodes/ScreenshotBox.tsx +++ b/src/client/views/nodes/ScreenshotBox.tsx @@ -43,7 +43,7 @@ declare class MediaRecorder { // @observer // export class VideoTile extends React.Component<VideoTileProps> { -// @observable _videoRef: HTMLVideoElement | undefined; +// @observable _videoRef: HTMLVideoElement | undefined = undefined; // _mesh: any = undefined; // render() { diff --git a/src/client/views/nodes/VideoBox.tsx b/src/client/views/nodes/VideoBox.tsx index 04780155d..2dad115e1 100644 --- a/src/client/views/nodes/VideoBox.tsx +++ b/src/client/views/nodes/VideoBox.tsx @@ -1,6 +1,6 @@ import * as React from 'react'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { action, computed, IReactionDisposer, observable, ObservableMap, reaction, runInAction, untracked } from 'mobx'; +import { action, computed, IReactionDisposer, makeObservable, observable, ObservableMap, override, reaction, runInAction, untracked } from 'mobx'; import { observer } from 'mobx-react'; import { basename } from 'path'; import { Doc, StrListCast } from '../../../fields/Doc'; @@ -9,7 +9,7 @@ import { List } from '../../../fields/List'; import { ObjectField } from '../../../fields/ObjectField'; import { Cast, NumCast, StrCast } from '../../../fields/Types'; import { AudioField, ImageField, VideoField } from '../../../fields/URLField'; -import { emptyFunction, formatTime, returnEmptyString, returnFalse, returnOne, returnZero, setupMoveUpEvents, Utils } from '../../../Utils'; +import { copyProps, emptyFunction, formatTime, returnEmptyString, returnFalse, returnOne, returnZero, setupMoveUpEvents, Utils } from '../../../Utils'; import { Docs, DocUtils } from '../../documents/Documents'; import { DocumentType } from '../../documents/DocumentTypes'; import { Networking } from '../../Network'; @@ -33,7 +33,6 @@ import { FieldView, FieldViewProps } from './FieldView'; import { RecordingBox } from './RecordingBox'; import { PinProps, PresBox } from './trails'; import './VideoBox.scss'; -const path = require('path'); /** * VideoBox @@ -52,7 +51,6 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp public static LayoutString(fieldKey: string) { return FieldView.LayoutString(VideoBox, fieldKey); } - static _youtubeIframeCounter: number = 0; static heightPercent = 80; // height of video relative to videoBox when timeline is open static numThumbnails = 20; @@ -70,6 +68,19 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp private _playRegionTimer: any = null; // timeout for playback private _controlsFadeTimer: any = null; // timeout for controls fade + _prevProps: ViewBoxAnnotatableProps & FieldViewProps; + @override _props: ViewBoxAnnotatableProps & FieldViewProps; + constructor(props: ViewBoxAnnotatableProps & FieldViewProps) { + super(props); + this._props = this._prevProps = props; + makeObservable(this); + this._props.setContentView?.(this); + } + + componentDidUpdate() { + copyProps(this); + } + @observable _stackedTimeline: any; // CollectionStackedTimeline ref @observable static _nativeControls: boolean; // default html controls @observable _savedAnnotations = new ObservableMap<number, HTMLDivElement[]>(); @@ -96,13 +107,13 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp @observable rawDuration: number = 0; @computed get youtubeVideoId() { - const field = Cast(this.dataDoc[this.props.fieldKey], VideoField); + const field = Cast(this.dataDoc[this._props.fieldKey], VideoField); return field && field.url.href.indexOf('youtube') !== -1 ? ((arr: string[]) => arr[arr.length - 1])(field.url.href.split('/')) : ''; } // returns the path of the audio file @computed get audiopath() { - const field = Cast(this.props.Document[this.props.fieldKey + '_audio'], AudioField, null); + const field = Cast(this.Document[this._props.fieldKey + '_audio'], AudioField, null); const vfield = Cast(this.dataDoc[this.fieldKey], VideoField, null); return field?.url.href ?? vfield?.url.href ?? ''; } @@ -125,7 +136,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp componentDidMount() { this.unmounting = false; - this.props.setContentView?.(this); // this tells the DocumentView that this VideoBox is the "content" of the document. this allows the DocumentView to indirectly call getAnchor() on the VideoBox when making a link. + this._props.setContentView?.(this); // this tells the DocumentView that this VideoBox is the "content" of the document. this allows the DocumentView to indirectly call getAnchor() on the VideoBox when making a link. if (this.youtubeVideoId) { const youtubeaspect = 400 / 315; const nativeWidth = Doc.NativeWidth(this.layoutDoc); @@ -162,7 +173,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp if ( // need to include range inputs because after dragging time slider it becomes target element !(e.target instanceof HTMLInputElement && !(e.target.type === 'range')) && - this.props.isSelected() + this._props.isSelected() ) { switch (e.key) { case 'ArrowLeft': @@ -267,7 +278,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp this.player && this._contentRef && this._contentRef.requestFullscreen(); } try { - this._youtubePlayer && this.props.addDocTab(this.Document, OpenWhere.add); + this._youtubePlayer && this._props.addDocTab(this.Document, OpenWhere.add); } catch (e) { console.log('Video FullScreen Exception:', e); } @@ -328,7 +339,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp title: (this.layoutDoc._layout_currentTimecode || 0).toString(), onClick: FollowLinkScript(), }); - this.props.addDocument?.(b); + this._props.addDocument?.(b); DocUtils.MakeLink(b, this.Document, { link_relationship: 'video snapshot' }); Networking.PostToServer('/youtubeScreenshot', { id: this.youtubeVideoId, @@ -336,7 +347,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp }).then(response => { const resolved = response?.accessPaths?.agnostic?.client; if (resolved) { - this.props.removeDocument?.(b); + this._props.removeDocument?.(b); this.createSnapshotLink(resolved); } }); @@ -377,7 +388,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp }); Doc.SetNativeWidth(Doc.GetProto(imageSnapshot), Doc.NativeWidth(this.layoutDoc)); Doc.SetNativeHeight(Doc.GetProto(imageSnapshot), Doc.NativeHeight(this.layoutDoc)); - this.props.addDocument?.(imageSnapshot); + this._props.addDocument?.(imageSnapshot); const link = DocUtils.MakeLink(imageSnapshot, this.getAnchor(true), { link_relationship: 'video snapshot' }); link && (Doc.GetProto(link.link_anchor_2 as Doc).timecodeToHide = NumCast((link.link_anchor_2 as Doc).timecodeToShow) + 3); setTimeout(() => downX !== undefined && downY !== undefined && DocumentManager.Instance.getFirstDocumentView(imageSnapshot)?.startDragging(downX, downY, 'move', true)); @@ -492,7 +503,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp // context menu specificContextMenu = (e: React.MouseEvent): void => { - const field = Cast(this.dataDoc[this.props.fieldKey], VideoField); + const field = Cast(this.dataDoc[this._props.fieldKey], VideoField); if (field) { const url = field.url.href; const subitems: ContextMenuProps[] = []; @@ -532,7 +543,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp event: () => { this.dataDoc.layout = RecordingBox.LayoutString(this.fieldKey); // delete assoicated video data - this.dataDoc[this.props.fieldKey] = ''; + this.dataDoc[this._props.fieldKey] = ''; this.dataDoc[this.fieldKey + '_duration'] = ''; // delete assoicated presentation data this.dataDoc[this.fieldKey + '_presentation'] = ''; @@ -550,7 +561,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp // renders the video and audio @computed get content() { const field = Cast(this.dataDoc[this.fieldKey], VideoField); - const interactive = Doc.ActiveTool !== InkTool.None || !this.props.isSelected() ? '' : '-interactive'; + const interactive = Doc.ActiveTool !== InkTool.None || !this._props.isSelected() ? '' : '-interactive'; const classname = 'videoBox-content' + (this._fullScreen ? '-fullScreen' : '') + interactive; const opacity = this._scrubbing ? 0.3 : this._controlsVisible ? 1 : 0; return !field ? ( @@ -586,7 +597,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp Not supported. </video> {!this.audiopath || this.audiopath === field.url.href ? null : ( - <audio ref={this.setAudioRef} className={`audiobox-control${this.props.isContentActive() ? '-interactive' : ''}`}> + <audio ref={this.setAudioRef} className={`audiobox-control${this._props.isContentActive() ? '-interactive' : ''}`}> <source src={this.audiopath} type="audio/mpeg" /> Not supported. </audio> @@ -626,7 +637,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp () => !this._playing && this.Seek(NumCast(this.layoutDoc._layout_currentTimecode)) ); this._disposers.youtubeReactionDisposer = reaction( - () => Doc.ActiveTool === InkTool.None && this.props.isSelected() && !SnappingManager.GetIsDragging() && !DocumentView.Interacting, + () => Doc.ActiveTool === InkTool.None && this._props.isSelected() && !SnappingManager.GetIsDragging() && !DocumentView.Interacting, interactive => (iframe.style.pointerEvents = interactive ? 'all' : 'none'), { fireImmediately: true } ); @@ -636,8 +647,8 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp (YT as any)?.ready(() => { this._youtubePlayer = new YT.Player(`${this.youtubeVideoId + this._youtubeIframeId}-player`, { events: { - onReady: this.props.dontRegisterView ? undefined : onYoutubePlayerReady, - onStateChange: this.props.dontRegisterView ? undefined : onYoutubePlayerStateChange, + onReady: this._props.dontRegisterView ? undefined : onYoutubePlayerReady, + onStateChange: this._props.dontRegisterView ? undefined : onYoutubePlayerStateChange, }, }); }); @@ -677,9 +688,9 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp e, action(encodeURIComponent => { this._clicking = false; - if (this.props.isContentActive()) { - // const local = this.props.ScreenToLocalTransform().scale(this.props.scaling?.() || 1).transformPoint(e.clientX, e.clientY); - // this.layoutDoc._layout_timelineHeightPercent = Math.max(0, Math.min(100, local[1] / this.props.PanelHeight() * 100)); + if (this._props.isContentActive()) { + // const local = this._props.ScreenToLocalTransform().scale(this._props.scaling?.() || 1).transformPoint(e.clientX, e.clientY); + // this.layoutDoc._layout_timelineHeightPercent = Math.max(0, Math.min(100, local[1] / this._props.PanelHeight() * 100)); this.layoutDoc._layout_timelineHeightPercent = 80; } @@ -693,15 +704,15 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp 500 ); }, - this.props.isContentActive(), - this.props.isContentActive() + this._props.isContentActive(), + this._props.isContentActive() ); }; // removes from currently playing display @action removeCurrentlyPlaying = () => { - const docView = this.props.DocumentView?.(); + const docView = this._props.DocumentView?.(); if (CollectionStackedTimeline.CurrentlyPlaying && docView) { const index = CollectionStackedTimeline.CurrentlyPlaying.indexOf(docView); index !== -1 && CollectionStackedTimeline.CurrentlyPlaying.splice(index, 1); @@ -710,7 +721,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp // adds doc to currently playing display @action addCurrentlyPlaying = () => { - const docView = this.props.DocumentView?.(); + const docView = this._props.DocumentView?.(); if (!CollectionStackedTimeline.CurrentlyPlaying) { CollectionStackedTimeline.CurrentlyPlaying = []; } @@ -861,7 +872,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp // starts marquee selection marqueeDown = (e: React.PointerEvent) => { - if (!e.altKey && e.button === 0 && NumCast(this.layoutDoc._freeform_scale, 1) === 1 && this.props.isContentActive(true) && ![InkTool.Highlighter, InkTool.Pen].includes(Doc.ActiveTool)) { + if (!e.altKey && e.button === 0 && NumCast(this.layoutDoc._freeform_scale, 1) === 1 && this._props.isContentActive(true) && ![InkTool.Highlighter, InkTool.Pen].includes(Doc.ActiveTool)) { setupMoveUpEvents( this, e, @@ -882,31 +893,31 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp @action finishMarquee = () => { this._marqueeref.current?.onTerminateSelection(); - this.props.select(true); + this._props.select(true); }; - timelineWhenChildContentsActiveChanged = action((isActive: boolean) => this.props.whenChildContentsActiveChanged((this._isAnyChildContentActive = isActive))); + timelineWhenChildContentsActiveChanged = action((isActive: boolean) => this._props.whenChildContentsActiveChanged((this._isAnyChildContentActive = isActive))); timelineScreenToLocal = () => - this.props + this._props .ScreenToLocalTransform() .scale(this.scaling()) - .translate(0, (-this.heightPercent / 100) * this.props.PanelHeight()); + .translate(0, (-this.heightPercent / 100) * this._props.PanelHeight()); setPlayheadTime = (time: number) => (this.player!.currentTime = this.layoutDoc._layout_currentTimecode = time); - timelineHeight = () => (this.props.PanelHeight() * (100 - this.heightPercent)) / 100; + timelineHeight = () => (this._props.PanelHeight() * (100 - this.heightPercent)) / 100; playing = () => this._playing; - scaling = () => this.props.NativeDimScaling?.() || 1; + scaling = () => this._props.NativeDimScaling?.() || 1; - panelWidth = () => (this.props.PanelWidth() * this.heightPercent) / 100; - panelHeight = () => (this.layoutDoc._layout_fitWidth ? this.panelWidth() / (Doc.NativeAspect(this.dataDoc) || 1) : (this.props.PanelHeight() * this.heightPercent) / 100); + panelWidth = () => (this._props.PanelWidth() * this.heightPercent) / 100; + panelHeight = () => (this.layoutDoc._layout_fitWidth ? this.panelWidth() / (Doc.NativeAspect(this.dataDoc) || 1) : (this._props.PanelHeight() * this.heightPercent) / 100); screenToLocalTransform = () => { - const offset = (this.props.PanelWidth() - this.panelWidth()) / 2 / this.scaling(); - return this.props + const offset = (this._props.PanelWidth() - this.panelWidth()) / 2 / this.scaling(); + return this._props .ScreenToLocalTransform() .translate(-offset, 0) .scale(100 / this.heightPercent); @@ -918,10 +929,10 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp // renders video controls componentUI = (boundsLeft: number, boundsTop: number) => { - const xf = this.props.ScreenToLocalTransform().inverse(); - const height = this.props.PanelHeight(); + const xf = this._props.ScreenToLocalTransform().inverse(); + const height = this._props.PanelHeight(); const vidHeight = (height * this.heightPercent) / 100 / this.scaling(); - const vidWidth = this.props.PanelWidth() / this.scaling(); + const vidWidth = this._props.PanelWidth() / this.scaling(); const uiHeight = 25; const uiMargin = 10; const yBot = xf.transformPoint(0, vidHeight)[1]; @@ -936,7 +947,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp <div className="videoBox-ui" style={{ - transform: `rotate(${this.props.ScreenToLocalTransform().inverse().RotateDeg}deg) translate(${-(xRight - xPos) + 10}px, ${yBot - yMid - uiHeight - uiMargin}px)`, + transform: `rotate(${this._props.ScreenToLocalTransform().inverse().RotateDeg}deg) translate(${-(xRight - xPos) + 10}px, ${yBot - yMid - uiHeight - uiMargin}px)`, left: xPos, top: yMid, height: uiHeight, @@ -957,13 +968,13 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp <div className="videoBox-stackPanel" style={{ transition: this.transition, height: `${100 - this.heightPercent}%`, display: this.heightPercent === 100 ? 'none' : '' }}> <CollectionStackedTimeline ref={action((r: any) => (this._stackedTimeline = r))} - {...this.props} + {...this._props} dataFieldKey={this.fieldKey} fieldKey={this.annotationKey} dictationKey={this.fieldKey + '_dictation'} mediaPath={this.audiopath} thumbnails={this.thumbnails} - renderDepth={this.props.renderDepth + 1} + renderDepth={this._props.renderDepth + 1} startTag={'_timecodeToShow' /* videoStart */} endTag={'_timecodeToHide' /* videoEnd */} bringToFront={emptyFunction} @@ -1011,8 +1022,8 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp cropping.title = 'crop: ' + this.Document.title; cropping.x = NumCast(this.Document.x) + NumCast(this.layoutDoc._width); cropping.y = NumCast(this.Document.y); - cropping._width = anchw * (this.props.NativeDimScaling?.() || 1); - cropping._height = anchh * (this.props.NativeDimScaling?.() || 1); + cropping._width = anchw * (this._props.NativeDimScaling?.() || 1); + cropping._height = anchh * (this._props.NativeDimScaling?.() || 1); cropping.timecodeToHide = undefined; cropping.timecodeToShow = undefined; cropping.onClick = undefined; @@ -1038,12 +1049,12 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp if (addCrop) { DocUtils.MakeLink(region, cropping, { link_relationship: 'cropped image' }); } - this.props.bringToFront(cropping); + this._props.bringToFront(cropping); return cropping; }; savedAnnotations = () => this._savedAnnotations; render() { - const borderRad = this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.BorderRounding); + const borderRad = this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.BorderRounding); const borderRadius = borderRad?.includes('px') ? `${Number(borderRad.split('px')[0]) / this.scaling()}px` : borderRad; return ( <div @@ -1053,7 +1064,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp style={{ pointerEvents: this.layoutDoc._lockedPosition ? 'none' : undefined, borderRadius, - overflow: this.props.docViewPath?.().slice(-1)[0].layout_fitWidth ? 'auto' : undefined, + overflow: this._props.docViewPath?.().slice(-1)[0].layout_fitWidth ? 'auto' : undefined, }}> <div className="videoBox-viewer" onPointerDown={this.marqueeDown}> <div @@ -1063,19 +1074,19 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp width: this.panelWidth(), height: this.panelHeight(), top: 0, - left: (this.props.PanelWidth() - this.panelWidth()) / 2, + left: (this._props.PanelWidth() - this.panelWidth()) / 2, }}> <CollectionFreeFormView - {...this.props} + {...this._props} setContentView={emptyFunction} NativeWidth={returnZero} NativeHeight={returnZero} - renderDepth={this.props.renderDepth + 1} + renderDepth={this._props.renderDepth + 1} fieldKey={this.annotationKey} isAnnotationOverlay={true} annotationLayerHostsContent={true} - PanelWidth={this.props.PanelWidth} - PanelHeight={this.props.PanelHeight} + PanelWidth={this._props.PanelWidth} + PanelHeight={this._props.PanelHeight} isAnyChildContentActive={returnFalse} ScreenToLocalTransform={this.screenToLocalTransform} childFilters={this.timelineDocFilter} @@ -1097,8 +1108,8 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp scrollTop={0} annotationLayerScrollTop={0} scaling={returnOne} - annotationLayerScaling={this.props.NativeDimScaling} - docView={this.props.DocumentView!} + annotationLayerScaling={this._props.NativeDimScaling} + docView={this._props.DocumentView!} containerOffset={this.marqueeOffset} addDocument={this.addDocWithTimecode} finishMarquee={this.finishMarquee} @@ -1116,7 +1127,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp } @computed get UIButtons() { - const bounds = this.props.docViewPath().lastElement().getBounds(); + const bounds = this._props.docViewPath().lastElement().getBounds(); const width = (bounds?.right || 0) - (bounds?.left || 0); const curTime = NumCast(this.layoutDoc._layout_currentTimecode); return ( diff --git a/src/client/views/nodes/WebBox.tsx b/src/client/views/nodes/WebBox.tsx index d02e5f3e8..c722399c1 100644 --- a/src/client/views/nodes/WebBox.tsx +++ b/src/client/views/nodes/WebBox.tsx @@ -67,7 +67,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps @observable private _searching: boolean = false; @observable private _showSidebar = false; @observable private _webPageHasBeenRendered = false; - @observable private _marqueeing: number[] | undefined; + @observable private _marqueeing: number[] | undefined = undefined; get marqueeing() { return this._marqueeing; } @@ -403,7 +403,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps const locpt = { x: (e.clientX / NumCast(this.Document.nativeWidth)) * this.props.PanelWidth(), y: ((e.clientY - NumCast(this.layoutDoc.layout_scrollTop))/ NumCast(this.Document.nativeHeight)) * this.props.PanelHeight() }; // prettier-ignore - const scrclick = this.props.DocumentView?.().props.ScreenToLocalTransform().inverse().transformPoint(locpt.x, locpt.y)!; + const scrclick = this.props.DocumentView?.()._props.ScreenToLocalTransform().inverse().transformPoint(locpt.x, locpt.y)!; const scrcent = this.props .DocumentView?.() .props.ScreenToLocalTransform() diff --git a/src/client/views/nodes/audio/AudioWaveform.scss b/src/client/views/nodes/audio/AudioWaveform.scss new file mode 100644 index 000000000..6cbd1759a --- /dev/null +++ b/src/client/views/nodes/audio/AudioWaveform.scss @@ -0,0 +1,17 @@ +.audioWaveform { + position: relative; + width: 100%; + height: 200%; + overflow: hidden; + z-index: -1000; + bottom: 0; + pointer-events: none; + div { + height: 100% !important; + width: 100% !important; + } + canvas { + height: 100% !important; + width: 100% !important; + } +} diff --git a/src/client/views/nodes/audio/AudioWaveform.tsx b/src/client/views/nodes/audio/AudioWaveform.tsx new file mode 100644 index 000000000..1b1a85800 --- /dev/null +++ b/src/client/views/nodes/audio/AudioWaveform.tsx @@ -0,0 +1,127 @@ +import axios from 'axios'; +import { computed, IReactionDisposer, makeObservable, observable, reaction } from 'mobx'; +import { observer } from 'mobx-react'; +import * as React from 'react'; +import { Doc, NumListCast } from '../../../../fields/Doc'; +import { List } from '../../../../fields/List'; +import { listSpec } from '../../../../fields/Schema'; +import { Cast } from '../../../../fields/Types'; +import { copyProps, numberRange } from '../../../../Utils'; +import { Colors } from './../../global/globalEnums'; +import './AudioWaveform.scss'; +import { WaveCanvas } from './WaveCanvas'; + +/** + * AudioWaveform + * + * Used in CollectionStackedTimeline to render a canvas with a visual of an audio waveform for AudioBox and VideoBox documents. + * Uses react-audio-waveform package. + * Bins the audio data into audioBuckets which are passed to package to render the lines. + * Calculates new buckets each time a new zoom factor or new set of trim bounds is created and stores it in a field on the layout doc with a title indicating the bounds and zoom for that list (see audioBucketField) + */ + +export interface AudioWaveformProps { + duration: number; // length of media clip + rawDuration: number; // length of underlying media data + mediaPath: string; + layoutDoc: Doc; + clipStart: number; + clipEnd: number; + zoomFactor: number; + PanelHeight: number; + PanelWidth: number; + fieldKey: string; + progress?: number; +} + +@observer +export class AudioWaveform extends React.Component<AudioWaveformProps> { + public static NUMBER_OF_BUCKETS = 100; // number of buckets data is divided into to draw waveform lines + _disposer: IReactionDisposer | undefined; + _prevProps: React.PropsWithChildren<AudioWaveformProps>; + @observable _props: React.PropsWithChildren<AudioWaveformProps>; + constructor(props: React.PropsWithChildren<AudioWaveformProps>) { + super(props); + this._props = this._prevProps = props; + makeObservable(this); + } + + componentDidUpdate() { + copyProps(this); + } + + get waveHeight() { + return Math.max(50, this._props.PanelHeight); + } + + get clipStart() { + return this._props.clipStart; + } + get clipEnd() { + return this._props.clipEnd; + } + get zoomFactor() { + return this._props.zoomFactor; + } + + @computed get audioBuckets() { + return NumListCast(this._props.layoutDoc[this.audioBucketField(this.clipStart, this.clipEnd, this.zoomFactor)]); + } + + audioBucketField = (start: number, end: number, zoomFactor: number) => this._props.fieldKey + '_audioBuckets/' + '/' + start.toFixed(2).replace('.', '_') + '/' + end.toFixed(2).replace('.', '_') + '/' + zoomFactor * 10; + + componentWillUnmount() { + this._disposer?.(); + } + + componentDidMount() { + this._disposer = reaction( + () => ({ clipStart: this.clipStart, clipEnd: this.clipEnd, fieldKey: this.audioBucketField(this.clipStart, this.clipEnd, this.zoomFactor), zoomFactor: this._props.zoomFactor }), + ({ clipStart, clipEnd, fieldKey, zoomFactor }) => { + if (!this._props.layoutDoc[fieldKey] && this._props.layoutDoc.layout_fieldKey != 'layout_icon') { + // setting these values here serves as a "lock" to prevent multiple attempts to create the waveform at nerly the same time. + const waveform = Cast(this._props.layoutDoc[this.audioBucketField(0, this._props.rawDuration, 1)], listSpec('number')); + this._props.layoutDoc[fieldKey] = waveform && new List<number>(waveform.slice((clipStart / this._props.rawDuration) * waveform.length, (clipEnd / this._props.rawDuration) * waveform.length)); + setTimeout(() => this.createWaveformBuckets(fieldKey, clipStart, clipEnd, zoomFactor)); + } + }, + { fireImmediately: true } + ); + } + + // decodes the audio file into peaks for generating the waveform + createWaveformBuckets = (fieldKey: string, clipStart: number, clipEnd: number, zoomFactor: number) => { + axios({ url: this._props.mediaPath, responseType: 'arraybuffer' }).then(response => + new window.AudioContext().decodeAudioData(response.data, buffer => { + const rawDecodedAudioData = buffer.getChannelData(0); + const startInd = clipStart / this._props.rawDuration; + const endInd = clipEnd / this._props.rawDuration; + const decodedAudioData = rawDecodedAudioData.slice(Math.floor(startInd * rawDecodedAudioData.length), Math.floor(endInd * rawDecodedAudioData.length)); + const numBuckets = Math.floor(AudioWaveform.NUMBER_OF_BUCKETS * zoomFactor); + + const bucketDataSize = Math.floor(decodedAudioData.length / numBuckets); + const brange = Array.from(Array(bucketDataSize)); + const bucketList = numberRange(numBuckets).map((i: number) => brange.reduce((p, x, j) => Math.abs(Math.max(p, decodedAudioData[i * bucketDataSize + j])), 0) / 2); + this._props.layoutDoc[fieldKey] = new List<number>(bucketList); + }) + ); + }; + + render() { + return ( + <div className="audioWaveform"> + <WaveCanvas + color={Colors.LIGHT_GRAY} + progressColor={Colors.MEDIUM_BLUE_ALT} + progress={this._props.progress ?? 1} + barWidth={200 / this.audioBuckets.length} + //gradientColors={this.props.gradientColors} + peaks={this.audioBuckets} + width={(this._props.PanelWidth ?? 0) * window.devicePixelRatio} + height={this.waveHeight * window.devicePixelRatio} + pixelRatio={window.devicePixelRatio} + /> + </div> + ); + } +} diff --git a/src/client/views/nodes/audio/WaveCanvas.tsx b/src/client/views/nodes/audio/WaveCanvas.tsx new file mode 100644 index 000000000..d3f5669a2 --- /dev/null +++ b/src/client/views/nodes/audio/WaveCanvas.tsx @@ -0,0 +1,100 @@ +import React from 'react'; + +interface WaveCanvasProps { + barWidth: number; + color: string; + progress: number; + progressColor: string; + gradientColors?: { stopPosition: number; color: string }[]; // stopPosition between 0 and 1 + peaks: number[]; + width: number; + height: number; + pixelRatio: number; +} + +export class WaveCanvas extends React.Component<WaveCanvasProps> { + // If the first value of peaks is negative, addToIndices will be 1 + posPeaks = (peaks: number[], addToIndices: number) => peaks.filter((_, index) => (index + addToIndices) % 2 == 0); + + drawBars = (waveCanvasCtx: CanvasRenderingContext2D, width: number, halfH: number, peaks: number[]) => { + // Bar wave draws the bottom only as a reflection of the top, + // so we don't need negative values + const posPeaks = peaks.some(val => val < 0) ? this.posPeaks(peaks, peaks[0] < 0 ? 1 : 0) : peaks; + + // A half-pixel offset makes lines crisp + const $ = 0.5 / this.props.pixelRatio; + const bar = this.props.barWidth * this.props.pixelRatio; + const gap = Math.max(this.props.pixelRatio, 2); + + const max = Math.max(...posPeaks); + const scale = posPeaks.length / width; + + for (let i = 0; i < width; i += bar + gap) { + if (i > width * this.props.progress) waveCanvasCtx.fillStyle = this.props.color; + + const h = Math.round((posPeaks[Math.floor(i * scale)] / max) * halfH) || 1; + + waveCanvasCtx.fillRect(i + $, halfH - h, bar + $, h * 2); + } + }; + + addNegPeaks = (peaks: number[]) => + peaks.reduce((reflectedPeaks, peak) => reflectedPeaks.push(peak, -peak) ? reflectedPeaks:[], + [] as number[]); // prettier-ignore + + drawWaves = (waveCanvasCtx: CanvasRenderingContext2D, width: number, halfH: number, peaks: number[]) => { + const allPeaks = peaks.some(val => val < 0) ? peaks : this.addNegPeaks(peaks); // add negative peaks to arrays without negative peaks + + // A half-pixel offset makes lines crisp + const $ = 0.5 / this.props.pixelRatio; + const length = ~~(allPeaks.length / 2); // ~~ is Math.floor for positive numbers. + + const scale = width / length; + const absmax = Math.max(...allPeaks.map(peak => Math.abs(peak))); + + waveCanvasCtx.beginPath(); + waveCanvasCtx.moveTo($, halfH); + + for (var i = 0; i < length; i++) { + var h = Math.round((allPeaks[2 * i] / absmax) * halfH); + waveCanvasCtx.lineTo(i * scale + $, halfH - h); + } + + // Draw the bottom edge going backwards, to make a single closed hull to fill. + for (var i = length - 1; i >= 0; i--) { + var h = Math.round((allPeaks[2 * i + 1] / absmax) * halfH); + waveCanvasCtx.lineTo(i * scale + $, halfH - h); + } + + waveCanvasCtx.fill(); + + // Always draw a median line + waveCanvasCtx.fillRect(0, halfH - $, width, $); + }; + + updateSize = (width: number, height: number, peaks: number[], waveCanvasCtx: CanvasRenderingContext2D) => { + const displayWidth = Math.round(width / this.props.pixelRatio); + const displayHeight = Math.round(height / this.props.pixelRatio); + waveCanvasCtx.canvas.width = width; + waveCanvasCtx.canvas.height = height; + waveCanvasCtx.canvas.style.width = `${displayWidth}px`; + waveCanvasCtx.canvas.style.height = `${displayHeight}px`; + + waveCanvasCtx.clearRect(0, 0, width, height); + + const gradient = this.props.gradientColors && waveCanvasCtx.createLinearGradient(0, 0, width, 0); + gradient && this.props.gradientColors?.forEach(color => gradient.addColorStop(color.stopPosition, color.color)); + waveCanvasCtx.fillStyle = gradient ?? this.props.progressColor; + + const waveDrawer = this.props.barWidth ? this.drawBars : this.drawWaves; + waveDrawer(waveCanvasCtx, width, height / 2, peaks); + }; + + render() { + return this.props.peaks ? ( + <div style={{ position: 'relative', width: '100%', height: '100%', cursor: 'pointer' }}> + <canvas ref={instance => (ctx => ctx && this.updateSize(this.props.width, this.props.height, this.props.peaks, ctx))(instance?.getContext('2d'))} /> + </div> + ) : null; + } +} diff --git a/src/client/views/nodes/formattedText/DashDocView.tsx b/src/client/views/nodes/formattedText/DashDocView.tsx index 4384c8958..6332b200d 100644 --- a/src/client/views/nodes/formattedText/DashDocView.tsx +++ b/src/client/views/nodes/formattedText/DashDocView.tsx @@ -82,7 +82,7 @@ export class DashDocViewInternal extends React.Component<IDashDocViewInternal> { _spanRef = React.createRef<HTMLDivElement>(); _disposers: { [name: string]: IReactionDisposer } = {}; _textBox: FormattedTextBox; - @observable _dashDoc: Doc | undefined; + @observable _dashDoc: Doc | undefined = undefined; @observable _finalLayout: any; @observable _width: number = 0; @observable _height: number = 0; diff --git a/src/client/views/nodes/formattedText/DashFieldView.tsx b/src/client/views/nodes/formattedText/DashFieldView.tsx index a395296d0..19e14d5a7 100644 --- a/src/client/views/nodes/formattedText/DashFieldView.tsx +++ b/src/client/views/nodes/formattedText/DashFieldView.tsx @@ -25,7 +25,7 @@ export class DashFieldView { node: any; tbox: FormattedTextBox; - unclickable = () => !this.tbox.props.isSelected() && this.node.marks.some((m: any) => m.type === this.tbox.EditorView?.state.schema.marks.linkAnchor && m.attrs.noPreview); + unclickable = () => !this.tbox._props.isSelected() && this.node.marks.some((m: any) => m.type === this.tbox.EditorView?.state.schema.marks.linkAnchor && m.attrs.noPreview); constructor(node: any, view: any, getPos: any, tbox: FormattedTextBox) { this.node = node; this.tbox = tbox; @@ -97,13 +97,13 @@ export class DashFieldViewInternal extends React.Component<IDashFieldViewInterna _textBoxDoc: Doc; _fieldKey: string; _fieldStringRef = React.createRef<HTMLSpanElement>(); - @observable _dashDoc: Doc | undefined; + @observable _dashDoc: Doc | undefined = undefined; @observable _expanded = false; constructor(props: IDashFieldViewInternal) { super(props); this._fieldKey = this.props.fieldKey; - this._textBoxDoc = this.props.tbox.props.Document; + this._textBoxDoc = this.props.tbox.Document; if (this.props.docId) { DocServer.GetRefField(this.props.docId).then(action(dashDoc => dashDoc instanceof Doc && (this._dashDoc = dashDoc))); @@ -126,7 +126,7 @@ export class DashFieldViewInternal extends React.Component<IDashFieldViewInterna deselectCell={emptyFunction} selectCell={emptyFunction} maxWidth={this.props.hideKey ? undefined : this.return100} - columnWidth={this.props.hideKey ? () => this.props.tbox.props.PanelWidth() - 20 : returnZero} + columnWidth={this.props.hideKey ? () => this.props.tbox._props.PanelWidth() - 20 : returnZero} selectedCell={() => [this._dashDoc!, 0]} fieldKey={this._fieldKey} rowHeight={returnZero} @@ -145,7 +145,7 @@ export class DashFieldViewInternal extends React.Component<IDashFieldViewInterna } createPivotForField = (e: React.MouseEvent) => { - let container = this.props.tbox.props.DocumentView?.().props.docViewPath().lastElement(); + let container = this.props.tbox._props.DocumentView?.()._props.docViewPath().lastElement(); if (container) { const embedding = Doc.MakeEmbedding(container.Document); embedding._type_collection = CollectionViewType.Time; @@ -157,7 +157,7 @@ export class DashFieldViewInternal extends React.Component<IDashFieldViewInterna list.map(c => c.heading).indexOf(this._fieldKey) === -1 && list.push(new SchemaHeaderField(this._fieldKey, '#f1efeb')); list.map(c => c.heading).indexOf('text') === -1 && list.push(new SchemaHeaderField('text', '#f1efeb')); embedding._pivotField = this._fieldKey.startsWith('#') ? 'tags' : this._fieldKey; - this.props.tbox.props.addDocTab(embedding, OpenWhere.addRight); + this.props.tbox._props.addDocTab(embedding, OpenWhere.addRight); } }; @@ -177,7 +177,7 @@ export class DashFieldViewInternal extends React.Component<IDashFieldViewInterna style={{ width: this.props.width, height: this.props.height, - pointerEvents: this.props.tbox.props.isSelected() || this.props.tbox.isAnyChildContentActive?.() ? undefined : 'none', + pointerEvents: this.props.tbox._props.isSelected() || this.props.tbox.isAnyChildContentActive?.() ? undefined : 'none', }}> {this.props.hideKey ? null : ( <span className="dashFieldView-labelSpan" title="click to see related tags" onPointerDown={this.onPointerDownLabelSpan}> diff --git a/src/client/views/nodes/formattedText/EquationEditor.scss b/src/client/views/nodes/formattedText/EquationEditor.scss new file mode 100644 index 000000000..b0c17e56e --- /dev/null +++ b/src/client/views/nodes/formattedText/EquationEditor.scss @@ -0,0 +1,468 @@ +// using this import, we get runtime errors when trying to load the specified font-faces +// so we copy the .css and remove the @font-face imports + +// @import 'mathquill/build/mathquill.css' +/* + * MathQuill v0.10.1 http://mathquill.com + * by Han, Jeanine, and Mary maintainers@mathquill.com + * + * This Source Code Form is subject to the terms of the + * Mozilla Public License, v. 2.0. If a copy of the MPL + * was not distributed with this file, You can obtain + * one at http://mozilla.org/MPL/2.0/. + */ +// @font-face { +// font-family: Symbola; +// src: url(font/Symbola.eot); +// src: +// local('Symbola Regular'), +// local('Symbola'), +// url(font/Symbola.woff2) format('woff2'), +// url(font/Symbola.woff) format('woff'), +// url(font/Symbola.ttf) format('truetype'), +// url(font/Symbola.otf) format('opentype'), +// url(font/Symbola.svg#Symbola) format('svg'); +// } +.mq-editable-field { + display: -moz-inline-box; + display: inline-block; +} +.mq-editable-field .mq-cursor { + border-left: 1px solid black; + margin-left: -1px; + position: relative; + z-index: 1; + padding: 0; + display: -moz-inline-box; + display: inline-block; +} +.mq-editable-field .mq-cursor.mq-blink { + visibility: hidden; +} +.mq-editable-field, +.mq-math-mode .mq-editable-field { + border: 1px solid gray; +} +.mq-editable-field.mq-focused, +.mq-math-mode .mq-editable-field.mq-focused { + -webkit-box-shadow: + #8bd 0 0 1px 2px, + inset #6ae 0 0 2px 0; + -moz-box-shadow: + #8bd 0 0 1px 2px, + inset #6ae 0 0 2px 0; + box-shadow: + #8bd 0 0 1px 2px, + inset #6ae 0 0 2px 0; + border-color: #709ac0; + border-radius: 1px; +} +.mq-math-mode .mq-editable-field { + margin: 1px; +} +.mq-editable-field .mq-latex-command-input { + color: inherit; + font-family: 'Courier New', monospace; + border: 1px solid gray; + padding-right: 1px; + margin-right: 1px; + margin-left: 2px; +} +.mq-editable-field .mq-latex-command-input.mq-empty { + background: transparent; +} +.mq-editable-field .mq-latex-command-input.mq-hasCursor { + border-color: ActiveBorder; +} +.mq-editable-field.mq-empty:after, +.mq-editable-field.mq-text-mode:after, +.mq-math-mode .mq-empty:after { + visibility: hidden; + content: 'c'; +} +.mq-editable-field .mq-cursor:only-child:after, +.mq-editable-field .mq-textarea + .mq-cursor:last-child:after { + visibility: hidden; + content: 'c'; +} +.mq-editable-field .mq-text-mode .mq-cursor:only-child:after { + content: ''; +} +.mq-editable-field.mq-text-mode { + overflow-x: auto; + overflow-y: hidden; +} +.mq-root-block, +.mq-math-mode .mq-root-block { + display: -moz-inline-box; + display: inline-block; + width: 100%; + padding: 2px; + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; + white-space: nowrap; + overflow: hidden; + vertical-align: middle; +} +.mq-math-mode { + font-variant: normal; + font-weight: normal; + font-style: normal; + font-size: 115%; + line-height: 1; + display: -moz-inline-box; + display: inline-block; +} +.mq-math-mode .mq-non-leaf, +.mq-math-mode .mq-scaled { + display: -moz-inline-box; + display: inline-block; +} +.mq-math-mode var, +.mq-math-mode .mq-text-mode, +.mq-math-mode .mq-nonSymbola { + font-family: 'Times New Roman', Symbola, serif; + line-height: 0.9; +} +.mq-math-mode * { + font-size: inherit; + line-height: inherit; + margin: 0; + padding: 0; + border-color: black; + -webkit-user-select: none; + -moz-user-select: none; + user-select: none; + box-sizing: border-box; +} +.mq-math-mode .mq-empty { + background: #ccc; +} +.mq-math-mode .mq-empty.mq-root-block { + background: transparent; +} +.mq-math-mode.mq-empty { + background: transparent; +} +.mq-math-mode .mq-text-mode { + display: inline-block; +} +.mq-math-mode .mq-text-mode.mq-hasCursor { + box-shadow: inset darkgray 0 0.1em 0.2em; + padding: 0 0.1em; + margin: 0 -0.1em; + min-width: 1ex; +} +.mq-math-mode .mq-font { + font: + 1em 'Times New Roman', + Symbola, + serif; +} +.mq-math-mode .mq-font * { + font-family: inherit; + font-style: inherit; +} +.mq-math-mode b, +.mq-math-mode b.mq-font { + font-weight: bolder; +} +.mq-math-mode var, +.mq-math-mode i, +.mq-math-mode i.mq-font { + font-style: italic; +} +.mq-math-mode var.mq-f { + margin-right: 0.2em; + margin-left: 0.1em; +} +.mq-math-mode .mq-roman var.mq-f { + margin: 0; +} +.mq-math-mode big { + font-size: 200%; +} +.mq-math-mode .mq-int > big { + display: inline-block; + -webkit-transform: scaleX(0.7); + -moz-transform: scaleX(0.7); + -ms-transform: scaleX(0.7); + -o-transform: scaleX(0.7); + transform: scaleX(0.7); + vertical-align: -0.16em; +} +.mq-math-mode .mq-int > .mq-supsub { + font-size: 80%; + vertical-align: -1.1em; + padding-right: 0.2em; +} +.mq-math-mode .mq-int > .mq-supsub > .mq-sup > .mq-sup-inner { + vertical-align: 1.3em; +} +.mq-math-mode .mq-int > .mq-supsub > .mq-sub { + margin-left: -0.35em; +} +.mq-math-mode .mq-roman { + font-style: normal; +} +.mq-math-mode .mq-sans-serif { + font-family: sans-serif, Symbola, serif; +} +.mq-math-mode .mq-monospace { + font-family: monospace, Symbola, serif; +} +.mq-math-mode .mq-overline { + border-top: 1px solid black; + margin-top: 1px; +} +.mq-math-mode .mq-underline { + border-bottom: 1px solid black; + margin-bottom: 1px; +} +.mq-math-mode .mq-binary-operator { + padding: 0 0.2em; + display: -moz-inline-box; + display: inline-block; +} +.mq-math-mode .mq-supsub { + text-align: left; + font-size: 90%; + vertical-align: -0.5em; +} +.mq-math-mode .mq-supsub.mq-sup-only { + vertical-align: 0.5em; +} +.mq-math-mode .mq-supsub.mq-sup-only .mq-sup { + display: inline-block; + vertical-align: text-bottom; +} +.mq-math-mode .mq-supsub .mq-sup { + display: block; +} +.mq-math-mode .mq-supsub .mq-sub { + display: block; + float: left; +} +.mq-math-mode .mq-supsub .mq-binary-operator { + padding: 0 0.1em; +} +.mq-math-mode .mq-supsub .mq-fraction { + font-size: 70%; +} +.mq-math-mode sup.mq-nthroot { + font-size: 80%; + vertical-align: 0.8em; + margin-right: -0.6em; + margin-left: 0.2em; + min-width: 0.5em; +} +.mq-math-mode .mq-paren { + padding: 0 0.1em; + vertical-align: top; + -webkit-transform-origin: center 0.06em; + -moz-transform-origin: center 0.06em; + -ms-transform-origin: center 0.06em; + -o-transform-origin: center 0.06em; + transform-origin: center 0.06em; +} +.mq-math-mode .mq-paren.mq-ghost { + color: silver; +} +.mq-math-mode .mq-paren + span { + margin-top: 0.1em; + margin-bottom: 0.1em; +} +.mq-math-mode .mq-array { + vertical-align: middle; + text-align: center; +} +.mq-math-mode .mq-array > span { + display: block; +} +.mq-math-mode .mq-operator-name { + font-family: Symbola, 'Times New Roman', serif; + line-height: 0.9; + font-style: normal; +} +.mq-math-mode var.mq-operator-name.mq-first { + padding-left: 0.2em; +} +.mq-math-mode var.mq-operator-name.mq-last, +.mq-math-mode .mq-supsub.mq-after-operator-name { + padding-right: 0.2em; +} +.mq-math-mode .mq-fraction { + font-size: 90%; + text-align: center; + vertical-align: -0.4em; + padding: 0 0.2em; +} +.mq-math-mode .mq-fraction, +.mq-math-mode .mq-large-operator, +.mq-math-mode x:-moz-any-link { + display: -moz-groupbox; +} +.mq-math-mode .mq-fraction, +.mq-math-mode .mq-large-operator, +.mq-math-mode x:-moz-any-link, +.mq-math-mode x:default { + display: inline-block; +} +.mq-math-mode .mq-numerator, +.mq-math-mode .mq-denominator { + display: block; +} +.mq-math-mode .mq-numerator { + padding: 0 0.1em; +} +.mq-math-mode .mq-denominator { + border-top: 1px solid; + float: right; + width: 100%; + padding: 0.1em; +} +.mq-math-mode .mq-sqrt-prefix { + padding-top: 0; + position: relative; + top: 0.1em; + vertical-align: top; + -webkit-transform-origin: top; + -moz-transform-origin: top; + -ms-transform-origin: top; + -o-transform-origin: top; + transform-origin: top; +} +.mq-math-mode .mq-sqrt-stem { + border-top: 1px solid; + margin-top: 1px; + padding-left: 0.15em; + padding-right: 0.2em; + margin-right: 0.1em; + padding-top: 1px; +} +.mq-math-mode .mq-vector-prefix { + display: block; + text-align: center; + line-height: 0.25em; + margin-bottom: -0.1em; + font-size: 0.75em; +} +.mq-math-mode .mq-vector-stem { + display: block; +} +.mq-math-mode .mq-large-operator { + vertical-align: -0.2em; + padding: 0.2em; + text-align: center; +} +.mq-math-mode .mq-large-operator .mq-from, +.mq-math-mode .mq-large-operator big, +.mq-math-mode .mq-large-operator .mq-to { + display: block; +} +.mq-math-mode .mq-large-operator .mq-from, +.mq-math-mode .mq-large-operator .mq-to { + font-size: 80%; +} +.mq-math-mode .mq-large-operator .mq-from { + float: right; + /* take out of normal flow to manipulate baseline */ + width: 100%; +} +.mq-math-mode, +.mq-math-mode .mq-editable-field { + cursor: text; + font-family: Symbola, 'Times New Roman', serif; +} +.mq-math-mode .mq-overarrow { + border-top: 1px solid black; + margin-top: 1px; + padding-top: 0.2em; +} +.mq-math-mode .mq-overarrow:before { + display: block; + position: relative; + top: -0.34em; + font-size: 0.5em; + line-height: 0em; + content: '\27A4'; + text-align: right; +} +.mq-math-mode .mq-overarrow.mq-arrow-left:before { + -moz-transform: scaleX(-1); + -o-transform: scaleX(-1); + -webkit-transform: scaleX(-1); + transform: scaleX(-1); + filter: FlipH; + -ms-filter: 'FlipH'; +} +.mq-math-mode .mq-selection, +.mq-editable-field .mq-selection, +.mq-math-mode .mq-selection .mq-non-leaf, +.mq-editable-field .mq-selection .mq-non-leaf, +.mq-math-mode .mq-selection .mq-scaled, +.mq-editable-field .mq-selection .mq-scaled { + background: #b4d5fe !important; + background: Highlight !important; + color: HighlightText; + border-color: HighlightText; +} +.mq-math-mode .mq-selection .mq-matrixed, +.mq-editable-field .mq-selection .mq-matrixed { + background: #39f !important; +} +.mq-math-mode .mq-selection .mq-matrixed-container, +.mq-editable-field .mq-selection .mq-matrixed-container { + filter: progid:DXImageTransform.Microsoft.Chroma(color='#3399FF') !important; +} +.mq-math-mode .mq-selection.mq-blur, +.mq-editable-field .mq-selection.mq-blur, +.mq-math-mode .mq-selection.mq-blur .mq-non-leaf, +.mq-editable-field .mq-selection.mq-blur .mq-non-leaf, +.mq-math-mode .mq-selection.mq-blur .mq-scaled, +.mq-editable-field .mq-selection.mq-blur .mq-scaled, +.mq-math-mode .mq-selection.mq-blur .mq-matrixed, +.mq-editable-field .mq-selection.mq-blur .mq-matrixed { + background: #d4d4d4 !important; + color: black; + border-color: black; +} +.mq-math-mode .mq-selection.mq-blur .mq-matrixed-container, +.mq-editable-field .mq-selection.mq-blur .mq-matrixed-container { + filter: progid:DXImageTransform.Microsoft.Chroma(color='#D4D4D4') !important; +} +.mq-editable-field .mq-textarea, +.mq-math-mode .mq-textarea { + position: relative; + -webkit-user-select: text; + -moz-user-select: text; + user-select: text; +} +.mq-editable-field .mq-textarea *, +.mq-math-mode .mq-textarea *, +.mq-editable-field .mq-selectable, +.mq-math-mode .mq-selectable { + -webkit-user-select: text; + -moz-user-select: text; + user-select: text; + position: absolute; + clip: rect(1em 1em 1em 1em); + -webkit-transform: scale(0); + -moz-transform: scale(0); + -ms-transform: scale(0); + -o-transform: scale(0); + transform: scale(0); + resize: none; + width: 1px; + height: 1px; +} +.mq-math-mode .mq-matrixed { + background: white; + display: -moz-inline-box; + display: inline-block; +} +.mq-math-mode .mq-matrixed-container { + filter: progid:DXImageTransform.Microsoft.Chroma(color='white'); + margin-top: -0.1em; +} diff --git a/src/client/views/nodes/formattedText/EquationEditor.tsx b/src/client/views/nodes/formattedText/EquationEditor.tsx index bde6c1315..07c70af77 100644 --- a/src/client/views/nodes/formattedText/EquationEditor.tsx +++ b/src/client/views/nodes/formattedText/EquationEditor.tsx @@ -3,8 +3,7 @@ import React, { Component, createRef } from 'react'; // Import JQuery, required for the functioning of the equation editor import $ from 'jquery'; -// Import the styles from the Mathquill editor -import 'mathquill/build/mathquill.css'; +import './EquationEditor.scss'; // eslint-disable-next-line @typescript-eslint/ban-ts-ignore // @ts-ignore @@ -14,6 +13,8 @@ window.jQuery = $; // @ts-ignore require('mathquill/build/mathquill'); +(window as any).MathQuill = (window as any).MathQuill.getInterface(1); + type EquationEditorProps = { onChange(latex: string): void; value: string; @@ -74,8 +75,7 @@ class EquationEditor extends Component<EquationEditorProps> { autoOperatorNames, }; - // @ts-ignore - this.mathField = (MathQuill as any).MathField(this.element.current, config); + this.mathField = (window as any).MathQuill.MathField(this.element.current, config); this.mathField.latex(value || ''); } diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx index 4f8e8769a..244de7849 100644 --- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx +++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx @@ -2,7 +2,7 @@ import { IconProp } from '@fortawesome/fontawesome-svg-core'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { Tooltip } from '@mui/material'; import { isEqual } from 'lodash'; -import { action, computed, IReactionDisposer, observable, ObservableSet, reaction, runInAction } from 'mobx'; +import { action, computed, IReactionDisposer, makeObservable, observable, ObservableSet, override, reaction, runInAction } from 'mobx'; import { observer } from 'mobx-react'; import { baseKeymap, selectAll } from 'prosemirror-commands'; import { history } from 'prosemirror-history'; @@ -24,7 +24,7 @@ import { RichTextUtils } from '../../../../fields/RichTextUtils'; import { ComputedField } from '../../../../fields/ScriptField'; import { BoolCast, Cast, DocCast, FieldValue, NumCast, ScriptCast, StrCast } from '../../../../fields/Types'; import { GetEffectiveAcl, TraceMobx } from '../../../../fields/util'; -import { addStyleSheet, addStyleSheetRule, clearStyleSheetRules, emptyFunction, numberRange, returnFalse, returnZero, setupMoveUpEvents, smoothScroll, unimplementedFunction, Utils } from '../../../../Utils'; +import { addStyleSheet, addStyleSheetRule, clearStyleSheetRules, copyProps, emptyFunction, numberRange, returnFalse, returnZero, setupMoveUpEvents, smoothScroll, unimplementedFunction, Utils } from '../../../../Utils'; import { GoogleApiClientUtils, Pulls, Pushes } from '../../../apis/google_docs/GoogleApiClientUtils'; import { gptAPICall, GPTCallType } from '../../../apis/gpt/GPT'; import { DocServer } from '../../../DocServer'; @@ -129,7 +129,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps } @computed get noSidebar() { - return this.props.docViewPath().lastElement()?.props.hideDecorationTitle || this.props.noSidebar || this.Document._layout_noSidebar; + return this._props.docViewPath().lastElement()?._props.hideDecorationTitle || this._props.noSidebar || this.Document._layout_noSidebar; } @computed get layout_sidebarWidthPercent() { return this._showSidebar ? '20%' : StrCast(this.layoutDoc._layout_sidebarWidthPercent, '0%'); @@ -138,7 +138,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps return StrCast(this.layoutDoc.sidebar_color, StrCast(this.layoutDoc[this.fieldKey + '_backgroundColor'], '#e4e4e4')); } @computed get layout_autoHeight() { - return (this.props.forceAutoHeight || this.layoutDoc._layout_autoHeight) && !this.props.ignoreAutoHeight; + return (this._props.forceAutoHeight || this.layoutDoc._layout_autoHeight) && !this._props.ignoreAutoHeight; } @computed get textHeight() { return NumCast(this.dataDoc[this.fieldKey + '_height']); @@ -150,7 +150,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps return !this.sidebarWidth() ? 0 : NumCast(this.dataDoc[this.SidebarKey + '_height']); } @computed get titleHeight() { - return this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.HeaderMargin) || 0; + return this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.HeaderMargin) || 0; } @computed get layout_autoHeightMargins() { return this.titleHeight + NumCast(this.layoutDoc._layout_autoHeightMargins); @@ -162,7 +162,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps !this.dataDoc[`${this.fieldKey}_recordingSource`] && (this.dataDoc.mediaState = value ? media_state.Recording : undefined); } @computed get config() { - this._keymap = buildKeymap(schema, this.props); + this._keymap = buildKeymap(schema, this._props); this._rules = new RichTextRules(this.Document, this); return { schema, @@ -208,12 +208,20 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps return url.startsWith(document.location.origin) ? new URL(url).pathname.split('doc/').lastElement() : ''; // docId } - constructor(props: any) { + _prevProps: React.PropsWithChildren<FieldViewProps & FormattedTextBoxProps>; + @override _props: React.PropsWithChildren<FieldViewProps & FormattedTextBoxProps>; + constructor(props: React.PropsWithChildren<FieldViewProps & FormattedTextBoxProps>) { super(props); + this._props = this._prevProps = props; + makeObservable(this); FormattedTextBox.Instance = this; this._recordingStart = Date.now(); } + componentDidUpdate() { + copyProps(this); + } + // removes all hyperlink anchors for the removed linkDoc // TODO: bcz: Argh... if a section of text has multiple anchors, this should just remove the intended one. // but since removing one anchor from the list of attr anchors isn't implemented, this will end up removing nothing. @@ -315,10 +323,10 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps return target; }; - DragManager.StartAnchorAnnoDrag([ele], new DragManager.AnchorAnnoDragData(this.props.docViewPath().lastElement(), () => this.getAnchor(true), targetCreator), e.pageX, e.pageY); + DragManager.StartAnchorAnnoDrag([ele], new DragManager.AnchorAnnoDragData(this._props.docViewPath().lastElement(), () => this.getAnchor(true), targetCreator), e.pageX, e.pageY); }); const coordsB = this._editorView!.coordsAtPos(this._editorView!.state.selection.to); - this.props.rootSelected?.() && AnchorMenu.Instance.jumpTo(coordsB.left, coordsB.bottom); + this._props.rootSelected?.() && AnchorMenu.Instance.jumpTo(coordsB.left, coordsB.bottom); let ele: Opt<HTMLDivElement> = undefined; try { const contents = window.getSelection()?.getRangeAt(0).cloneContents(); @@ -361,7 +369,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps textChange && (dataDoc[this.fieldKey + '_modificationDate'] = new DateField(new Date(Date.now()))); if ((!prevData && !protoData) || newText || (!newText && !templateData)) { // if no template, or there's text that didn't come from the layout template, write it to the document. (if this is driven by a template, then this overwrites the template text which is intended) - if ((this._finishingLink || this.props.isContentActive()) && removeSelection(newJson) !== removeSelection(prevData?.Data)) { + if ((this._finishingLink || this._props.isContentActive()) && removeSelection(newJson) !== removeSelection(prevData?.Data)) { const numstring = NumCast(dataDoc[this.fieldKey], null); dataDoc[this.fieldKey] = numstring !== undefined ? Number(newText) : new RichTextField(newJson, newText); dataDoc[this.fieldKey + '_noTemplate'] = true; // mark the data field as being split from the template if it has been edited @@ -390,7 +398,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps this._editorView.updateState(EditorState.fromJSON(this.config, json)); } } - if (window.getSelection()?.isCollapsed && this.props.rootSelected?.()) { + if (window.getSelection()?.isCollapsed && this._props.rootSelected?.()) { AnchorMenu.Instance.fadeOut(true); } } @@ -438,7 +446,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps autoLink = () => { const newAutoLinks = new Set<Doc>(); - const oldAutoLinks = LinkManager.Links(this.props.Document).filter(link => link.link_relationship === LinkManager.AutoKeywords); + const oldAutoLinks = LinkManager.Links(this._props.Document).filter(link => link.link_relationship === LinkManager.AutoKeywords); if (this._editorView?.state.doc.textContent) { const isNodeSel = this._editorView.state.selection instanceof NodeSelection; const f = this._editorView.state.selection.from; @@ -456,7 +464,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps updateTitle = () => { const title = StrCast(this.dataDoc.title, Cast(this.dataDoc.title, RichTextField, null)?.Text); if ( - !this.props.dontRegisterView && // (this.props.Document.isTemplateForField === "text" || !this.props.Document.isTemplateForField) && // only update the title if the data document's data field is changing + !this._props.dontRegisterView && // (this._props.Document.isTemplateForField === "text" || !this._props.Document.isTemplateForField) && // only update the title if the data document's data field is changing (title.startsWith('-') || title.startsWith('@')) && this._editorView && !this.dataDoc.title_custom && @@ -499,7 +507,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps DocUtils.MakeLink(this.Document, target, { link_relationship: LinkManager.AutoKeywords })!); newAutoLinks.add(alink); // DocCast(alink.link_anchor_1).followLinkLocation = 'add:right'; - const allAnchors = [{ href: Doc.localServerPath(target), title: 'a link', anchorId: this.props.Document[Id] }]; + const allAnchors = [{ href: Doc.localServerPath(target), title: 'a link', anchorId: this._props.Document[Id] }]; allAnchors.push(...(node.marks.find((m: Mark) => m.type.name === schema.marks.autoLinkAnchor.name)?.attrs.allAnchors ?? [])); const link = editorView.state.schema.marks.autoLinkAnchor.create({ allAnchors, title: 'auto term' }); tr = tr.addMark(pos, pos + node.nodeSize, link); @@ -568,7 +576,6 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps }; @undoBatch - @action drop = (e: Event, de: DragManager.DropEvent) => { if (de.complete.annoDragData) { de.complete.annoDragData.dropDocCreator = () => this.getAnchor(true); @@ -584,7 +591,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps // replace text contents when dragging with Alt if (de.altKey) { const fieldKey = Doc.LayoutFieldKey(draggedDoc); - if (draggedDoc[fieldKey] instanceof RichTextField && !Doc.AreProtosEqual(draggedDoc, this.props.Document)) { + if (draggedDoc[fieldKey] instanceof RichTextField && !Doc.AreProtosEqual(draggedDoc, this._props.Document)) { Doc.GetProto(this.dataDoc)[this.fieldKey] = Field.Copy(draggedDoc[fieldKey]); } @@ -742,9 +749,9 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps ); }; sidebarMove = (e: PointerEvent, down: number[], delta: number[]) => { - const localDelta = this.props + const localDelta = this._props .ScreenToLocalTransform() - .scale(this.props.NativeDimScaling?.() || 1) + .scale(this._props.NativeDimScaling?.() || 1) .transformDirection(delta[0], delta[1]); const sidebarWidth = (NumCast(this.layoutDoc._width) * Number(this.layout_sidebarWidthPercent.replace('%', ''))) / 100; const width = NumCast(this.layoutDoc._width) + localDelta[0]; @@ -758,15 +765,15 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps deleteAnnotation = (anchor: Doc) => { const batch = UndoManager.StartBatch('delete link'); LinkManager.Instance.deleteLink(LinkManager.Links(anchor)[0]); - // const docAnnotations = DocListCast(this.props.dataDoc[this.fieldKey]); - // this.props.dataDoc[this.fieldKey] = new List<Doc>(docAnnotations.filter(a => a !== this.annoTextRegion)); + // const docAnnotations = DocListCast(this._props.dataDoc[this.fieldKey]); + // this._props.dataDoc[this.fieldKey] = new List<Doc>(docAnnotations.filter(a => a !== this.annoTextRegion)); // AnchorMenu.Instance.fadeOut(true); - this.props.select(false); + this._props.select(false); setTimeout(batch.end); // wait for reaction to remove link from document }; @undoBatch - pinToPres = (anchor: Doc) => this.props.pinToPres(anchor, {}); + pinToPres = (anchor: Doc) => this._props.pinToPres(anchor, {}); @undoBatch makeTargetToggle = (anchor: Doc) => (anchor.followLinkToggle = !anchor.followLinkToggle); @@ -776,7 +783,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps const trail = DocCast(anchor.presentationTrail); if (trail) { Doc.ActivePresentation = trail; - this.props.addDocTab(trail, OpenWhere.replaceRight); + this._props.addDocTab(trail, OpenWhere.replaceRight); } }; @@ -913,7 +920,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps const optionItems = options && 'subitems' in options ? options.subitems : []; optionItems.push({ description: `Generate Dall-E Image`, event: () => this.generateImage(), icon: 'star' }); optionItems.push({ description: `Ask GPT-3`, event: () => this.askGPT(), icon: 'lightbulb' }); - this.props.renderDepth && + this._props.renderDepth && optionItems.push({ description: !this.Document._createDocOnCR ? 'Create New Doc on Carriage Return' : 'Allow Carriage Returns', event: () => (this.layoutDoc._createDocOnCR = !this.layoutDoc._createDocOnCR), @@ -958,7 +965,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps generateImage = async () => { GPTPopup.Instance?.setTextAnchor(this.getAnchor(false)); GPTPopup.Instance?.setImgTargetDoc(this.Document); - GPTPopup.Instance.addToCollection = this.props.addDocument; + GPTPopup.Instance.addToCollection = this._props.addDocument; GPTPopup.Instance.setImgDesc((this.dataDoc.text as RichTextField)?.Text); GPTPopup.Instance.generateImage(); }; @@ -1127,7 +1134,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps setTimeout(() => clearStyleSheetRules(FormattedTextBox._highlightStyleSheet), Math.max(this._focusSpeed || 0, 3000)); return focusSpeed; } else { - return this.props.focus(this.Document, options); + return this._props.focus(this.Document, options); } } }; @@ -1141,10 +1148,10 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps }; @computed get contentScaling() { - return Doc.NativeAspect(this.Document, this.dataDoc, false) ? this.props.NativeDimScaling?.() || 1 : 1; + return Doc.NativeAspect(this.Document, this.dataDoc, false) ? this._props.NativeDimScaling?.() || 1 : 1; } componentDidMount() { - !this.props.dontSelectOnLoad && this.props.setContentView?.(this); // this tells the DocumentView that this AudioBox is the "content" of the document. this allows the DocumentView to indirectly call getAnchor() on the AudioBox when making a link. + !this._props.dontSelectOnLoad && this._props.setContentView?.(this); // this tells the DocumentView that this AudioBox is the "content" of the document. this allows the DocumentView to indirectly call getAnchor() on the AudioBox when making a link. this._cachedLinks = LinkManager.Links(this.Document); this._disposers.breakupDictation = reaction(() => Doc.RecordingEvent, this.breakupDictation); this._disposers.layout_autoHeight = reaction( @@ -1157,7 +1164,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps { fireImmediately: true } ); this._disposers.width = reaction( - () => this.props.PanelWidth(), + () => this._props.PanelWidth(), width => this.tryUpdateScrollHeight() ); this._disposers.scrollHeight = reaction( @@ -1171,13 +1178,13 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps ({ sidebarHeight, textHeight, layout_autoHeight, marginsHeight }) => { const newHeight = this.contentScaling * (marginsHeight + Math.max(sidebarHeight, textHeight)); if ( - (!Array.from(FormattedTextBox._globalHighlights).includes('Bold Text') || this.props.isSelected()) && // + (!Array.from(FormattedTextBox._globalHighlights).includes('Bold Text') || this._props.isSelected()) && // layout_autoHeight && newHeight && newHeight !== this.layoutDoc.height && - !this.props.dontRegisterView + !this._props.dontRegisterView ) { - this.props.setHeight?.(newHeight); + this._props.setHeight?.(newHeight); } }, { fireImmediately: !Array.from(FormattedTextBox._globalHighlights).includes('Bold Text') } @@ -1219,7 +1226,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps } ); this._disposers.pullDoc = reaction( - () => this.props.Document[Pulls], + () => this._props.Document[Pulls], () => { if (!DocumentButtonBar.hasPulledHack) { DocumentButtonBar.hasPulledHack = true; @@ -1228,7 +1235,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps } ); this._disposers.pushDoc = reaction( - () => this.props.Document[Pushes], + () => this._props.Document[Pushes], () => { if (!DocumentButtonBar.hasPushedHack) { DocumentButtonBar.hasPushedHack = true; @@ -1244,7 +1251,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps ); this._disposers.selected = reaction( - () => this.props.rootSelected?.(), + () => this._props.rootSelected?.(), action(selected => { //selected && setTimeout(() => this.prepareForTyping()); if (FormattedTextBox._globalHighlights.has('Bold Text')) { @@ -1254,14 +1261,14 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps RichTextMenu.Instance?.updateMenu(undefined, undefined, undefined, undefined); } if (this._editorView && selected) { - RichTextMenu.Instance?.updateMenu(this._editorView, undefined, this.props, this.layoutDoc); + RichTextMenu.Instance?.updateMenu(this._editorView, undefined, this._props, this.layoutDoc); setTimeout(this.autoLink, 20); } }), { fireImmediately: true } ); - if (!this.props.dontRegisterView) { + if (!this._props.dontRegisterView) { this._disposers.record = reaction( () => this._recordingDictation, () => { @@ -1276,7 +1283,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps this._disposers.scroll = reaction( () => NumCast(this.layoutDoc._layout_scrollTop), pos => { - if (!this._ignoreScroll && this._scrollRef.current && !this.props.dontSelectOnLoad) { + if (!this._ignoreScroll && this._scrollRef.current && !this._props.dontSelectOnLoad) { const viewTrans = quickScroll ?? StrCast(this.Document._viewTransition); const durationMiliStr = viewTrans.match(/([0-9]*)ms/); const durationSecStr = viewTrans.match(/([0-9.]*)s/); @@ -1439,8 +1446,8 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps const self = this; return new Plugin({ view(newView) { - runInAction(() => self.props.rootSelected?.() && RichTextMenu.Instance && (RichTextMenu.Instance.view = newView)); - return new RichTextMenuPlugin({ editorProps: this.props }); + runInAction(() => self._props.rootSelected?.() && RichTextMenu.Instance && (RichTextMenu.Instance.view = newView)); + return new RichTextMenuPlugin({ editorProps: this._props }); }, }); } @@ -1462,7 +1469,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps const botOff = docPos.bottom > viewRect.bottom ? docPos.bottom - viewRect.bottom : undefined; if (((topOff && Math.abs(Math.trunc(topOff)) > 0) || (botOff && Math.abs(Math.trunc(botOff)) > 0)) && scrollRef) { const shift = Math.min(topOff ?? Number.MAX_VALUE, botOff ?? Number.MAX_VALUE); - const scrollPos = scrollRef.scrollTop + shift * self.props.ScreenToLocalTransform().Scale; + const scrollPos = scrollRef.scrollTop + shift * self._props.ScreenToLocalTransform().Scale; if (this._focusSpeed !== undefined) { scrollPos && (this._scrollStopper = smoothScroll(this._focusSpeed, scrollRef, scrollPos, 'ease', this._scrollStopper)); } else { @@ -1514,11 +1521,11 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps (this._editorView as any).TextView = this; } - const selectOnLoad = Doc.AreProtosEqual(this.props.TemplateDataDocument ?? this.Document, FormattedTextBox.SelectOnLoad) && (!LightboxView.LightboxDoc || LightboxView.IsLightboxDocView(this.props.docViewPath())); - if (this._editorView && selectOnLoad && !this.props.dontRegisterView && !this.props.dontSelectOnLoad && this.isActiveTab(this.ProseRef)) { + const selectOnLoad = Doc.AreProtosEqual(this._props.TemplateDataDocument ?? this.Document, FormattedTextBox.SelectOnLoad) && (!LightboxView.LightboxDoc || LightboxView.IsLightboxDocView(this._props.docViewPath())); + if (this._editorView && selectOnLoad && !this._props.dontRegisterView && !this._props.dontSelectOnLoad && this.isActiveTab(this.ProseRef)) { const selLoadChar = FormattedTextBox.SelectOnLoadChar; FormattedTextBox.SelectOnLoad = undefined; - this.props.select(false); + this._props.select(false); if (selLoadChar) { const $from = this._editorView.state.selection.anchor ? this._editorView.state.doc.resolve(this._editorView.state.selection.anchor - 1) : undefined; const mark = schema.marks.user_mark.create({ userid: Doc.CurrentUserEmail, modified: Math.floor(Date.now() / 1000) }); @@ -1534,7 +1541,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps } } selectOnLoad && this._editorView!.focus(); - if (this.props.isContentActive()) this.prepareForTyping(); + if (this._props.isContentActive()) this.prepareForTyping(); if (this._editorView) { const tr = this._editorView.state.tr; const { from, to } = tr.selection; @@ -1557,15 +1564,14 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps ...(Doc.UserDoc().fontColor !== 'transparent' && Doc.UserDoc().fontColor ? [schema.mark(schema.marks.pFontColor, { color: StrCast(Doc.UserDoc().fontColor) })] : []), ...(Doc.UserDoc().fontStyle === 'italics' ? [schema.mark(schema.marks.em)] : []), ...(Doc.UserDoc().textDecoration === 'underline' ? [schema.mark(schema.marks.underline)] : []), - ...(Doc.UserDoc().fontFamily ? [schema.mark(schema.marks.pFontFamily, { family: this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.FontFamily) })] : []), - ...(Doc.UserDoc().fontSize ? [schema.mark(schema.marks.pFontSize, { fontSize: this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.FontSize) })] : []), + ...(Doc.UserDoc().fontFamily ? [schema.mark(schema.marks.pFontFamily, { family: this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.FontFamily) })] : []), + ...(Doc.UserDoc().fontSize ? [schema.mark(schema.marks.pFontSize, { fontSize: this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.FontSize) })] : []), ...(Doc.UserDoc().fontWeight === 'bold' ? [schema.mark(schema.marks.strong)] : []), ...[schema.marks.user_mark.create({ userid: Doc.CurrentUserEmail, modified: Math.floor(Date.now() / 1000) })], ]; this._editorView?.dispatch(this._editorView?.state.tr.setStoredMarks(docDefaultMarks)); }; - @action componentWillUnmount() { if (this._recordingDictation) { this._recordingDictation = !this._recordingDictation; @@ -1598,7 +1604,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps const func = () => { const docView = DocumentManager.Instance.getDocumentView(audiodoc); if (!docView) { - this.props.addDocTab(audiodoc, OpenWhere.addBottom); + this._props.addDocTab(audiodoc, OpenWhere.addBottom); setTimeout(func); } else docView.ComponentView?.playFrom?.(timecode, Cast(anchor.timecodeToHide, 'number', null)); // bcz: would be nice to find the next audio tag in the doc and play until that }; @@ -1614,7 +1620,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps this._downTime = Date.now(); this._hadDownFocus = this.ProseRef?.children[0].className.includes('focused') ?? false; FormattedTextBoxComment.textBox = this; - if (e.button === 0 && this.props.rootSelected?.() && !e.altKey && !e.ctrlKey && !e.metaKey) { + if (e.button === 0 && this._props.rootSelected?.() && !e.altKey && !e.ctrlKey && !e.metaKey) { if (e.clientX < this.ProseRef!.getBoundingClientRect().right) { // stop propagation if not in sidebar, otherwise nested boxes will lose focus to outer boxes. e.stopPropagation(); // if the text box's content is active, then it consumes all down events @@ -1636,7 +1642,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps } if (!state || !editor || !this.ProseRef?.children[0].className.includes('-focused')) return; if (!state.selection.empty && !(state.selection instanceof NodeSelection)) this.setupAnchorMenu(); - else if (this.props.isContentActive(true)) { + else if (this._props.isContentActive(true)) { const pcords = editor.posAtCoords({ left: e.clientX, top: e.clientY }); let xpos = pcords?.pos || 0; while (xpos > 0 && !state.doc.resolve(xpos).node()?.isTextblock) { @@ -1651,7 +1657,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps @action onDoubleClick = (e: React.MouseEvent): void => { FormattedTextBoxComment.textBox = this; - if (e.button === 0 && this.props.rootSelected?.() && !e.altKey && !e.ctrlKey && !e.metaKey) { + if (e.button === 0 && this._props.rootSelected?.() && !e.altKey && !e.ctrlKey && !e.metaKey) { if (e.clientX < this.ProseRef!.getBoundingClientRect().right) { // stop propagation if not in sidebar e.stopPropagation(); // if the text box is selected, then it consumes all click events @@ -1662,7 +1668,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps } FormattedTextBoxComment.Hide(); - if (e.buttons === 1 && this.props.rootSelected?.() && !e.altKey) { + if (e.buttons === 1 && this._props.rootSelected?.() && !e.altKey) { e.stopPropagation(); } }; @@ -1673,14 +1679,13 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps }; @action onFocused = (e: React.FocusEvent): void => { - console.log('FOCUSED = ' + this.layoutDoc.title + ' ' + this.props.rootSelected?.()); //applyDevTools.applyDevTools(this._editorView); - this.ProseRef?.children[0] === e.nativeEvent.target && this._editorView && RichTextMenu.Instance?.updateMenu(this._editorView, undefined, this.props, this.layoutDoc); + this.ProseRef?.children[0] === e.nativeEvent.target && this._editorView && RichTextMenu.Instance?.updateMenu(this._editorView, undefined, this._props, this.layoutDoc); e.stopPropagation(); }; onClick = (e: React.MouseEvent): void => { - if (!this.props.isContentActive()) return; + if (!this._props.isContentActive()) return; if ((e.nativeEvent as any).handledByInnerReactInstance) { e.stopPropagation(); return; @@ -1709,7 +1714,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps this._editorView!.dispatch(this._editorView!.state.tr.setSelection(NodeSelection.create(this._editorView!.state.doc, pcords.pos))); } } - if (this.props.rootSelected?.()) { + if (this._props.rootSelected?.()) { // if text box is selected, then it consumes all click events (e.nativeEvent as any).handledByInnerReactInstance = true; this.hitBulletTargets(e.clientX, e.clientY, !this._editorView?.state.selection.empty || this._forceUncollapse, false, this._forceDownNode, e.shiftKey); @@ -1723,7 +1728,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps clearStyleSheetRules(FormattedTextBox._bulletStyleSheet); const clickPos = this._editorView!.posAtCoords({ left: x, top: y }); let olistPos = clickPos?.pos; - if (clickPos && olistPos && this.props.rootSelected?.()) { + if (clickPos && olistPos && this._props.rootSelected?.()) { const clickNode = this._editorView?.state.doc.nodeAt(olistPos); const nodeBef = this._editorView?.state.doc.nodeAt(Math.max(0, olistPos - 1)); olistPos = nodeBef?.type === this._editorView?.state.schema.nodes.ordered_list ? olistPos - 1 : olistPos; @@ -1772,7 +1777,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps tr && this._editorView.dispatch(tr); } } - if (RichTextMenu.Instance?.view === this._editorView && !this.props.rootSelected?.()) { + if (RichTextMenu.Instance?.view === this._editorView && !this._props.rootSelected?.()) { RichTextMenu.Instance?.updateMenu(undefined, undefined, undefined, undefined); } FormattedTextBox._hadSelection = window.getSelection()?.toString() !== ''; @@ -1797,7 +1802,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps if ((e.altKey || e.ctrlKey) && e.key === 't') { e.preventDefault(); e.stopPropagation(); - this.props.setTitleFocus?.(); + this._props.setTitleFocus?.(); return; } const state = this._editorView!.state; @@ -1851,7 +1856,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps }; onScroll = (e: React.UIEvent) => { if (!LinkDocPreview.LinkInfo && this._scrollRef.current) { - if (!this.props.dontSelectOnLoad) { + if (!this._props.dontSelectOnLoad) { this._ignoreScroll = true; this.layoutDoc._layout_scrollTop = this._scrollRef.current.scrollTop; this._ignoreScroll = false; @@ -1861,7 +1866,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps } }; tryUpdateScrollHeight = () => { - const margins = 2 * NumCast(this.layoutDoc._yMargin, this.props.yPadding || 0); + 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.GetIsDragging()) { const toNum = (val: string) => Number(val.replace('px', '').replace('auto', '0')); @@ -1871,7 +1876,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps }; const proseHeight = !this.ProseRef ? 0 : children.reduce((p, child) => p + toHgt(child), margins); const scrollHeight = this.ProseRef && Math.min(NumCast(this.layoutDoc.layout_maxAutoHeight, proseHeight), proseHeight); - if (this.props.setHeight && scrollHeight && !this.props.dontRegisterView) { + if (this._props.setHeight && scrollHeight && !this._props.dontRegisterView) { // if top === 0, then the text box is growing upward (as the overlay caption) which doesn't contribute to the height computation const setScrollHeight = () => (this.dataDoc[this.fieldKey + '_scrollHeight'] = scrollHeight); @@ -1883,8 +1888,8 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps } } }; - fitContentsToBox = () => BoolCast(this.props.Document._freeform_fitContentsToBox); - sidebarContentScaling = () => (this.props.NativeDimScaling?.() || 1) * NumCast(this.layoutDoc._freeform_scale, 1); + fitContentsToBox = () => BoolCast(this._props.Document._freeform_fitContentsToBox); + sidebarContentScaling = () => (this._props.NativeDimScaling?.() || 1) * NumCast(this.layoutDoc._freeform_scale, 1); sidebarAddDocument = (doc: Doc | Doc[], sidebarKey: string = this.SidebarKey) => { if (!this.layoutDoc._layout_showSidebar) this.toggleSidebar(); return this.addDocument(doc, sidebarKey); @@ -1892,12 +1897,12 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps sidebarMoveDocument = (doc: Doc | Doc[], targetCollection: Doc | undefined, addDocument: (doc: Doc | Doc[]) => boolean) => this.moveDocument(doc, targetCollection, addDocument, this.SidebarKey); sidebarRemDocument = (doc: Doc | Doc[]) => this.removeDocument(doc, this.SidebarKey); setSidebarHeight = (height: number) => (this.dataDoc[this.SidebarKey + '_height'] = height); - sidebarWidth = () => (Number(this.layout_sidebarWidthPercent.substring(0, this.layout_sidebarWidthPercent.length - 1)) / 100) * this.props.PanelWidth(); + sidebarWidth = () => (Number(this.layout_sidebarWidthPercent.substring(0, this.layout_sidebarWidthPercent.length - 1)) / 100) * this._props.PanelWidth(); sidebarScreenToLocal = () => - this.props + this._props .ScreenToLocalTransform() - .translate(-(this.props.PanelWidth() - this.sidebarWidth()) / (this.props.NativeDimScaling?.() || 1), 0) - .scale(1 / NumCast(this.layoutDoc._freeform_scale, 1) / (this.props.NativeDimScaling?.() || 1)); + .translate(-(this._props.PanelWidth() - this.sidebarWidth()) / (this._props.NativeDimScaling?.() || 1), 0) + .scale(1 / NumCast(this.layoutDoc._freeform_scale, 1) / (this._props.NativeDimScaling?.() || 1)); @computed get audioHandle() { return !this._recordingDictation ? null : ( @@ -1920,9 +1925,9 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps TraceMobx(); const annotated = DocListCast(this.dataDoc[this.SidebarKey]).filter(d => d?.author).length; const color = !annotated ? Colors.WHITE : Colors.BLACK; - const backgroundColor = !annotated ? (this.sidebarWidth() ? Colors.MEDIUM_BLUE : Colors.BLACK) : this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.WidgetColor + (annotated ? ':annotated' : '')); + const backgroundColor = !annotated ? (this.sidebarWidth() ? Colors.MEDIUM_BLUE : Colors.BLACK) : this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.WidgetColor + (annotated ? ':annotated' : '')); - return !annotated && (!this.props.isContentActive() || SnappingManager.GetIsDragging() || Doc.ActiveTool !== InkTool.None) ? null : ( + return !annotated && (!this._props.isContentActive() || SnappingManager.GetIsDragging() || Doc.ActiveTool !== InkTool.None) ? null : ( <div className="formattedTextBox-sidebar-handle" onPointerDown={this.sidebarDown} @@ -1941,7 +1946,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps return ComponentTag === CollectionStackingView ? ( <SidebarAnnos ref={this._sidebarRef} - {...this.props} + {...this._props} Document={this.Document} layoutDoc={this.layoutDoc} dataDoc={this.dataDoc} @@ -1958,14 +1963,14 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps setHeight={this.setSidebarHeight} /> ) : ( - <div onPointerDown={e => setupMoveUpEvents(this, e, returnFalse, emptyFunction, () => SelectionManager.SelectView(this.props.DocumentView?.()!, false), true)}> + <div onPointerDown={e => setupMoveUpEvents(this, e, returnFalse, emptyFunction, () => SelectionManager.SelectView(this._props.DocumentView?.()!, false), true)}> <ComponentTag - {...this.props} + {...this._props} ref={this._sidebarTagRef as any} setContentView={emptyFunction} NativeWidth={returnZero} NativeHeight={returnZero} - PanelHeight={this.props.PanelHeight} + PanelHeight={this._props.PanelHeight} PanelWidth={this.sidebarWidth} xPadding={0} yPadding={0} @@ -1979,7 +1984,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps moveDocument={this.sidebarMoveDocument} addDocument={this.sidebarAddDocument} ScreenToLocalTransform={this.sidebarScreenToLocal} - renderDepth={this.props.renderDepth + 1} + renderDepth={this._props.renderDepth + 1} setHeight={this.setSidebarHeight} fitContentsToBox={this.fitContentsToBox} noSidebar={true} @@ -1997,12 +2002,12 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps } cycleAlternateText = () => { if (this.layoutDoc._layout_enableAltContentUI) { - const usePath = this.layoutDoc[`_${this.props.fieldKey}_usePath`]; - this.layoutDoc[`_${this.props.fieldKey}_usePath`] = usePath === undefined ? 'alternate' : usePath === 'alternate' ? 'alternate:hover' : undefined; + const usePath = this.layoutDoc[`_${this._props.fieldKey}_usePath`]; + this.layoutDoc[`_${this._props.fieldKey}_usePath`] = usePath === undefined ? 'alternate' : usePath === 'alternate' ? 'alternate:hover' : undefined; } }; @computed get overlayAlternateIcon() { - const usePath = this.layoutDoc[`_${this.props.fieldKey}_usePath`]; + const usePath = this.layoutDoc[`_${this._props.fieldKey}_usePath`]; return ( <Tooltip title={ @@ -2024,7 +2029,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps className="formattedTextBox-alternateButton" onPointerDown={e => setupMoveUpEvents(e.target, e, returnFalse, emptyFunction, e => this.cycleAlternateText())} style={{ - display: this.props.isContentActive() && !SnappingManager.GetIsDragging() ? 'flex' : 'none', + display: this._props.isContentActive() && !SnappingManager.GetIsDragging() ? 'flex' : 'none', background: usePath === undefined ? 'white' : usePath === 'alternate' ? 'black' : 'gray', color: usePath === undefined ? 'black' : 'white', }}> @@ -2033,9 +2038,9 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps </Tooltip> ); } - @computed get fieldKey() { - const usePath = StrCast(this.layoutDoc[`${this.props.fieldKey}_usePath`]); - return this.props.fieldKey + (usePath && (!usePath.includes(':hover') || this._isHovering || this.props.isContentActive()) ? `_${usePath.replace(':hover', '')}` : ''); + get fieldKey() { + const usePath = StrCast(this.layoutDoc[`${this._props.fieldKey}_usePath`]); + return this._props.fieldKey + (usePath && (!usePath.includes(':hover') || this._isHovering || this._props.isContentActive()) ? `_${usePath.replace(':hover', '')}` : ''); } @observable _isHovering = false; onPassiveWheel = (e: WheelEvent) => { @@ -2049,14 +2054,14 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps } // if scrollTop is 0, then don't let wheel trigger scroll on any container (which it would since onScroll won't be triggered on this) - if (this.props.isContentActive()) { - const scale = this.props.NativeDimScaling?.() || 1; - const styleFromLayoutString = Doc.styleFromLayoutString(this.Document, this.props, scale); // this converts any expressions in the format string to style props. e.g., <FormattedTextBox height='{this._headerHeight}px' > + if (this._props.isContentActive()) { + const scale = this._props.NativeDimScaling?.() || 1; + const styleFromLayoutString = Doc.styleFromLayoutString(this.Document, this._props, scale); // this converts any expressions in the format string to style props. e.g., <FormattedTextBox height='{this._headerHeight}px' > const height = Number(styleFromLayoutString.height?.replace('px', '')); // prevent default if selected || child is active but this doc isn't scrollable if ( - (this._scrollRef.current?.scrollHeight ?? 0) <= Math.ceil((height ? height : this.props.PanelHeight()) / scale) && // - (this.props.rootSelected?.() || this.isAnyChildContentActive()) + (this._scrollRef.current?.scrollHeight ?? 0) <= Math.ceil((height ? height : this._props.PanelHeight()) / scale) && // + (this._props.rootSelected?.() || this.isAnyChildContentActive()) ) { e.preventDefault(); } @@ -2065,25 +2070,25 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps }; _oldWheel: any; @computed get fontColor() { - return this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.Color); + return this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.Color); } @computed get fontSize() { - return this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.FontSize); + return this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.FontSize); } @computed get fontFamily() { - return this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.FontFamily); + return this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.FontFamily); } @computed get fontWeight() { - return this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.FontWeight); + return this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.FontWeight); } render() { TraceMobx(); - const scale = (this.props.NativeDimScaling?.() || 1) * NumCast(this.layoutDoc._freeform_scale, 1); + const scale = (this._props.NativeDimScaling?.() || 1) * NumCast(this.layoutDoc._freeform_scale, 1); const rounded = StrCast(this.layoutDoc.layout_borderRounding) === '100%' ? '-rounded' : ''; - setTimeout(() => !this.props.isContentActive() && FormattedTextBoxComment.textBox === this && FormattedTextBoxComment.Hide); - const paddingX = NumCast(this.layoutDoc._xMargin, this.props.xPadding || 0); - const paddingY = NumCast(this.layoutDoc._yMargin, this.props.yPadding || 0); - const styleFromLayoutString = Doc.styleFromLayoutString(this.Document, this.props, scale); // this converts any expressions in the format string to style props. e.g., <FormattedTextBox height='{this._headerHeight}px' > + setTimeout(() => !this._props.isContentActive() && FormattedTextBoxComment.textBox === this && FormattedTextBoxComment.Hide); + const paddingX = NumCast(this.layoutDoc._xMargin, this._props.xPadding || 0); + const paddingY = NumCast(this.layoutDoc._yMargin, this._props.yPadding || 0); + const styleFromLayoutString = Doc.styleFromLayoutString(this.Document, this._props, scale); // this converts any expressions in the format string to style props. e.g., <FormattedTextBox height='{this._headerHeight}px' > return styleFromLayoutString?.height === '0px' ? null : ( <div className="formattedTextBox" @@ -2095,7 +2100,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps r?.addEventListener('wheel', this.onPassiveWheel, { passive: false }); }} style={{ - ...(this.props.dontScale + ...(this._props.dontScale ? {} : { transform: `scale(${scale})`, @@ -2114,9 +2119,9 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps className="formattedTextBox-cont" ref={this._ref} style={{ - cursor: this.props.isContentActive() ? 'text' : undefined, - height: this.props.height ? 'max-content' : undefined, - pointerEvents: Doc.ActiveTool === InkTool.None && !this.props.onBrowseClick?.() ? undefined : 'none', + cursor: this._props.isContentActive() ? 'text' : undefined, + height: this._props.height ? 'max-content' : undefined, + pointerEvents: Doc.ActiveTool === InkTool.None && !this._props.onBrowseClick?.() ? undefined : 'none', }} onContextMenu={this.specificContextMenu} onKeyDown={this.onKeyDown} @@ -2131,7 +2136,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps className="formattedTextBox-outer" ref={this._scrollRef} style={{ - width: this.props.dontSelectOnLoad || this.noSidebar ? '100%' : `calc(100% - ${this.layout_sidebarWidthPercent})`, + width: this._props.dontSelectOnLoad || this.noSidebar ? '100%' : `calc(100% - ${this.layout_sidebarWidthPercent})`, overflow: this.layoutDoc._createDocOnCR ? 'hidden' : this.layoutDoc._layout_autoHeight ? 'visible' : undefined, }} onScroll={this.onScroll} @@ -2148,8 +2153,8 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps }} /> </div> - {this.noSidebar || this.props.dontSelectOnLoad || !this.SidebarShown || this.layout_sidebarWidthPercent === '0%' ? null : this.sidebarCollection} - {this.noSidebar || this.Document._layout_noSidebar || this.props.dontSelectOnLoad || this.Document._createDocOnCR || this.layoutDoc._chromeHidden ? null : this.sidebarHandle} + {this.noSidebar || this._props.dontSelectOnLoad || !this.SidebarShown || this.layout_sidebarWidthPercent === '0%' ? null : this.sidebarCollection} + {this.noSidebar || this.Document._layout_noSidebar || this._props.dontSelectOnLoad || this.Document._createDocOnCR || this.layoutDoc._chromeHidden ? null : this.sidebarHandle} {this.audioHandle} {this.layoutDoc._layout_enableAltContentUI && !this.layoutDoc._chromeHidden ? this.overlayAlternateIcon : null} </div> diff --git a/src/client/views/nodes/formattedText/RichTextMenu.tsx b/src/client/views/nodes/formattedText/RichTextMenu.tsx index 7ce06cf7f..d7e799161 100644 --- a/src/client/views/nodes/formattedText/RichTextMenu.tsx +++ b/src/client/views/nodes/formattedText/RichTextMenu.tsx @@ -1,7 +1,7 @@ import * as React from 'react'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { Tooltip } from '@mui/material'; -import { action, computed, IReactionDisposer, observable, reaction, runInAction } from 'mobx'; +import { action, computed, IReactionDisposer, makeObservable, observable, override, reaction, runInAction } from 'mobx'; import { observer } from 'mobx-react'; import { lift, wrapIn } from 'prosemirror-commands'; import { Mark, MarkType, Node as ProsNode, ResolvedPos } from 'prosemirror-model'; @@ -10,7 +10,7 @@ import { EditorState, NodeSelection, TextSelection } from 'prosemirror-state'; import { EditorView } from 'prosemirror-view'; import { Doc } from '../../../../fields/Doc'; import { BoolCast, Cast, StrCast } from '../../../../fields/Types'; -import { numberRange } from '../../../../Utils'; +import { copyProps, numberRange } from '../../../../Utils'; import { DocServer } from '../../../DocServer'; import { LinkManager } from '../../../util/LinkManager'; import { SelectionManager } from '../../../util/SelectionManager'; @@ -63,8 +63,12 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> { @observable private showLinkDropdown: boolean = false; _reaction: IReactionDisposer | undefined; - constructor(props: Readonly<{}>) { + _prevProps: AntimodeMenuProps; + @override _props: AntimodeMenuProps; + constructor(props: AntimodeMenuProps) { super(props); + this._props = this._prevProps = props; + makeObservable(this); runInAction(() => { RichTextMenu.Instance = this; this.updateMenu(undefined, undefined, props, this.layoutDoc); @@ -107,6 +111,9 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> { return BoolCast(this.layoutDoc?.layout_centered); } _disposer: IReactionDisposer | undefined; + componentDidUpdate() { + copyProps(this); + } componentDidMount() { this._disposer = reaction( () => SelectionManager.Views().slice(), @@ -361,7 +368,7 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> { } else if (SelectionManager.Views().some(dv => dv.ComponentView instanceof EquationBox)) { SelectionManager.Views().forEach(dv => (dv.Document._text_fontSize = fontSize)); } else Doc.UserDoc().fontSize = fontSize; - this.updateMenu(this.view, undefined, this.props, this.layoutDoc); + this.updateMenu(this.view, undefined, this._props, this.layoutDoc); }; setFontFamily = (family: string) => { @@ -370,7 +377,7 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> { this.setMark(fmark, this.view.state, (tx: any) => this.view!.dispatch(tx.addStoredMark(fmark)), true); this.view.focus(); } else Doc.UserDoc().fontFamily = family; - this.updateMenu(this.view, undefined, this.props, this.layoutDoc); + this.updateMenu(this.view, undefined, this._props, this.layoutDoc); }; setHighlight(color: string) { @@ -379,7 +386,7 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> { this.setMark(highlightMark, this.view.state, (tx: any) => this.view!.dispatch(tx.addStoredMark(highlightMark)), true); this.view.focus(); } else Doc.UserDoc()._fontHighlight = color; - this.updateMenu(this.view, undefined, this.props, this.layoutDoc); + this.updateMenu(this.view, undefined, this._props, this.layoutDoc); } setColor(color: string) { @@ -388,7 +395,7 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> { this.setMark(colorMark, this.view.state, (tx: any) => this.view!.dispatch(tx.addStoredMark(colorMark)), true); this.view.focus(); } else Doc.UserDoc().fontColor = color; - this.updateMenu(this.view, undefined, this.props, this.layoutDoc); + this.updateMenu(this.view, undefined, this._props, this.layoutDoc); } // TODO: remove doesn't work @@ -429,7 +436,7 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> { } } this.view.focus(); - this.updateMenu(this.view, undefined, this.props, this.layoutDoc); + this.updateMenu(this.view, undefined, this._props, this.layoutDoc); }; insertSummarizer(state: EditorState, dispatch: any) { @@ -669,7 +676,6 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> { }; @undoBatch - @action deleteLink = () => { if (this.view) { const linkAnchor = this.view.state.selection.$from.nodeAfter?.marks.find(m => m.type === this.view!.state.schema.marks.linkAnchor); @@ -816,6 +822,18 @@ export class ButtonDropdown extends React.Component<ButtonDropdownProps> { @observable private showDropdown: boolean = false; private ref: HTMLDivElement | null = null; + _prevProps: React.PropsWithChildren<ButtonDropdownProps>; + @observable _props: React.PropsWithChildren<ButtonDropdownProps>; + constructor(props: React.PropsWithChildren<ButtonDropdownProps>) { + super(props); + this._props = this._prevProps = props; + makeObservable(this); + } + + componentDidUpdate() { + copyProps(this); + } + componentDidMount() { document.addEventListener('pointerdown', this.onBlur); } @@ -850,22 +868,22 @@ export class ButtonDropdown extends React.Component<ButtonDropdownProps> { render() { return ( <div className="button-dropdown-wrapper" ref={node => (this.ref = node)}> - {!this.props.pdf ? ( - <div className="antimodeMenu-button dropdown-button-combined" onPointerDown={this.props.openDropdownOnButton ? this.onDropdownClick : undefined}> - {this.props.button} - <div style={{ marginTop: '-8.5', position: 'relative' }} onPointerDown={!this.props.openDropdownOnButton ? this.onDropdownClick : undefined}> + {!this._props.pdf ? ( + <div className="antimodeMenu-button dropdown-button-combined" onPointerDown={this._props.openDropdownOnButton ? this.onDropdownClick : undefined}> + {this._props.button} + <div style={{ marginTop: '-8.5', position: 'relative' }} onPointerDown={!this._props.openDropdownOnButton ? this.onDropdownClick : undefined}> <FontAwesomeIcon icon="caret-down" size="sm" /> </div> </div> ) : ( <> - {this.props.button} + {this._props.button} <button className="dropdown-button antimodeMenu-button" key="antimodebutton" onPointerDown={this.onDropdownClick}> <FontAwesomeIcon icon="caret-down" size="sm" /> </button> </> )} - {this.showDropdown ? this.props.dropdownContent : null} + {this.showDropdown ? this._props.dropdownContent : null} </div> ); } diff --git a/src/client/views/nodes/trails/PresBox.tsx b/src/client/views/nodes/trails/PresBox.tsx index 1317ca814..a224ec7fa 100644 --- a/src/client/views/nodes/trails/PresBox.tsx +++ b/src/client/views/nodes/trails/PresBox.tsx @@ -36,9 +36,6 @@ import { FieldView, FieldViewProps } from '../FieldView'; import { ScriptingBox } from '../ScriptingBox'; import './PresBox.scss'; import { PresEffect, PresEffectDirection, PresMovement, PresStatus } from './PresEnums'; -import _ = require('lodash'); -const { Howl } = require('howler'); - export interface pinDataTypes { scrollable?: boolean; dataviz?: number[]; @@ -156,7 +153,6 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { addToSelectedArray = action((doc: Doc) => this.selectedArray.add(doc)); removeFromSelectedArray = action((doc: Doc) => this.selectedArray.delete(doc)); - @action componentWillUnmount() { this._unmounting = true; if (this._presTimer) clearTimeout(this._presTimer); @@ -187,7 +183,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { }, { fireImmediately: true } ); - this.props.setContentView?.(this); + this._props.setContentView?.(this); this._unmounting = false; this.turnOffEdit(true); this._disposers.selection = reaction( @@ -603,7 +599,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { const dv = DocumentManager.Instance.getDocumentView(bestTarget); if (dv) { changed = true; - const computedScale = NumCast(activeItem.config_zoom, 1) * Math.min(dv.props.PanelWidth() / viewport.width, dv.props.PanelHeight() / viewport.height); + const computedScale = NumCast(activeItem.config_zoom, 1) * Math.min(dv._props.PanelWidth() / viewport.width, dv._props.PanelHeight() / viewport.height); activeItem.presentation_movement === PresMovement.Zoom && (bestTarget._freeform_scale = computedScale); dv.ComponentView?.brushView?.(viewport, transTime, 2500); } @@ -748,7 +744,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { const dragViewCache = Array.from(this._dragArray); const eleViewCache = Array.from(this._eleArray); const resetSelection = action(() => { - if (!this.props.isSelected()) { + if (!this._props.isSelected()) { const presDocView = DocumentManager.Instance.getDocumentView(this.Document); if (presDocView) SelectionManager.SelectView(presDocView, false); this.clearSelectedArray(); @@ -982,9 +978,9 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { enterMinimize = () => { this.updateCurrentPresentation(this.Document); clearTimeout(this._presTimer); - const pt = this.props.ScreenToLocalTransform().inverse().transformPoint(0, 0); - this.props.removeDocument?.(this.layoutDoc); - return PresBox.OpenPresMinimized(this.Document, [pt[0] + (this.props.PanelWidth() - 250), pt[1] + 10]); + const pt = this._props.ScreenToLocalTransform().inverse().transformPoint(0, 0); + this._props.removeDocument?.(this.layoutDoc); + return PresBox.OpenPresMinimized(this.Document, [pt[0] + (this._props.PanelWidth() - 250), pt[1] + 10]); }; exitMinimize = () => { if (Doc.IsInMyOverlay(this.layoutDoc)) { @@ -1052,7 +1048,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { return StrCast(activeItem.presentation_movement); }); - whenChildContentsActiveChanged = action((isActive: boolean) => this.props.whenChildContentsActiveChanged((this._isChildActive = isActive))); + whenChildContentsActiveChanged = action((isActive: boolean) => this._props.whenChildContentsActiveChanged((this._isChildActive = isActive))); // For dragging documents into the presentation trail addDocumentFilter = (docs: Doc[]) => { docs.forEach((doc, i) => { @@ -1063,7 +1059,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { audio.config_clipStart = NumCast(doc._timecodeToShow /* audioStart */, NumCast(doc._timecodeToShow /* videoStart */)); audio.config_clipEnd = NumCast(doc._timecodeToHide /* audioEnd */, NumCast(doc._timecodeToHide /* videoEnd */)); audio.presentation_duration = audio.config_clipStart - audio.config_clipEnd; - this.props.pinToPres(audio, { audioRange: true }); + this._props.pinToPres(audio, { audioRange: true }); setTimeout(() => this.removeDocument(doc), 0); return false; } @@ -1079,8 +1075,8 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { childLayoutTemplate = () => Docs.Create.PresElementBoxDocument(); removeDocument = (doc: Doc) => Doc.RemoveDocFromList(this.Document, this.fieldKey, doc); - getTransform = () => this.props.ScreenToLocalTransform().translate(-5, -65); // listBox padding-left and pres-box-cont minHeight - panelHeight = () => this.props.PanelHeight() - 40; + getTransform = () => this._props.ScreenToLocalTransform().translate(-5, -65); // listBox padding-left and pres-box-cont minHeight + panelHeight = () => this._props.PanelHeight() - 40; /** * For sorting the array so that the order is maintained when it is dropped. */ @@ -1313,7 +1309,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { } else if (doc.config_pinView && presCollection === tagDoc && dv) { // Case B: Document is presPinView and is presCollection const scale = 1 / NumCast(doc.config_viewScale); - const viewBounds = NumListCast(doc.config_viewBounds, [0, 0, dv.props.PanelWidth(), dv.props.PanelHeight()]); + const viewBounds = NumListCast(doc.config_viewBounds, [0, 0, dv._props.PanelWidth(), dv._props.PanelHeight()]); const height = (viewBounds[3] - viewBounds[1]) * scale; const width = (viewBounds[2] - viewBounds[0]) * scale; const indWidth = width / 10; @@ -1433,46 +1429,39 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { updateMovement = action((movement: PresMovement, all?: boolean) => (all ? this.childDocs : this.selectedArray).forEach(doc => (doc.presentation_movement = movement))); @undoBatch - @action updateHideBefore = (activeItem: Doc) => { activeItem.presentation_hideBefore = !activeItem.presentation_hideBefore; this.selectedArray.forEach(doc => (doc.presentation_hideBefore = activeItem.presentation_hideBefore)); }; @undoBatch - @action updateHide = (activeItem: Doc) => { activeItem.presentation_hide = !activeItem.presentation_hide; this.selectedArray.forEach(doc => (doc.presentation_hide = activeItem.presentation_hide)); }; @undoBatch - @action updateHideAfter = (activeItem: Doc) => { activeItem.presentation_hideAfter = !activeItem.presentation_hideAfter; this.selectedArray.forEach(doc => (doc.presentation_hideAfter = activeItem.presentation_hideAfter)); }; @undoBatch - @action updateOpenDoc = (activeItem: Doc) => { activeItem.presentation_openInLightbox = !activeItem.presentation_openInLightbox; this.selectedArray.forEach(doc => (doc.presentation_openInLightbox = activeItem.presentation_openInLightbox)); }; @undoBatch - @action updateEaseFunc = (activeItem: Doc) => { activeItem.presEaseFunc = activeItem.presEaseFunc === 'linear' ? 'ease' : 'linear'; this.selectedArray.forEach(doc => (doc.presEaseFunc = activeItem.presEaseFunc)); }; @undoBatch - @action updateEffectDirection = (effect: PresEffectDirection, all?: boolean) => (all ? this.childDocs : this.selectedArray).forEach(doc => (doc.presentation_effectDirection = effect)); @undoBatch - @action updateEffect = (effect: PresEffect, bullet: boolean, all?: boolean) => (all ? this.childDocs : this.selectedArray).forEach(doc => (bullet ? (doc.presBulletEffect = effect) : (doc.presentation_effect = effect))); static _sliderBatch: any; @@ -1505,7 +1494,6 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { }; @undoBatch - @action applyTo = (array: Doc[]) => { this.updateMovement(this.activeItem.presentation_movement as PresMovement, true); this.updateEffect(this.activeItem.presentation_effect as PresEffect, false, true); @@ -2219,10 +2207,10 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { const config_data = Cast(this.Document.data, listSpec(Doc)); if (data && config_data) { data.push(doc); - this.props.pinToPres(doc, {}); + this._props.pinToPres(doc, {}); this.gotoDocument(this.childDocs.length, this.activeItem); } else { - this.props.addDocTab(doc, OpenWhere.addRight); + this._props.addDocTab(doc, OpenWhere.addRight); } } }; @@ -2283,15 +2271,15 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { @computed get toolbarWidth(): number { - return this.props.PanelWidth(); + return this._props.PanelWidth(); } @action - toggleProperties = () => (SettingsManager.propertiesWidth = SettingsManager.propertiesWidth > 0 ? 0 : 250); + toggleProperties = () => (SettingsManager.Instance.propertiesWidth = SettingsManager.Instance.propertiesWidth > 0 ? 0 : 250); @computed get toolbar() { - const propIcon = SettingsManager.propertiesWidth > 0 ? 'angle-double-right' : 'angle-double-left'; - const propTitle = SettingsManager.propertiesWidth > 0 ? 'Close Presentation Panel' : 'Open Presentation Panel'; + const propIcon = SettingsManager.Instance.propertiesWidth > 0 ? 'angle-double-right' : 'angle-double-left'; + const propTitle = SettingsManager.Instance.propertiesWidth > 0 ? 'Close Presentation Panel' : 'Open Presentation Panel'; const mode = StrCast(this.Document._type_collection) as CollectionViewType; const isMini: boolean = this.toolbarWidth <= 100; const activeColor = SettingsManager.userVariantColor; @@ -2320,7 +2308,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { </Tooltip> <Tooltip title={<div className="dash-tooltip">{propTitle}</div>}> <div className="toolbar-button" style={{ position: 'absolute', right: 4, fontSize: 16 }} onClick={this.toggleProperties}> - <FontAwesomeIcon className={'toolbar-thumbtack'} icon={propIcon} style={{ color: SettingsManager.propertiesWidth > 0 ? activeColor : inactiveColor }} /> + <FontAwesomeIcon className={'toolbar-thumbtack'} icon={propIcon} style={{ color: SettingsManager.Instance.propertiesWidth > 0 ? activeColor : inactiveColor }} /> </div> </Tooltip> </> @@ -2366,7 +2354,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { } })}> <FontAwesomeIcon icon={'play-circle'} /> - <div style={{ display: this.props.PanelWidth() > 200 ? 'inline-flex' : 'none' }}> Present</div> + <div style={{ display: this._props.PanelWidth() > 200 ? 'inline-flex' : 'none' }}> Present</div> </div> {mode === CollectionViewType.Carousel3D || isMini ? null : ( <div @@ -2476,12 +2464,12 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { <b>1</b> </div> </Tooltip> - <div className="presPanel-button-text" onClick={() => this.gotoDocument(0, this.activeItem)} style={{ display: inOverlay || this.props.PanelWidth() > 250 ? 'inline-flex' : 'none' }}> + <div className="presPanel-button-text" onClick={() => this.gotoDocument(0, this.activeItem)} style={{ display: inOverlay || this._props.PanelWidth() > 250 ? 'inline-flex' : 'none' }}> {inOverlay ? '' : 'Slide'} {this.itemIndex + 1} {this.activeItem?.presentation_indexed !== undefined ? `(${this.activeItem.presentation_indexed}/${this.progressivizedItems(this.activeItem)?.length})` : ''} / {this.childDocs.length} </div> <div className="presPanel-divider"></div> - {this.props.PanelWidth() > 250 ? ( + {this._props.PanelWidth() > 250 ? ( <div className="presPanel-button-text" onClick={undoBatch( @@ -2527,7 +2515,6 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { }; @undoBatch - @action exitClicked = () => { this.layoutDoc.presentation_status = this._exitTrail?.() ?? this.exitMinimize(); clearTimeout(this._presTimer); @@ -2570,7 +2557,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { this.itemIndex === this.childDocs.length - 1 && (this.activeItem.presentation_indexed === undefined || NumCast(this.activeItem.presentation_indexed) === (this.progressivizedItems(this.activeItem)?.length ?? 0)); const presStart = !this.layoutDoc.presLoop && this.itemIndex === 0; - return this.props.addDocTab === returnFalse ? ( // bcz: hack!! - addDocTab === returnFalse only when this is being rendered by the OverlayView which means the doc is a mini player + return this._props.addDocTab === returnFalse ? ( // bcz: hack!! - addDocTab === returnFalse only when this is being rendered by the OverlayView which means the doc is a mini player <div className="miniPres" onClick={e => e.stopPropagation()} onPointerEnter={action(e => (this._forceKeyEvents = true))}> <div className="presPanelOverlay" @@ -2620,8 +2607,8 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { <div className="Slide"> {mode !== CollectionViewType.Invalid ? ( <CollectionView - {...this.props} - PanelWidth={this.props.PanelWidth} + {...this._props} + PanelWidth={this._props.PanelWidth} PanelHeight={this.panelHeight} childIgnoreNativeSize={true} moveDocument={returnFalse} diff --git a/src/client/views/nodes/trails/PresElementBox.tsx b/src/client/views/nodes/trails/PresElementBox.tsx index cd6beac57..9fcb496b8 100644 --- a/src/client/views/nodes/trails/PresElementBox.tsx +++ b/src/client/views/nodes/trails/PresElementBox.tsx @@ -1,12 +1,12 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { Tooltip } from '@mui/material'; -import { action, computed, IReactionDisposer, observable, reaction, runInAction } from 'mobx'; +import { action, computed, IReactionDisposer, makeObservable, observable, override, reaction, runInAction } from 'mobx'; import { observer } from 'mobx-react'; import { Doc, DocListCast, Opt } from '../../../../fields/Doc'; import { Id } from '../../../../fields/FieldSymbols'; import { List } from '../../../../fields/List'; import { BoolCast, Cast, DocCast, NumCast, StrCast } from '../../../../fields/Types'; -import { emptyFunction, returnEmptyDoclist, returnFalse, returnTrue, setupMoveUpEvents } from '../../../../Utils'; +import { copyProps, emptyFunction, returnEmptyDoclist, returnFalse, returnTrue, setupMoveUpEvents } from '../../../../Utils'; import { Docs } from '../../../documents/Documents'; import { CollectionViewType } from '../../../documents/DocumentTypes'; import { DocumentManager } from '../../../util/DocumentManager'; @@ -43,9 +43,21 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() { @observable _dragging = false; + _prevProps: FieldViewProps; + @override _props: FieldViewProps; + constructor(props: FieldViewProps) { + super(props); + this._props = this._prevProps = props; + makeObservable(this); + } + + componentDidUpdate() { + copyProps(this); + } + // the presentation view that renders this slide @computed get presBoxView() { - return this.props.DocumentView?.()?.props.docViewPath().lastElement()?.ComponentView as PresBox; + return this._props.DocumentView?.()?._props.docViewPath().lastElement()?.ComponentView as PresBox; } // the presentation view document that renders this slide @@ -56,7 +68,7 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() { // Since this node is being rendered with a template, this method retrieves // the actual slide being rendered from the auto-generated rendering template @computed get slideDoc() { - return this.props.TemplateDataDocument ?? this.props.Document; + return this._props.TemplateDataDocument ?? this.Document; } // this is the document in the workspaces that is targeted by the slide @@ -91,9 +103,9 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() { presExpandDocumentClick = () => (this.slideDoc.presentation_expandInlineButton = !this.slideDoc.presentation_expandInlineButton); embedHeight = () => this.collapsedHeight + this.expandViewHeight; - embedWidth = () => this.props.PanelWidth() / 2; + embedWidth = () => this._props.PanelWidth() / 2; styleProvider = (doc: Doc | undefined, props: Opt<DocumentViewProps>, property: string): any => { - return property === StyleProp.Opacity ? 1 : this.props.styleProvider?.(doc, props, property); + return property === StyleProp.Opacity ? 1 : this._props.styleProvider?.(doc, props, property); }; /** * The function that is responsible for rendering a preview or not for this @@ -106,19 +118,19 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() { Document={PresBox.targetRenderedDoc(this.slideDoc)} PanelWidth={this.embedWidth} PanelHeight={this.embedHeight} - isContentActive={this.props.isContentActive} + isContentActive={this._props.isContentActive} styleProvider={this.styleProvider} hideLinkButton={true} ScreenToLocalTransform={Transform.Identity} - renderDepth={this.props.renderDepth + 1} + renderDepth={this._props.renderDepth + 1} docViewPath={returnEmptyDoclist} - childFilters={this.props.childFilters} - childFiltersByRanges={this.props.childFiltersByRanges} - searchFilterDocs={this.props.searchFilterDocs} + childFilters={this._props.childFilters} + childFiltersByRanges={this._props.childFiltersByRanges} + searchFilterDocs={this._props.searchFilterDocs} addDocument={returnFalse} removeDocument={returnFalse} fitContentsToBox={returnTrue} - moveDocument={this.props.moveDocument!} + moveDocument={this._props.moveDocument!} focus={emptyFunction} whenChildContentsActiveChanged={returnFalse} addDocTab={returnFalse} @@ -191,8 +203,8 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() { const dragArray = this.presBoxView?._dragArray ?? []; const dragData = new DragManager.DocumentDragData(this.presBoxView?.sortArray() ?? []); if (!dragData.draggedDocuments.length) dragData.draggedDocuments.push(this.slideDoc); - dragData.treeViewDoc = this.presBox?._type_collection === CollectionViewType.Tree ? this.presBox : undefined; // this.props.DocumentView?.()?.props.treeViewDoc; - dragData.moveDocument = this.props.moveDocument; + dragData.treeViewDoc = this.presBox?._type_collection === CollectionViewType.Tree ? this.presBox : undefined; // this._props.DocumentView?.()?._props.treeViewDoc; + dragData.moveDocument = this._props.moveDocument; const dragItem: HTMLElement[] = []; const classesToRestore = new Map<HTMLElement, string>(); if (dragArray.length === 1) { @@ -265,8 +277,8 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() { @action toggleProperties = () => { - if (SettingsManager.propertiesWidth < 5) { - SettingsManager.propertiesWidth = 250; + if (SettingsManager.Instance.propertiesWidth < 5) { + SettingsManager.Instance.propertiesWidth = 250; } }; @@ -276,7 +288,7 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() { if (this.presBox && this.indexInPres < (this.presBoxView?.itemIndex || 0)) { this.presBox.itemIndex = (this.presBoxView?.itemIndex || 0) - 1; } - this.props.removeDocument?.(this.slideDoc); + this._props.removeDocument?.(this.slideDoc); this.presBoxView?.removeFromSelectedArray(this.slideDoc); this.removeAllRecordingInOverlay(); }), @@ -299,7 +311,6 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() { * @param activeItem */ @undoBatch - @action updateCapturedContainerLayout = (presTargetDoc: Doc, activeItem: Doc) => { const targetDoc = DocCast(presTargetDoc.annotationOn) ?? presTargetDoc; activeItem.config_x = NumCast(targetDoc.x); @@ -389,7 +400,6 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() { ); @undoBatch - @action lfg = (e: React.MouseEvent) => { e.stopPropagation(); // TODO: fix this bug @@ -404,7 +414,7 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() { get toolbarWidth(): number { const presBoxDocView = DocumentManager.Instance.getDocumentView(this.presBox); const width = NumCast(this.presBox?._width); - return presBoxDocView ? presBoxDocView.props.PanelWidth() : width ? width : 300; + return presBoxDocView ? presBoxDocView._props.PanelWidth() : width ? width : 300; } @computed get presButtons() { @@ -453,8 +463,8 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() { {!activeItem.presentation_groupWithUp ? 'Not grouped with previous slide (click to group)' : activeItem.presentation_groupWithUp === 1 - ? 'Run simultaneously with previous slide (click again to run after)' - : 'Run after previous slide (click to ungroup from previous)'} + ? 'Run simultaneously with previous slide (click again to run after)' + : 'Run after previous slide (click to ungroup from previous)'} </div> }> <div @@ -530,10 +540,10 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() { style={{ backgroundColor: presColorBool ? (isSelected ? 'rgba(250,250,250,0.3)' : 'transparent') : isSelected ? Colors.LIGHT_BLUE : 'transparent', opacity: this._dragging ? 0.3 : 1, - paddingLeft: NumCast(this.layoutDoc._xPadding, this.props.xPadding), - paddingRight: NumCast(this.layoutDoc._xPadding, this.props.xPadding), - paddingTop: NumCast(this.layoutDoc._yPadding, this.props.yPadding), - paddingBottom: NumCast(this.layoutDoc._yPadding, this.props.yPadding), + paddingLeft: NumCast(this.layoutDoc._xPadding, this._props.xPadding), + paddingRight: NumCast(this.layoutDoc._xPadding, this._props.xPadding), + paddingTop: NumCast(this.layoutDoc._yPadding, this._props.yPadding), + paddingBottom: NumCast(this.layoutDoc._yPadding, this._props.yPadding), }} onDoubleClick={action(e => { this.toggleProperties(); @@ -552,7 +562,7 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() { className={`presItem-slide ${isCurrent ? 'active' : ''}${this.slideDoc.runProcess ? ' testingv2' : ''}`} style={{ display: 'infline-block', - backgroundColor: this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.BackgroundColor), + backgroundColor: this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.BackgroundColor), //layout_boxShadow: presBoxColor && presBoxColor !== 'white' && presBoxColor !== 'transparent' ? (isCurrent ? '0 0 0px 1.5px' + presBoxColor : undefined) : undefined, border: presBoxColor && presBoxColor !== 'white' && presBoxColor !== 'transparent' ? (isCurrent ? presBoxColor + ' solid 2.5px' : undefined) : undefined, }}> |
