diff options
Diffstat (limited to 'src/client/views/nodes')
91 files changed, 4947 insertions, 4601 deletions
diff --git a/src/client/views/nodes/AudioBox.tsx b/src/client/views/nodes/AudioBox.tsx index c685ec66f..9deed4de4 100644 --- a/src/client/views/nodes/AudioBox.tsx +++ b/src/client/views/nodes/AudioBox.tsx @@ -1,28 +1,34 @@ +/* eslint-disable jsx-a11y/no-static-element-interactions */ +/* eslint-disable jsx-a11y/click-events-have-key-events */ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { Tooltip } from '@mui/material'; import { action, computed, IReactionDisposer, makeObservable, observable, runInAction } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; +import { returnFalse, setupMoveUpEvents } from '../../../ClientUtils'; import { DateField } from '../../../fields/DateField'; import { Doc } from '../../../fields/Doc'; import { DocData } from '../../../fields/DocSymbols'; 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 { Docs, DocUtils } from '../../documents/Documents'; +import { formatTime } from '../../../Utils'; +import { Docs } from '../../documents/Documents'; +import { DocumentType } from '../../documents/DocumentTypes'; +import { DocUtils } from '../../documents/DocUtils'; import { Networking } from '../../Network'; import { DragManager } from '../../util/DragManager'; -import { LinkManager } from '../../util/LinkManager'; import { undoBatch } from '../../util/UndoManager'; import { CollectionStackedTimeline, TrimScope } from '../collections/CollectionStackedTimeline'; import { ContextMenu } from '../ContextMenu'; import { ContextMenuProps } from '../ContextMenuItem'; import { ViewBoxAnnotatableComponent } from '../DocComponent'; +import { DocViewUtils } from '../DocViewUtils'; +import { PinDocView, PinProps } from '../PinFuncs'; import './AudioBox.scss'; -import { FocusViewOptions, FieldView, FieldViewProps } from './FieldView'; -import { PinProps, PresBox } from './trails'; -import { OpenWhere } from './DocumentView'; +import { DocumentView } from './DocumentView'; +import { FieldView, FieldViewProps } from './FieldView'; +import { OpenWhere } from './OpenWhere'; /** * AudioBox @@ -42,7 +48,7 @@ declare class MediaRecorder { constructor(e: any); // whatever MediaRecorder has } -export enum media_state { +export enum mediaState { PendingRecording = 'pendingRecording', Recording = 'recording', Paused = 'paused', @@ -94,19 +100,19 @@ export class AudioBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { return this._props.PanelHeight() < 50; } // used to collapse timeline when node is shrunk @computed get links() { - return LinkManager.Links(this.dataDoc); + return Doc.Links(this.dataDoc); } @computed get mediaState() { - return this.dataDoc.mediaState as media_state; + return this.dataDoc.mediaState as mediaState; + } + set mediaState(value) { + this.dataDoc.mediaState = value; } @computed get path() { // returns the path of the audio file const path = Cast(this.Document[this.fieldKey], AudioField, null)?.url.href || ''; return path === nullAudio ? '' : path; } - set mediaState(value) { - this.dataDoc.mediaState = value; - } @computed get timeline() { return this._stackedTimeline; @@ -117,17 +123,17 @@ export class AudioBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { this._dropDisposer?.(); Object.values(this._disposers).forEach(disposer => disposer?.()); - this.mediaState === media_state.Recording && this.stopRecording(); + this.mediaState === mediaState.Recording && this.stopRecording(); } @action componentDidMount() { this._props.setContentViewBox?.(this); if (this.path) { - this.mediaState = media_state.Paused; + this.mediaState = mediaState.Paused; this.setPlayheadTime(NumCast(this.layoutDoc.clipStart)); } else { - this.mediaState = undefined as any as media_state; + this.mediaState = undefined as any as mediaState; } } @@ -149,24 +155,24 @@ export class AudioBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { this.Document, this.dataDoc, this.annotationKey, - this._ele?.currentTime || Cast(this.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 === mediaState.Recording ? (Date.now() - (this.recordingStart || 0)) / 1000 : undefined), undefined, undefined, addAsAnnotation ) || this.Document : Docs.Create.ConfigDocument({ title: '#' + timecode, _timecodeToShow: timecode, annotationOn: this.Document }); - PresBox.pinDocView(anchor, { pinDocLayout: pinProps?.pinDocLayout, pinData: { ...(pinProps?.pinData ?? {}), temporal: true } }, this.Document); + PinDocView(anchor, { pinDocLayout: pinProps?.pinDocLayout, pinData: { ...(pinProps?.pinData ?? {}), temporal: true } }, this.Document); return anchor; }; // updates timecode and shows it in timeline, follows links at time @action timecodeChanged = () => { - if (this.mediaState !== media_state.Recording && this._ele) { + if (this.mediaState !== mediaState.Recording && this._ele) { this.links .map(l => this.getLinkData(l)) - .forEach(({ la1, la2, linkTime }) => { + .forEach(({ la1, linkTime }) => { if (linkTime > NumCast(this.layoutDoc._layout_currentTimecode) && linkTime < this._ele!.currentTime) { Doc.linkFollowHighlight(la1); } @@ -180,7 +186,7 @@ export class AudioBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { @action playFrom = (seekTimeInSeconds: number, endTime?: number, fullPlay: boolean = false) => { clearTimeout(this._play); // abort any previous clip ending - if (Number.isNaN(this._ele?.duration)) { + if (isNaN(this._ele?.duration ?? Number.NaN)) { // audio element isn't loaded yet... wait 1/2 second and try again setTimeout(() => this.playFrom(seekTimeInSeconds, endTime), 500); } else if (this.timeline && this._ele && AudioBox.Enabled) { @@ -191,7 +197,7 @@ export class AudioBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { if (seekTimeInSeconds >= 0 && this.timeline.trimStart <= end && seekTimeInSeconds <= this.timeline.trimEnd) { this._ele.currentTime = start; this._ele.play(); - this.mediaState = media_state.Playing; + this.mediaState = mediaState.Playing; this.addCurrentlyPlaying(); this._play = setTimeout( () => { @@ -213,9 +219,9 @@ export class AudioBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { @action removeCurrentlyPlaying = () => { const docView = this.DocumentView?.(); - if (CollectionStackedTimeline.CurrentlyPlaying && docView) { - const index = CollectionStackedTimeline.CurrentlyPlaying.indexOf(docView); - index !== -1 && CollectionStackedTimeline.CurrentlyPlaying.splice(index, 1); + if (DocumentView.CurrentlyPlaying && docView) { + const index = DocumentView.CurrentlyPlaying.indexOf(docView); + index !== -1 && DocumentView.CurrentlyPlaying.splice(index, 1); } }; @@ -223,17 +229,17 @@ export class AudioBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { @action addCurrentlyPlaying = () => { const docView = this.DocumentView?.(); - if (!CollectionStackedTimeline.CurrentlyPlaying) { - CollectionStackedTimeline.CurrentlyPlaying = []; + if (!DocumentView.CurrentlyPlaying) { + DocumentView.CurrentlyPlaying = []; } - if (docView && CollectionStackedTimeline.CurrentlyPlaying.indexOf(docView) === -1) { - CollectionStackedTimeline.CurrentlyPlaying.push(docView); + if (docView && DocumentView.CurrentlyPlaying.indexOf(docView) === -1) { + DocumentView.CurrentlyPlaying.push(docView); } }; // update the recording time updateRecordTime = () => { - if (this.mediaState === media_state.Recording) { + if (this.mediaState === mediaState.Recording) { setTimeout(this.updateRecordTime, 30); if (!this._paused) { this.layoutDoc._layout_currentTimecode = (new Date().getTime() - this._recordStart - this._pausedTime) / 1000; @@ -246,7 +252,7 @@ export class AudioBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { this._stream = await navigator.mediaDevices.getUserMedia({ audio: true }); this._recorder = new MediaRecorder(this._stream); this.dataDoc[this.fieldKey + '_recordingStart'] = new DateField(); - DocUtils.ActiveRecordings.push(this); + DocViewUtils.ActiveRecordings.push(this); this._recorder.ondataavailable = async (e: any) => { const [{ result }] = await Networking.UploadFilesToServer({ file: e.data }); if (!(result instanceof Error)) { @@ -254,7 +260,9 @@ export class AudioBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { } }; this._recordStart = new Date().getTime(); - runInAction(() => (this.mediaState = media_state.Recording)); + runInAction(() => { + this.mediaState = mediaState.Recording; + }); setTimeout(this.updateRecordTime); this._recorder.start(); setTimeout(this.stopRecording, 60 * 60 * 1000); // stop after an hour @@ -269,34 +277,34 @@ export class AudioBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { const now = new Date().getTime(); this._paused && (this._pausedTime += now - this._pauseStart); this.dataDoc[this.fieldKey + '_duration'] = (now - this._recordStart - this._pausedTime) / 1000; - this.mediaState = media_state.Paused; + this.mediaState = mediaState.Paused; this._stream?.getAudioTracks()[0].stop(); - const ind = DocUtils.ActiveRecordings.indexOf(this); - ind !== -1 && DocUtils.ActiveRecordings.splice(ind, 1); + const ind = DocViewUtils.ActiveRecordings.indexOf(this); + ind !== -1 && DocViewUtils.ActiveRecordings.splice(ind, 1); } }; // context menu - specificContextMenu = (e: React.MouseEvent): void => { + specificContextMenu = (): void => { const funcs: ContextMenuProps[] = []; funcs.push({ description: (this.layoutDoc.hideAnchors ? "Don't hide" : 'Hide') + ' anchors', - event: e => (this.layoutDoc.hideAnchors = !this.layoutDoc.hideAnchors), + event: () => { this.layoutDoc.hideAnchors = !this.layoutDoc.hideAnchors; }, // prettier-ignore icon: 'expand-arrows-alt', }); funcs.push({ description: (this.layoutDoc.dontAutoFollowLinks ? '' : "Don't") + ' follow links when encountered', - event: e => (this.layoutDoc.dontAutoFollowLinks = !this.layoutDoc.dontAutoFollowLinks), + event: () => { this.layoutDoc.dontAutoFollowLinks = !this.layoutDoc.dontAutoFollowLinks}, // prettier-ignore icon: 'expand-arrows-alt', }); funcs.push({ description: (this.layoutDoc.dontAutoPlayFollowedLinks ? '' : "Don't") + ' play when link is selected', - event: e => (this.layoutDoc.dontAutoPlayFollowedLinks = !this.layoutDoc.dontAutoPlayFollowedLinks), + event: () => { this.layoutDoc.dontAutoPlayFollowedLinks = !this.layoutDoc.dontAutoPlayFollowedLinks; }, // prettier-ignore icon: 'expand-arrows-alt', }); funcs.push({ description: (this.layoutDoc.autoPlayAnchors ? "Don't auto" : 'Auto') + ' play anchors onClick', - event: e => (this.layoutDoc.autoPlayAnchors = !this.layoutDoc.autoPlayAnchors), + event: () => { this.layoutDoc.autoPlayAnchors = !this.layoutDoc.autoPlayAnchors; }, // prettier-ignore icon: 'expand-arrows-alt', }); ContextMenu.Instance?.addItem({ @@ -342,9 +350,9 @@ export class AudioBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { } }; - IsPlaying = () => this.mediaState === media_state.Playing; + IsPlaying = () => this.mediaState === mediaState.Playing; TogglePause = () => { - if (this.mediaState === media_state.Paused) this.Play(); + if (this.mediaState === mediaState.Paused) this.Play(); else this.pause(); }; // pause playback without removing from the playback list to allow user to play it again. @@ -352,7 +360,7 @@ export class AudioBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { pause = () => { if (this._ele) { this._ele.pause(); - this.mediaState = media_state.Paused; + this.mediaState = mediaState.Paused; // if paused in the middle of playback, prevents restart on next play if (!this._finished) clearTimeout(this._play); @@ -434,7 +442,7 @@ export class AudioBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { }; // plays link - playLink = (link: Doc, options: FocusViewOptions) => { + playLink = (link: Doc /* , options: FocusViewOptions */) => { if (link.annotationOn === this.Document) { if (!this.layoutDoc.dontAutoPlayFollowedLinks) { this.playFrom(this.timeline?.anchorStart(link) || 0, this.timeline?.anchorEnd(link)); @@ -460,13 +468,17 @@ export class AudioBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { }; @action - timelineWhenChildContentsActiveChanged = (isActive: boolean) => this._props.whenChildContentsActiveChanged((this._isAnyChildContentActive = isActive)); + timelineWhenChildContentsActiveChanged = (isActive: boolean) => { + this._props.whenChildContentsActiveChanged((this._isAnyChildContentActive = isActive)); + }; timelineScreenToLocal = () => this.ScreenToLocalBoxXf().translate(0, -AudioBox.topControlsHeight); - setPlayheadTime = (time: number) => (this._ele!.currentTime /*= this.layoutDoc._layout_currentTimecode*/ = time); + setPlayheadTime = (time: number) => { + this._ele!.currentTime /* = this.layoutDoc._layout_currentTimecode */ = time; + }; - playing = () => this.mediaState === media_state.Playing; + playing = () => this.mediaState === mediaState.Playing; isActiveChild = () => this._isAnyChildContentActive; @@ -497,7 +509,7 @@ export class AudioBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { e, returnFalse, returnFalse, - action(e => { + action(() => { if (this.timeline?.IsTrimming !== TrimScope.None) { this.timeline?.CancelTrimming(); } else { @@ -523,7 +535,7 @@ export class AudioBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { e, returnFalse, returnFalse, - action((e: PointerEvent, doubleTap?: boolean) => { + action((moveEv: PointerEvent, doubleTap?: boolean) => { if (doubleTap) { this.startTrim(TrimScope.All); } else if (this.timeline) { @@ -563,14 +575,7 @@ export class AudioBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { setupTimelineDrop = (r: HTMLDivElement | null) => { if (r && this.timeline) { this._dropDisposer?.(); - this._dropDisposer = DragManager.MakeDropTarget( - r, - (e, de) => { - const [xp, yp] = this.ScreenToLocalBoxXf().transformPoint(de.x, de.y); - de.complete.docDragData && this.timeline?.internalDocDrop(e, de, de.complete.docDragData, xp); - }, - this.layoutDoc - ); + this._dropDisposer = DragManager.MakeDropTarget(r, (e, de) => de.complete.docDragData && this.timeline?.internalDocDrop(e, de, de.complete.docDragData), this.layoutDoc); } }; @@ -581,7 +586,7 @@ export class AudioBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { <div className="audiobox-dictation" onPointerDown={this.onFile}> <FontAwesomeIcon size="2x" icon="file-alt" /> </div> - {[media_state.Recording, media_state.Playing].includes(this.mediaState) ? ( + {[mediaState.Recording, mediaState.Playing].includes(this.mediaState) ? ( <div className="recording-controls" onClick={e => e.stopPropagation()}> <div className="record-button" onPointerDown={this.Record}> <FontAwesomeIcon size="2x" icon="stop" /> @@ -614,31 +619,29 @@ export class AudioBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { <div className="controls-left"> <div className="audiobox-button" - title={this.mediaState === media_state.Paused ? 'play' : 'pause'} + title={this.mediaState === mediaState.Paused ? 'play' : 'pause'} onPointerDown={ - this.mediaState === media_state.Paused + this.mediaState === mediaState.Paused ? this.Play : e => { e.stopPropagation(); this.Pause(); } }> - <FontAwesomeIcon icon={this.mediaState === media_state.Paused ? 'play' : 'pause'} size={'1x'} /> + <FontAwesomeIcon icon={this.mediaState === mediaState.Paused ? 'play' : 'pause'} size="1x" /> </div> {!this.miniPlayer && ( <> <Tooltip title={<>trim audio</>}> <div className="audiobox-button" onPointerDown={this.onClipPointerDown}> - <FontAwesomeIcon icon={this.timeline?.IsTrimming !== TrimScope.None ? 'check' : 'cut'} size={'1x'} /> + <FontAwesomeIcon icon={this.timeline?.IsTrimming !== TrimScope.None ? 'check' : 'cut'} size="1x" /> </div> </Tooltip> - {this.timeline?.IsTrimming == TrimScope.None && !NumCast(this.layoutDoc.clipStart) && NumCast(this.layoutDoc.clipEnd) === this.rawDuration ? ( - <></> - ) : ( - <Tooltip title={<>{this.timeline?.IsTrimming !== TrimScope.None ? 'Cancel trimming' : 'Edit original timeline'}</>}> + {this.timeline?.IsTrimming === TrimScope.None && !NumCast(this.layoutDoc.clipStart) && NumCast(this.layoutDoc.clipEnd) === this.rawDuration ? null : ( + <Tooltip title={this.timeline?.IsTrimming !== TrimScope.None ? 'Cancel trimming' : 'Edit original timeline'}> <div className="audiobox-button" onPointerDown={this.onResetPointerDown}> - <FontAwesomeIcon icon={this.timeline?.IsTrimming !== TrimScope.None ? 'cancel' : 'arrows-left-right'} size={'1x'} /> + <FontAwesomeIcon icon={this.timeline?.IsTrimming !== TrimScope.None ? 'cancel' : 'arrows-left-right'} size="1x" /> </div> </Tooltip> )} @@ -705,9 +708,11 @@ export class AudioBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { @computed get renderTimeline() { return ( <CollectionStackedTimeline - ref={action((r: CollectionStackedTimeline | null) => (this._stackedTimeline = r))} + ref={action((r: CollectionStackedTimeline | null) => { + this._stackedTimeline = r; + })} + // eslint-disable-next-line react/jsx-props-no-spreading {...this._props} - CollectionFreeFormDocumentView={undefined} dataFieldKey={this.fieldKey} fieldKey={this.annotationKey} dictationKey={this.fieldKey + '_dictation'} @@ -738,10 +743,13 @@ export class AudioBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { // returns the html audio element @computed get audio() { return ( + // eslint-disable-next-line jsx-a11y/media-has-caption <audio ref={this.setRef} 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))}> + onLoadedData={action(() => { + this._ele?.duration && this._ele?.duration !== Infinity && (this.dataDoc[this.fieldKey + '_duration'] = this._ele.duration); + })}> <source src={this.path} type="audio/mpeg" /> Not supported. </audio> @@ -756,3 +764,8 @@ export class AudioBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { ); } } + +Docs.Prototypes.TemplateMap.set(DocumentType.AUDIO, { + layout: { view: AudioBox, dataField: 'data' }, + options: { acl: '', _height: 100, _layout_fitWidth: true, _layout_reflowHorizontal: true, _layout_reflowVertical: true, _layout_nativeDimEditable: true, systemIcon: 'BsFillVolumeUpFill' }, +}); diff --git a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx index 0d0a7c623..685a5aca4 100644 --- a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx +++ b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx @@ -1,23 +1,24 @@ -import { action, makeObservable, observable, trace } from 'mobx'; +import { action, makeObservable, observable } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; -import { OmitKeys, numberRange } from '../../../Utils'; +import { OmitKeys } from '../../../ClientUtils'; +import { numberRange } from '../../../Utils'; import { Doc, DocListCast, Opt } from '../../../fields/Doc'; +import { TransitionTimer } from '../../../fields/DocSymbols'; +import { InkField } from '../../../fields/InkField'; import { List } from '../../../fields/List'; import { listSpec } from '../../../fields/Schema'; import { ComputedField } from '../../../fields/ScriptField'; import { Cast, NumCast, StrCast } from '../../../fields/Types'; import { TraceMobx } from '../../../fields/util'; -import { DocumentManager } from '../../util/DocumentManager'; +import { DragManager } from '../../util/DragManager'; import { ScriptingGlobals } from '../../util/ScriptingGlobals'; -import { SelectionManager } from '../../util/SelectionManager'; import { DocComponent } from '../DocComponent'; -import { StyleProp } from '../StyleProvider'; -import { CollectionFreeFormView } from '../collections/collectionFreeForm/CollectionFreeFormView'; +import { StyleProp } from '../StyleProp'; import './CollectionFreeFormDocumentView.scss'; -import { DocumentView, DocumentViewProps, OpenWhere } from './DocumentView'; +import { DocumentView, DocumentViewProps } from './DocumentView'; import { FieldViewProps } from './FieldView'; -import { TransitionTimer } from '../../../fields/DocSymbols'; +import { OpenWhere } from './OpenWhere'; /// Ugh, typescript has no run-time way of iterating through the keys of an interface. so we need /// manaully keep this list of keys in synch wih the fields of the freeFormProps interface @@ -37,16 +38,18 @@ interface freeFormProps { highlight?: boolean; transition?: string; } + export interface CollectionFreeFormDocumentViewProps extends DocumentViewProps { RenderCutoffProvider: (doc: Doc) => boolean; - CollectionFreeFormView: CollectionFreeFormView; + isAnyChildContentActive: () => boolean; + parent: any; } @observer export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeFormDocumentViewProps & freeFormProps>() { get displayName() { // this makes mobx trace() statements more descriptive return 'CollectionFreeFormDocumentView(' + this.Document.title + ')'; } // prettier-ignore - public static CollectionFreeFormDocViewClassName = 'collectionFreeFormDocumentView-container'; + public static CollectionFreeFormDocViewClassName = DragManager.dragClassName; public static animFields: { key: string; val?: number }[] = [ { key: 'x' }, { key: 'y' }, @@ -62,6 +65,9 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF ]; // fields that are configured to be animatable using animation frames 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 + public static from(dv?: DocumentView): CollectionFreeFormDocumentView | undefined { + return dv?._props.parent instanceof CollectionFreeFormDocumentView ? dv._props.parent : undefined; + } constructor(props: CollectionFreeFormDocumentViewProps & freeFormProps) { super(props); @@ -96,7 +102,9 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF if (this.props.transition && !this.Document[TransitionTimer]) { const num = Number(this.props.transition.match(/([0-9.]+)s/)?.[1]) * 1000 || Number(this.props.transition.match(/([0-9.]+)ms/)?.[1]); this.Document[TransitionTimer] = setTimeout( - action(() => (this.Document[TransitionTimer] = this.Transition = undefined)), + action(() => { + this.Document[TransitionTimer] = this.Transition = undefined; + }), num ); } @@ -104,10 +112,13 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF componentDidUpdate(prevProps: Readonly<React.PropsWithChildren<CollectionFreeFormDocumentViewProps & freeFormProps>>) { super.componentDidUpdate(prevProps); - this.WrapperKeys.forEach(action(keys => ((this as any)[keys.upper] = (this.props as any)[keys.lower]))); + this.WrapperKeys.forEach( + action(keys => { + (this as any)[keys.upper] = (this.props as any)[keys.lower]; + }) + ); } - CollectionFreeFormView = this.props.CollectionFreeFormView; // needed for type checking // this way, downstream code only invalidates when it uses a specific prop, not when any prop changes DataTransition = () => this.Transition || StrCast(this.Document.dataTransition); // prettier-ignore RenderCutoffProvider = this.props.RenderCutoffProvider; // needed for type checking @@ -120,6 +131,7 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF case StyleProp.Opacity: return this.Opacity; // only change the opacity for this specific document, not its children case StyleProp.BackgroundColor: return this.BackgroundColor; case StyleProp.Color: return this.Color; + default: } // prettier-ignore } return this._props.styleProvider?.(doc, props, property); @@ -128,7 +140,10 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF 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); + p[val.key] = Cast(doc[`${val.key}_indexed`], listSpec('number'), fillIn ? [NumCast(doc[val.key], val.val)] : []).reduce( + (prev, v, i) => ((i <= Math.round(time) && v !== undefined) || prev === undefined ? v : prev), + undefined as any as number + ); return p; }, {} as { [val: string]: Opt<number> } @@ -138,7 +153,7 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF 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); + p[val] = Cast(doc[`${val}_indexed`], listSpec('string'), [StrCast(doc[val])]).reduce((prev, v, i) => ((i <= Math.round(time) && v !== undefined) || prev === undefined ? v : prev), undefined as any as string); return p; }, {} as { [val: string]: Opt<string> } @@ -149,7 +164,7 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF const timecode = Math.round(time); Object.keys(vals).forEach(val => { const findexed = Cast(d[`${val}_indexed`], listSpec('string'), []).slice(); - findexed[timecode] = vals[val] as any as string; + findexed[timecode] = vals[val] || ''; d[`${val}_indexed`] = new List<string>(findexed); }); } @@ -158,7 +173,7 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF const timecode = Math.round(time); Object.keys(vals).forEach(val => { const findexed = Cast(d[`${val}_indexed`], listSpec('number'), []).slice(); - findexed[timecode] = vals[val] as any as number; + findexed[timecode] = vals[val] || 0; d[`${val}_indexed`] = new List<number>(findexed); }); } @@ -168,25 +183,50 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF const currentFrame = Cast(doc._currentFrame, 'number', null); if (currentFrame === undefined) { doc._currentFrame = 0; - CollectionFreeFormDocumentView.setupKeyframes(childDocs, 0); + this.setupKeyframes(childDocs, 0); } - CollectionFreeFormView.updateKeyframe(undefined, [...childDocs, doc], currentFrame || 0); + this.updateKeyframe(undefined, [...childDocs, doc], currentFrame || 0); doc._currentFrame = newFrame === undefined ? 0 : Math.max(0, newFrame); } } + public static updateKeyframe(timer: NodeJS.Timeout | undefined, docs: Doc[], time: number) { + const newTimer = DocumentView.SetViewTransition(docs, 'all', 1000, timer, undefined, true); + const timecode = Math.round(time); + docs.forEach(doc => { + this.animFields.forEach(val => { + const findexed = Cast(doc[`${val.key}_indexed`], listSpec('number'), null); + findexed?.length <= timecode + 1 && findexed.push(undefined as any as number); + }); + this.animStringFields.forEach(val => { + const findexed = Cast(doc[`${val}_indexed`], listSpec('string'), null); + findexed?.length <= timecode + 1 && findexed.push(undefined as any as string); + }); + this.animDataFields(doc).forEach(val => { + const findexed = Cast(doc[`${val}_indexed`], listSpec(InkField), null); + findexed?.length <= timecode + 1 && findexed.push(undefined as any); + }); + }); + return newTimer; + } public static setupKeyframes(docs: Doc[], currTimecode: number, makeAppear: boolean = false) { docs.forEach(doc => { if (doc.appearFrame === undefined) doc.appearFrame = currTimecode; - if (!doc['opacity_indexed']) { + if (!doc.opacity_indexed) { // opacity is unlike other fields because it's value should not be undefined before it appears to enable it to fade-in - doc['opacity_indexed'] = new List<number>(numberRange(currTimecode + 1).map(t => (!doc.z && makeAppear && t < NumCast(doc.appearFrame) ? 0 : 1))); + doc.opacity_indexed = new List<number>(numberRange(currTimecode + 1).map(t => (!doc.z && makeAppear && t < NumCast(doc.appearFrame) ? 0 : 1))); } - CollectionFreeFormDocumentView.animFields.forEach(val => (doc[val.key] = ComputedField.MakeInterpolatedNumber(val.key, 'activeFrame', doc, currTimecode, val.val))); - CollectionFreeFormDocumentView.animStringFields.forEach(val => (doc[val] = ComputedField.MakeInterpolatedString(val, 'activeFrame', doc, currTimecode))); - CollectionFreeFormDocumentView.animDataFields(doc).forEach(val => (doc[val] = ComputedField.MakeInterpolatedDataField(val, 'activeFrame', doc, currTimecode))); + CollectionFreeFormDocumentView.animFields.forEach(val => { + doc[val.key] = ComputedField.MakeInterpolatedNumber(val.key, 'activeFrame', doc, currTimecode, val.val); + }); + CollectionFreeFormDocumentView.animStringFields.forEach(val => { + doc[val] = ComputedField.MakeInterpolatedString(val, 'activeFrame', doc, currTimecode); + }); + CollectionFreeFormDocumentView.animDataFields(doc).forEach(val => { + doc[val] = ComputedField.MakeInterpolatedDataField(val, 'activeFrame', doc, currTimecode); + }); const targetDoc = doc; // data fields, like rtf 'text' exist on the data doc, so - //doc !== targetDoc && (targetDoc.embedContainer = doc.embedContainer); // the computed fields don't see the layout doc -- need to copy the embedContainer to the data doc (HACK!!!) and set the activeFrame on the data doc (HACK!!!) + // doc !== targetDoc && (targetDoc.embedContainer = doc.embedContainer); // the computed fields don't see the layout doc -- need to copy the embedContainer to the data doc (HACK!!!) and set the activeFrame on the data doc (HACK!!!) targetDoc.activeFrame = ComputedField.MakeFunction('this.embedContainer?._currentFrame||0'); targetDoc.dataTransition = 'inherit'; }); @@ -197,22 +237,20 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF const containerDocView = this._props.containerViewPath?.().lastElement(); const screenXf = containerDocView?.screenToContentsTransform(); if (screenXf) { - SelectionManager.DeselectAll(); + DocumentView.DeselectAll(); if (topDoc.z) { const spt = screenXf.inverse().transformPoint(NumCast(topDoc.x), NumCast(topDoc.y)); topDoc.z = 0; - topDoc.x = spt[0]; - topDoc.y = spt[1]; + [topDoc.x, topDoc.y] = spt; 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]); topDoc.z = 1; - topDoc.x = fpt[0]; - topDoc.y = fpt[1]; + [topDoc.x, topDoc.y] = fpt; } - setTimeout(() => SelectionManager.SelectView(DocumentManager.Instance.getDocumentView(topDoc, containerDocView), false), 0); + setTimeout(() => DocumentView.SelectView(DocumentView.getDocumentView(topDoc, containerDocView), false), 0); } }; @@ -234,11 +272,12 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF // 'inactive' - this is a group child but it is not active // undefined - this is not activated by a group isGroupActive = () => { - if (this.CollectionFreeFormView.isAnyChildContentActive()) return undefined; + if (this._props.isAnyChildContentActive()) return undefined; const backColor = this.BackgroundColor; const isGroup = this.dataDoc.isGroup && (!backColor || backColor === 'transparent'); return isGroup ? (this._props.isDocumentActive?.() ? 'group' : this._props.isGroupActive?.() ? 'child' : 'inactive') : this._props.isGroupActive?.() ? 'child' : undefined; }; + localRotation = () => this._props.rotation; render() { TraceMobx(); @@ -257,8 +296,11 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF <div style={{ position: 'absolute', width: this.PanelWidth(), height: this.PanelHeight(), background: 'lightGreen' }} /> ) : ( <DocumentView + parent={this} + // eslint-disable-next-line react/jsx-props-no-spreading {...OmitKeys(this._props,this.WrapperKeys.map(val => val.lower)).omit} // prettier-ignore DataTransition={this.DataTransition} + LocalRotation={this.localRotation} CollectionFreeFormDocumentView={this.returnThis} styleProvider={this.styleProvider} ScreenToLocalTransform={this.screenToLocalTransform} @@ -271,6 +313,7 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF ); } } +// eslint-disable-next-line prefer-arrow-callback ScriptingGlobals.add(function gotoFrame(doc: any, newFrame: any) { CollectionFreeFormDocumentView.gotoKeyFrame(doc, newFrame); }); diff --git a/src/client/views/nodes/ComparisonBox.tsx b/src/client/views/nodes/ComparisonBox.tsx index 9ffdc350d..474d54119 100644 --- a/src/client/views/nodes/ComparisonBox.tsx +++ b/src/client/views/nodes/ComparisonBox.tsx @@ -2,24 +2,27 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { action, computed, makeObservable, observable } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; -import { emptyFunction, returnFalse, returnNone, returnZero, setupMoveUpEvents } from '../../../Utils'; +import { returnFalse, returnNone, returnZero, setupMoveUpEvents } from '../../../ClientUtils'; +import { emptyFunction } from '../../../Utils'; import { Doc, Opt } from '../../../fields/Doc'; import { RichTextField } from '../../../fields/RichTextField'; -import { DocCast, NumCast, RTFCast, StrCast } from '../../../fields/Types'; -import { DocUtils, Docs } from '../../documents/Documents'; -import { DragManager, dropActionType } from '../../util/DragManager'; +import { DocCast, NumCast, RTFCast, StrCast, toList } from '../../../fields/Types'; +import { DocUtils } from '../../documents/DocUtils'; +import { DocumentType } from '../../documents/DocumentTypes'; +import { Docs } from '../../documents/Documents'; +import { DragManager } from '../../util/DragManager'; +import { dropActionType } from '../../util/DropActionTypes'; import { undoBatch } from '../../util/UndoManager'; import { ViewBoxAnnotatableComponent, ViewBoxInterface } from '../DocComponent'; -import { StyleProp } from '../StyleProvider'; +import { PinDocView, PinProps } from '../PinFuncs'; +import { StyleProp } from '../StyleProp'; import './ComparisonBox.scss'; import { DocumentView } from './DocumentView'; import { FieldView, FieldViewProps } from './FieldView'; -import { KeyValueBox } from './KeyValueBox'; import { FormattedTextBox } from './formattedText/FormattedTextBox'; -import { PinProps, PresBox } from './trails'; @observer -export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implements ViewBoxInterface { +export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { public static LayoutString(fieldKey: string) { return FieldView.LayoutString(ComparisonBox, fieldKey); } @@ -50,13 +53,14 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>() @undoBatch private internalDrop = (e: Event, dropEvent: DragManager.DropEvent, fieldKey: string) => { if (dropEvent.complete.docDragData) { - const droppedDocs = dropEvent.complete.docDragData?.droppedDocuments; - const added = dropEvent.complete.docDragData.moveDocument?.(droppedDocs, this.Document, (doc: Doc | Doc[]) => this.addDoc(doc instanceof Doc ? doc : doc.lastElement(), fieldKey)); - Doc.SetContainer(droppedDocs.lastElement(), this.dataDoc); + const { droppedDocuments } = dropEvent.complete.docDragData; + const added = dropEvent.complete.docDragData.moveDocument?.(droppedDocuments, this.Document, (doc: Doc | Doc[]) => this.addDoc(toList(doc).lastElement(), fieldKey)); + Doc.SetContainer(droppedDocuments.lastElement(), this.dataDoc); !added && e.preventDefault(); e.stopPropagation(); // prevent parent Doc from registering new position so that it snaps back into place return added; } + return undefined; }; private registerSliding = (e: React.PointerEvent<HTMLDivElement>, targetWidth: number) => { @@ -66,7 +70,7 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>() e, this.onPointerMove, emptyFunction, - action((e, doubleTap) => { + action((moveEv, doubleTap) => { if (doubleTap) { this._isAnyChildContentActive = true; if (!this.dataDoc[this.fieldKey + '_1'] && !this.dataDoc[this.fieldKey]) this.dataDoc[this.fieldKey + '_1'] = DocUtils.copyDragFactory(Doc.UserDoc().emptyNote as Doc); @@ -81,7 +85,9 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>() // on click, animate slider movement to the targetWidth this.layoutDoc[this.clipWidthKey] = (targetWidth * 100) / this._props.PanelWidth(); setTimeout( - action(() => (this._animating = '')), + action(() => { + this._animating = ''; + }), 200 ); }) @@ -107,8 +113,8 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>() }); if (anchor) { if (!addAsAnnotation) anchor.backgroundColor = 'transparent'; - /* addAsAnnotation &&*/ this.addDocument(anchor); - PresBox.pinDocView(anchor, { pinDocLayout: pinProps?.pinDocLayout, pinData: { ...(pinProps?.pinData ?? {}), clippable: true } }, this.Document); + /* addAsAnnotation && */ this.addDocument(anchor); + PinDocView(anchor, { pinDocLayout: pinProps?.pinDocLayout, pinData: { ...(pinProps?.pinData ?? {}), clippable: true } }, this.Document); return anchor; } return this.Document; @@ -135,28 +141,28 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>() setupMoveUpEvents( this, e, - e => { + moveEv => { const de = new DragManager.DocumentDragData([DocCast(this.dataDoc[which])], dropActionType.move); de.moveDocument = (doc: Doc | Doc[], targetCollection: Doc | undefined, addDocument: (doc: Doc | Doc[]) => boolean): boolean => { this.clearDoc(which); return addDocument(doc); }; de.canEmbed = true; - DragManager.StartDocumentDrag([this._closeRef.current!], de, e.clientX, e.clientY); + DragManager.StartDocumentDrag([this._closeRef.current!], de, moveEv.clientX, moveEv.clientY); return true; }, emptyFunction, - e => this.clearDoc(which) + () => this.clearDoc(which) ); }; docStyleProvider = (doc: Opt<Doc>, props: Opt<FieldViewProps>, property: string): any => { if (property === StyleProp.PointerEvents) return 'none'; return this._props.styleProvider?.(doc, props, property); }; - moveDoc1 = (doc: Doc | Doc[], targetCol: Doc | undefined, addDoc: any) => (doc instanceof Doc ? [doc] : doc).reduce((res, doc: Doc) => res && this.moveDoc(doc, addDoc, this.fieldKey + '_1'), true); - moveDoc2 = (doc: Doc | Doc[], targetCol: Doc | undefined, addDoc: any) => (doc instanceof Doc ? [doc] : doc).reduce((res, doc: Doc) => res && this.moveDoc(doc, addDoc, this.fieldKey + '_2'), true); - remDoc1 = (doc: Doc | Doc[]) => (doc instanceof Doc ? [doc] : doc).reduce((res, doc) => res && this.remDoc(doc, this.fieldKey + '_1'), true); - remDoc2 = (doc: Doc | Doc[]) => (doc instanceof Doc ? [doc] : doc).reduce((res, doc) => res && this.remDoc(doc, this.fieldKey + '_2'), true); + moveDoc1 = (docs: Doc | Doc[], targetCol: Doc | undefined, addDoc: any) => toList(docs).reduce((res, doc: Doc) => res && this.moveDoc(doc, addDoc, this.fieldKey + '_1'), true); + moveDoc2 = (docs: Doc | Doc[], targetCol: Doc | undefined, addDoc: any) => toList(docs).reduce((res, doc: Doc) => res && this.moveDoc(doc, addDoc, this.fieldKey + '_2'), true); + remDoc1 = (docs: Doc | Doc[]) => toList(docs).reduce((res, doc) => res && this.remDoc(doc, this.fieldKey + '_1'), true); + remDoc2 = (docs: Doc | Doc[]) => toList(docs).reduce((res, doc) => res && this.remDoc(doc, this.fieldKey + '_2'), true); /** * Tests for whether a comparison box slot (ie, before or after) has renderable text content @@ -164,7 +170,8 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>() * @returns a JSX layout string if a text field is found, othwerise undefined */ testForTextFields = (whichSlot: string) => { - const slotHasText = Doc.Get(this.dataDoc, whichSlot, true) instanceof RichTextField || typeof Doc.Get(this.dataDoc, whichSlot, true) === 'string'; + const slotData = Doc.Get(this.dataDoc, whichSlot, true); + const slotHasText = slotData instanceof RichTextField || typeof slotData === 'string'; const subjectText = RTFCast(this.Document[this.fieldKey])?.Text.trim(); const altText = RTFCast(this.Document[this.fieldKey + '_alternate'])?.Text.trim(); const layoutTemplateString = @@ -180,9 +187,9 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>() // where (this) is replaced by the text in the fieldKey slot abd this.excludeWords is repalced by the conetnts of the excludeWords field // The GPT call will put the "answer" in the second slot of the comparison (eg., text_2) if (whichSlot.endsWith('2') && !layoutTemplateString?.includes(whichSlot)) { - var queryText = altText?.replace('(this)', subjectText); // TODO: this should be done in KeyValueBox.setField but it doesn't know about the fieldKey ... - if (queryText && queryText.match(/\(\(.*\)\)/)) { - KeyValueBox.SetField(this.Document, whichSlot, ':=' + queryText, false); // make the second slot be a computed field on the data doc that calls ChatGpt + const queryText = altText?.replace('(this)', subjectText); // TODO: this should be done in Doc.setField but it doesn't know about the fieldKey ... + if (queryText?.match(/\(\(.*\)\)/)) { + Doc.SetField(this.Document, whichSlot, ':=' + queryText, false); // make the second slot be a computed field on the data doc that calls ChatGpt } } return layoutTemplateString; @@ -190,17 +197,15 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>() _closeRef = React.createRef<HTMLDivElement>(); render() { - const clearButton = (which: string) => { - return ( - <div - ref={this._closeRef} - className={`clear-button ${which}`} - onPointerDown={e => this.closeDown(e, which)} // prevent triggering slider movement in registerSliding - > - <FontAwesomeIcon className={`clear-button ${which}`} icon="times" size="sm" /> - </div> - ); - }; + const clearButton = (which: string) => ( + <div + ref={this._closeRef} + className={`clear-button ${which}`} + onPointerDown={e => this.closeDown(e, which)} // prevent triggering slider movement in registerSliding + > + <FontAwesomeIcon className={`clear-button ${which}`} icon="times" size="sm" /> + </div> + ); /** * Display the Docs in the before/after fields of the comparison. This also supports a GPT flash card use case @@ -211,15 +216,16 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>() const displayDoc = (whichSlot: string) => { const whichDoc = DocCast(this.dataDoc[whichSlot]); const targetDoc = DocCast(whichDoc?.annotationOn, whichDoc); - const layoutTemplateString = targetDoc ? '' : this.testForTextFields(whichSlot); - return targetDoc || layoutTemplateString ? ( + const layoutString = targetDoc ? '' : this.testForTextFields(whichSlot); + return targetDoc || layoutString ? ( <> <DocumentView + // eslint-disable-next-line react/jsx-props-no-spreading {...this._props} - ignoreUsePath={layoutTemplateString ? true : undefined} + ignoreUsePath={layoutString ? true : undefined} renderDepth={this.props.renderDepth + 1} - LayoutTemplateString={layoutTemplateString} - Document={layoutTemplateString ? this.Document : targetDoc} + LayoutTemplateString={layoutString} + Document={layoutString ? this.Document : targetDoc} containerViewPath={this.DocumentView?.().docViewPath} moveDocument={whichSlot.endsWith('1') ? this.moveDoc1 : this.moveDoc2} removeDocument={whichSlot.endsWith('1') ? this.remDoc1 : this.remDoc2} @@ -229,10 +235,10 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>() isDocumentActive={returnFalse} whenChildContentsActiveChanged={this.whenChildContentsActiveChanged} styleProvider={this._isAnyChildContentActive ? this._props.styleProvider : this.docStyleProvider} - hideLinkButton={true} + hideLinkButton pointerEvents={this._isAnyChildContentActive ? undefined : returnNone} /> - {layoutTemplateString ? null : clearButton(whichSlot)} + {layoutString ? null : clearButton(whichSlot)} </> // placeholder image if doc is missing ) : ( <div className="placeholder"> @@ -240,13 +246,11 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>() </div> ); }; - const displayBox = (which: string, index: number, cover: number) => { - return ( - <div className={`${index === 0 ? 'before' : 'after'}Box-cont`} key={which} style={{ width: this._props.PanelWidth() }} onPointerDown={e => this.registerSliding(e, cover)} ref={ele => this.createDropTarget(ele, which, index)}> - {displayDoc(which)} - </div> - ); - }; + const displayBox = (which: string, index: number, cover: number) => ( + <div className={`${index === 0 ? 'before' : 'after'}Box-cont`} key={which} style={{ width: this._props.PanelWidth() }} onPointerDown={e => this.registerSliding(e, cover)} ref={ele => this.createDropTarget(ele, which, index)}> + {displayDoc(which)} + </div> + ); return ( <div className={`comparisonBox${this._props.isContentActive() ? '-interactive' : ''}` /* change className to easily disable/enable pointer events in CSS */}> @@ -269,3 +273,18 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>() ); } } + +Docs.Prototypes.TemplateMap.set(DocumentType.COMPARISON, { + data: '', + layout: { view: ComparisonBox, dataField: 'data' }, + options: { + acl: '', + backgroundColor: 'gray', + dropAction: dropActionType.move, + waitForDoubleClickToClick: 'always', + _layout_reflowHorizontal: true, + _layout_reflowVertical: true, + _layout_nativeDimEditable: true, + systemIcon: 'BsLayoutSplit', + }, +}); diff --git a/src/client/views/nodes/DataVizBox/DataVizBox.tsx b/src/client/views/nodes/DataVizBox/DataVizBox.tsx index 60c5fdba2..15187b4e4 100644 --- a/src/client/views/nodes/DataVizBox/DataVizBox.tsx +++ b/src/client/views/nodes/DataVizBox/DataVizBox.tsx @@ -1,34 +1,37 @@ +/* eslint-disable react/jsx-props-no-spreading */ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { Checkbox } from '@mui/material'; import { Colors, Toggle, ToggleType, Type } from 'browndash-components'; import { IReactionDisposer, ObservableMap, action, computed, makeObservable, observable, reaction, runInAction } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; -import { emptyFunction, returnEmptyString, returnFalse, returnOne, setupMoveUpEvents } from '../../../../Utils'; +import { returnEmptyString, returnFalse, returnOne, setupMoveUpEvents } from '../../../../ClientUtils'; +import { emptyFunction } from '../../../../Utils'; import { Doc, DocListCast, Field, Opt, StrListCast } from '../../../../fields/Doc'; import { InkTool } from '../../../../fields/InkField'; import { List } from '../../../../fields/List'; -import { listSpec } from '../../../../fields/Schema'; import { Cast, CsvCast, DocCast, NumCast, StrCast } from '../../../../fields/Types'; import { CsvField } from '../../../../fields/URLField'; import { TraceMobx } from '../../../../fields/util'; -import { DocUtils, Docs } from '../../../documents/Documents'; -import { DocumentManager } from '../../../util/DocumentManager'; +import { DocUtils } from '../../../documents/DocUtils'; +import { DocumentType } from '../../../documents/DocumentTypes'; +import { Docs } from '../../../documents/Documents'; import { UndoManager, undoable } from '../../../util/UndoManager'; +import { ContextMenu } from '../../ContextMenu'; import { ViewBoxAnnotatableComponent, ViewBoxInterface } from '../../DocComponent'; import { MarqueeAnnotator } from '../../MarqueeAnnotator'; +import { PinProps } from '../../PinFuncs'; import { SidebarAnnos } from '../../SidebarAnnos'; import { AnchorMenu } from '../../pdf/AnchorMenu'; import { GPTPopup, GPTPopupMode } from '../../pdf/GPTPopup/GPTPopup'; import { DocumentView } from '../DocumentView'; -import { FocusViewOptions, FieldView, FieldViewProps } from '../FieldView'; -import { PinProps } from '../trails'; +import { FieldView, FieldViewProps } from '../FieldView'; +import { FocusViewOptions } from '../FocusViewOptions'; import './DataVizBox.scss'; import { Histogram } from './components/Histogram'; import { LineChart } from './components/LineChart'; import { PieChart } from './components/PieChart'; import { TableBox } from './components/TableBox'; -import { Checkbox } from '@mui/material'; -import { ContextMenu } from '../../ContextMenu'; export enum DataVizView { TABLE = 'table', @@ -64,9 +67,9 @@ export class DataVizBox extends ViewBoxAnnotatableComponent<FieldViewProps>() im setupMoveUpEvents( this, e, - action(e => { + action(moveEv => { MarqueeAnnotator.clearAnnotations(this._savedAnnotations); - this._marqueeref.current?.onInitiateSelection([e.clientX, e.clientY]); + this._marqueeref.current?.onInitiateSelection([moveEv.clientX, moveEv.clientY]); return true; }), returnFalse, @@ -97,7 +100,7 @@ export class DataVizBox extends ViewBoxAnnotatableComponent<FieldViewProps>() im // all CSV records in the dataset (that aren't an empty row) @computed.struct get records() { - var records = DataVizBox.dataset.get(CsvCast(this.dataDoc[this.fieldKey]).url.href); + const records = DataVizBox.dataset.get(CsvCast(this.dataDoc[this.fieldKey]).url.href); return records?.filter(record => Object.keys(record).some(key => record[key])) ?? []; } @@ -112,11 +115,15 @@ export class DataVizBox extends ViewBoxAnnotatableComponent<FieldViewProps>() im @computed.struct get axes() { return StrListCast(this.layoutDoc._dataViz_axes); } - selectAxes = (axes: string[]) => (this.layoutDoc._dataViz_axes = new List<string>(axes)); + selectAxes = (axes: string[]) => { + this.layoutDoc._dataViz_axes = new List<string>(axes); + }; @computed.struct get titleCol() { return StrCast(this.layoutDoc._dataViz_titleCol); } - selectTitleCol = (titleCol: string) => (this.layoutDoc._dataViz_titleCol = titleCol); + selectTitleCol = (titleCol: string) => { + this.layoutDoc._dataViz_titleCol = titleCol; + }; @action // pinned / linked anchor doc includes selected rows, graph titles, and graph colors restoreView = (data: Doc) => { @@ -126,7 +133,7 @@ export class DataVizBox extends ViewBoxAnnotatableComponent<FieldViewProps>() im this.layoutDoc.dataViz_histogram_barColors = Field.Copy(data.dataViz_histogram_barColors); this.layoutDoc.dataViz_histogram_defaultColor = data.dataViz_histogram_defaultColor; this.layoutDoc.dataViz_pie_sliceColors = Field.Copy(data.dataViz_pie_sliceColors); - Object.keys(this.layoutDoc).map(key => { + Object.keys(this.layoutDoc).forEach(key => { if (key.startsWith('dataViz_histogram_title') || key.startsWith('dataViz_lineChart_title') || key.startsWith('dataViz_pieChart_title')) { this.layoutDoc['_' + key] = data[key]; } @@ -152,7 +159,7 @@ export class DataVizBox extends ViewBoxAnnotatableComponent<FieldViewProps>() im annotationOn: this.Document, // when we clear selection -> we should have it so chartBox getAnchor returns undefined // this is for when we want the whole doc (so when the chartBox getAnchor returns without a marker) - /*put in some options*/ + /* put in some options */ }); anchor.config_dataViz = this.dataVizView; anchor.config_dataVizAxes = this.axes.length ? new List<string>(this.axes) : undefined; @@ -160,24 +167,21 @@ export class DataVizBox extends ViewBoxAnnotatableComponent<FieldViewProps>() im anchor.dataViz_histogram_barColors = Field.Copy(this.layoutDoc.dataViz_histogram_barColors); anchor.dataViz_histogram_defaultColor = this.layoutDoc.dataViz_histogram_defaultColor; anchor.dataViz_pie_sliceColors = Field.Copy(this.layoutDoc.dataViz_pie_sliceColors); - Object.keys(this.layoutDoc).map(key => { + Object.keys(this.layoutDoc).forEach(key => { if (key.startsWith('dataViz_histogram_title') || key.startsWith('dataViz_lineChart_title') || key.startsWith('dataViz_pieChart_title')) { anchor[key] = this.layoutDoc[key]; } }); this.addDocument(anchor); - //addAsAnnotation && this.addDocument(anchor); + // addAsAnnotation && this.addDocument(anchor); return anchor; }; createNoteAnnotation = () => { - const createFunc = undoable( - action(() => { - const note = this._sidebarRef.current?.anchorMenuClick(this.getAnchor(false), ['latitude', 'longitude', '-linkedTo']); - }), - 'create note annotation' - ); + const createFunc = undoable(() => { + this._sidebarRef.current?.anchorMenuClick(this.getAnchor(false), ['latitude', 'longitude', '-linkedTo']); + }, 'create note annotation'); if (!this.layoutDoc.layout_showSidebar) { this.toggleSidebar(); setTimeout(createFunc); @@ -194,7 +198,7 @@ export class DataVizBox extends ViewBoxAnnotatableComponent<FieldViewProps>() im this.layoutDoc._width = this.layoutDoc._layout_showSidebar ? NumCast(this.layoutDoc._width) * 1.2 : Math.max(20, NumCast(this.layoutDoc._width) - prevWidth); }; @computed get SidebarShown() { - return this.layoutDoc._layout_showSidebar ? true : false; + return !!this.layoutDoc._layout_showSidebar; } @computed get sidebarHandle() { return ( @@ -208,7 +212,7 @@ export class DataVizBox extends ViewBoxAnnotatableComponent<FieldViewProps>() im backgroundColor: this.SidebarShown ? Colors.MEDIUM_BLUE : Colors.BLACK, }} onPointerDown={this.sidebarBtnDown}> - <FontAwesomeIcon style={{ color: Colors.WHITE }} icon={'comment-alt'} size="sm" /> + <FontAwesomeIcon style={{ color: Colors.WHITE }} icon="comment-alt" size="sm" /> </div> ); } @@ -220,7 +224,7 @@ export class DataVizBox extends ViewBoxAnnotatableComponent<FieldViewProps>() im setupMoveUpEvents( this, e, - (e, down, delta) => + (moveEv, down, delta) => runInAction(() => { const localDelta = this._props .ScreenToLocalTransform() @@ -248,7 +252,9 @@ export class DataVizBox extends ViewBoxAnnotatableComponent<FieldViewProps>() im options.didMove = true; this.toggleSidebar(); } - return new Promise<Opt<DocumentView>>(res => DocumentManager.Instance.AddViewRenderedCb(doc, dv => res(dv))); + return new Promise<Opt<DocumentView>>(res => { + DocumentView.addViewRenderedCb(doc, dv => res(dv)); + }); }; @computed get sidebarWidthPercent() { return StrCast(this.layoutDoc._layout_sidebarWidthPercent, '0%'); @@ -268,32 +274,32 @@ export class DataVizBox extends ViewBoxAnnotatableComponent<FieldViewProps>() im if (!DataVizBox.dataset.has(CsvCast(this.dataDoc[this.fieldKey]).url.href)) this.fetchData(); this._disposers.datavis = reaction( () => { - if (this.layoutDoc.dataViz_schemaLive == undefined) this.layoutDoc.dataViz_schemaLive = true; + if (this.layoutDoc.dataViz_schemaLive === undefined) this.layoutDoc.dataViz_schemaLive = true; const getFrom = DocCast(this.layoutDoc.dataViz_asSchema); - const keys = Cast(getFrom?.schema_columnKeys, listSpec('string'))?.filter(key => key != 'text'); - if (!keys) return; - const children = DocListCast(getFrom[Doc.LayoutFieldKey(getFrom)]); - var current: { [key: string]: string }[] = []; + if (!getFrom?.schema_columnKeys) return undefined; + const keys = StrListCast(getFrom?.schema_columnKeys).filter(key => key !== 'text'); + const children = DocListCast(getFrom?.[Doc.LayoutFieldKey(getFrom)]); + const current: { [key: string]: string }[] = []; children .filter(child => child) .forEach(child => { const row: { [key: string]: string } = {}; keys.forEach(key => { - var cell = child[key]; - if (cell && (cell as string)) cell = cell.toString().replace(/\,/g, ''); + let cell = child[key]; + if (cell && (cell as string)) cell = cell.toString().replace(/,/g, ''); row[key] = StrCast(cell); }); current.push(row); }); if (!this.layoutDoc._dataViz_schemaOG) { // makes a copy of the original table for the "live" toggle - let csvRows = []; + const csvRows = []; csvRows.push(keys.join(',')); for (let i = 0; i < children.length - 1; i++) { - let eachRow = []; + const eachRow = []; for (let j = 0; j < keys.length; j++) { - var cell = children[i][keys[j]]; - if (cell && (cell as string)) cell = cell.toString().replace(/\,/g, ''); + let cell = children[i][keys[j]]; + if (cell && (cell as string)) cell = cell.toString().replace(/,/g, ''); eachRow.push(cell); } csvRows.push(eachRow); @@ -307,19 +313,19 @@ export class DataVizBox extends ViewBoxAnnotatableComponent<FieldViewProps>() im } const ogDoc = this.layoutDoc._dataViz_schemaOG as Doc; const ogHref = CsvCast(ogDoc[this.fieldKey]) ? CsvCast(ogDoc[this.fieldKey]).url.href : undefined; - const href = CsvCast(this.Document[this.fieldKey]).url.href; + const { href } = CsvCast(this.Document[this.fieldKey]).url; if (ogHref && !DataVizBox.datasetSchemaOG.has(href)) { // sets original dataset to the var const lastRow = current.pop(); DataVizBox.datasetSchemaOG.set(href, current); current.push(lastRow!); - fetch('/csvData?uri=' + ogHref).then(res => res.json().then(action(res => !res.errno && DataVizBox.datasetSchemaOG.set(href, res)))); + fetch('/csvData?uri=' + ogHref).then(res => res.json().then(action(jsonRes => !jsonRes.errno && DataVizBox.datasetSchemaOG.set(href, jsonRes)))); } return current; }, current => { if (current) { - const href = CsvCast(this.Document[this.fieldKey]).url.href; + const { href } = CsvCast(this.Document[this.fieldKey]).url; if (this.layoutDoc.dataViz_schemaLive) DataVizBox.dataset.set(href, current); else DataVizBox.dataset.set(href, DataVizBox.datasetSchemaOG.get(href)!); } @@ -331,8 +337,8 @@ export class DataVizBox extends ViewBoxAnnotatableComponent<FieldViewProps>() im fetchData = () => { if (!this.Document.dataViz_asSchema) { DataVizBox.dataset.set(CsvCast(this.dataDoc[this.fieldKey]).url.href, []); // assign temporary dataset as a lock to prevent duplicate server requests - fetch('/csvData?uri=' + this.dataUrl?.url.href) // - .then(res => res.json().then(action(res => !res.errno && DataVizBox.dataset.set(CsvCast(this.dataDoc[this.fieldKey]).url.href, res)))); + fetch('/csvData?uri=' + (this.dataUrl?.url.href ?? '')) // + .then(res => res.json().then(action(jsonRes => !jsonRes.errno && DataVizBox.dataset.set(CsvCast(this.dataDoc[this.fieldKey]).url.href, jsonRes)))); } }; @@ -345,7 +351,7 @@ export class DataVizBox extends ViewBoxAnnotatableComponent<FieldViewProps>() im records: this.records, axes: this.axes, titleCol: this.titleCol, - //width: this.SidebarShown? this._props.PanelWidth()*.9/1.2: this._props.PanelWidth() * 0.9, + // width: this.SidebarShown? this._props.PanelWidth()*.9/1.2: this._props.PanelWidth() * 0.9, height: (this._props.PanelHeight() / scale - 32) /* height of 'change view' button */ * 0.9, width: ((this._props.PanelWidth() - this.sidebarWidth()) / scale) * 0.9, margin: { top: 10, right: 25, bottom: 75, left: 45 }, @@ -353,11 +359,13 @@ export class DataVizBox extends ViewBoxAnnotatableComponent<FieldViewProps>() im if (!this.records.length) return 'no data/visualization'; switch (this.dataVizView) { case DataVizView.TABLE: return <TableBox {...sharedProps} docView={this.DocumentView} selectAxes={this.selectAxes} selectTitleCol={this.selectTitleCol}/>; - case DataVizView.LINECHART: return <LineChart {...sharedProps} dataDoc={this.dataDoc} fieldKey={this.fieldKey} ref={r => (this._vizRenderer = r ?? undefined)} vizBox={this} />; - case DataVizView.HISTOGRAM: return <Histogram {...sharedProps} dataDoc={this.dataDoc} fieldKey={this.fieldKey} ref={r => (this._vizRenderer = r ?? undefined)} />; - case DataVizView.PIECHART: return <PieChart {...sharedProps} dataDoc={this.dataDoc} fieldKey={this.fieldKey} ref={r => (this._vizRenderer = r ?? undefined)} + case DataVizView.LINECHART: return <LineChart {...sharedProps} dataDoc={this.dataDoc} fieldKey={this.fieldKey} ref={r => {this._vizRenderer = r ?? undefined;}} vizBox={this} />; + case DataVizView.HISTOGRAM: return <Histogram {...sharedProps} dataDoc={this.dataDoc} fieldKey={this.fieldKey} ref={r => {this._vizRenderer = r ?? undefined;}} />; + case DataVizView.PIECHART: return <PieChart {...sharedProps} dataDoc={this.dataDoc} fieldKey={this.fieldKey} ref={r => {this._vizRenderer = r ?? undefined;}} margin={{ top: 10, right: 15, bottom: 15, left: 15 }} />; + default: } // prettier-ignore + return null; } @action @@ -369,10 +377,13 @@ export class DataVizBox extends ViewBoxAnnotatableComponent<FieldViewProps>() im this._marqueeing = [e.clientX, e.clientY]; const target = e.target as any; if (e.target && (target.className.includes('endOfContent') || (target.parentElement.className !== 'textLayer' && target.parentElement.parentElement?.className !== 'textLayer'))) { + /* empty */ } else { // if textLayer is hit, then we select text instead of using a marquee so clear out the marquee. setTimeout( - action(() => (this._marqueeing = undefined)), + action(() => { + this._marqueeing = undefined; + }), 100 ); // bcz: hack .. anchor menu is setup within MarqueeAnnotator so we need to at least create the marqueeAnnotator even though we aren't using it. @@ -402,34 +413,44 @@ export class DataVizBox extends ViewBoxAnnotatableComponent<FieldViewProps>() im @action changeLiveSchemaCheckbox = () => { - this.layoutDoc.dataViz_schemaLive = !this.layoutDoc.dataViz_schemaLive - } + this.layoutDoc.dataViz_schemaLive = !this.layoutDoc.dataViz_schemaLive; + }; - specificContextMenu = (e: React.MouseEvent): void => { + specificContextMenu = (): void => { const cm = ContextMenu.Instance; const options = cm.findByDescription('Options...'); const optionItems = options && 'subitems' in options ? options.subitems : []; optionItems.push({ description: `Analyze with AI`, event: () => this.askGPT(), icon: 'lightbulb' }); !options && cm.addItem({ description: 'Options...', subitems: optionItems, icon: 'eye' }); - } - - + }; askGPT = action(async () => { GPTPopup.Instance.setSidebarId('data_sidebar'); GPTPopup.Instance.addDoc = this.sidebarAddDocument; - GPTPopup.Instance.setDataJson(""); + GPTPopup.Instance.setDataJson(''); GPTPopup.Instance.setMode(GPTPopupMode.DATA); - let data = DataVizBox.dataset.get(CsvCast(this.dataDoc[this.fieldKey]).url.href); - let input = JSON.stringify(data); + const data = DataVizBox.dataset.get(CsvCast(this.dataDoc[this.fieldKey]).url.href); + const input = JSON.stringify(data); GPTPopup.Instance.setDataJson(input); GPTPopup.Instance.generateDataAnalysis(); }); render() { const scale = this._props.NativeDimScaling?.() || 1; + const toggleBtn = (name: string, type: DataVizView) => ( + <Toggle + text={name} + toggleType={ToggleType.BUTTON} + type={Type.SEC} + color="black" + onClick={() => { + this.layoutDoc._dataViz = type; + }} + toggleStatus={this.layoutDoc._dataViz === type} + /> + ); return !this.records.length ? ( // displays how to get data into the DataVizBox if its empty - <div className="start-message">To create a DataViz box, either import / drag a CSV file into your canvas or copy a data table and use the command 'ctrl + p' to bring the data table to your canvas.</div> + <div className="start-message">To create a DataViz box, either import / drag a CSV file into your canvas or copy a data table and use the command (ctrl + p) to bring the data table to your canvas.</div> ) : ( <div className="dataViz-box" @@ -445,19 +466,19 @@ export class DataVizBox extends ViewBoxAnnotatableComponent<FieldViewProps>() im onWheel={e => e.stopPropagation()} ref={this._mainCont}> <div className="datatype-button"> - <Toggle text={' TABLE '} toggleType={ToggleType.BUTTON} type={Type.SEC} color={'black'} onClick={e => (this.layoutDoc._dataViz = DataVizView.TABLE)} toggleStatus={this.layoutDoc._dataViz === DataVizView.TABLE} /> - <Toggle text={'LINECHART'} toggleType={ToggleType.BUTTON} type={Type.SEC} color={'black'} onClick={e => (this.layoutDoc._dataViz = DataVizView.LINECHART)} toggleStatus={this.layoutDoc._dataViz === DataVizView.LINECHART} /> - <Toggle text={'HISTOGRAM'} toggleType={ToggleType.BUTTON} type={Type.SEC} color={'black'} onClick={e => (this.layoutDoc._dataViz = DataVizView.HISTOGRAM)} toggleStatus={this.layoutDoc._dataViz === DataVizView.HISTOGRAM} /> - <Toggle text={'PIE CHART'} toggleType={ToggleType.BUTTON} type={Type.SEC} color={'black'} onClick={e => (this.layoutDoc._dataViz = DataVizView.PIECHART)} toggleStatus={this.layoutDoc._dataViz == -DataVizView.PIECHART} /> + {toggleBtn(' TABLE ', DataVizView.TABLE)} + {toggleBtn('LINECHART', DataVizView.LINECHART)} + {toggleBtn('HISTOGRAM', DataVizView.HISTOGRAM)} + {toggleBtn('PIE CHART', DataVizView.PIECHART)} </div> - {(this.layoutDoc && this.layoutDoc.dataViz_asSchema)?( - <div className={'displaySchemaLive'}> - <div className={'liveSchema-checkBox'} style={{ width: this._props.width }}> - <Checkbox color="primary" onChange={this.changeLiveSchemaCheckbox} checked={this.layoutDoc.dataViz_schemaLive as boolean} /> - Display Live Updates to Canvas + {this.layoutDoc && this.layoutDoc.dataViz_asSchema ? ( + <div className="displaySchemaLive"> + <div className="liveSchema-checkBox" style={{ width: this._props.width }}> + <Checkbox color="primary" onChange={this.changeLiveSchemaCheckbox} checked={this.layoutDoc.dataViz_schemaLive as boolean} /> + Display Live Updates to Canvas + </div> </div> - </div> ) : null} {this.renderVizView} @@ -470,7 +491,7 @@ export class DataVizBox extends ViewBoxAnnotatableComponent<FieldViewProps>() im Document={this.Document} layoutDoc={this.layoutDoc} dataDoc={this.dataDoc} - usePanelWidth={true} + usePanelWidth showSidebar={this.SidebarShown} nativeWidth={NumCast(this.layoutDoc._nativeWidth)} whenChildContentsActiveChanged={this.whenChildContentsActiveChanged} @@ -504,3 +525,18 @@ export class DataVizBox extends ViewBoxAnnotatableComponent<FieldViewProps>() im ); } } +Docs.Prototypes.TemplateMap.set(DocumentType.DATAVIZ, { + layout: { view: DataVizBox, dataField: 'data' }, + options: { + acl: '', + dataViz_title: '', + dataViz_line: '', + dataViz_pie: '', + dataViz_histogram: '', + dataViz: 'table', + _layout_fitWidth: true, + _layout_reflowHorizontal: true, + _layout_reflowVertical: true, + _layout_nativeDimEditable: true, + }, +}); diff --git a/src/client/views/nodes/DataVizBox/SchemaCSVPopUp.tsx b/src/client/views/nodes/DataVizBox/SchemaCSVPopUp.tsx index 24023077f..60bc8df18 100644 --- a/src/client/views/nodes/DataVizBox/SchemaCSVPopUp.tsx +++ b/src/client/views/nodes/DataVizBox/SchemaCSVPopUp.tsx @@ -1,9 +1,12 @@ +/* eslint-disable jsx-a11y/label-has-associated-control */ +/* eslint-disable jsx-a11y/alt-text */ import { IconButton } from 'browndash-components'; import { action, makeObservable, observable } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; import { CgClose } from 'react-icons/cg'; -import { Utils, emptyFunction, setupMoveUpEvents } from '../../../../Utils'; +import { ClientUtils, setupMoveUpEvents } from '../../../../ClientUtils'; +import { emptyFunction } from '../../../../Utils'; import { Doc } from '../../../../fields/Doc'; import { StrCast } from '../../../../fields/Types'; import { DragManager } from '../../../util/DragManager'; @@ -14,56 +17,52 @@ interface SchemaCSVPopUpProps {} @observer export class SchemaCSVPopUp extends React.Component<SchemaCSVPopUpProps> { + // eslint-disable-next-line no-use-before-define static Instance: SchemaCSVPopUp; - @observable - public dataVizDoc: Doc | undefined = undefined; + @observable public dataVizDoc: Doc | undefined = undefined; + @observable public view: DocumentView | undefined = undefined; + @observable public target: Doc | undefined = undefined; + @observable public visible: boolean = false; + + constructor(props: SchemaCSVPopUpProps) { + super(props); + makeObservable(this); + SchemaCSVPopUp.Instance = this; + } + @action public setDataVizDoc = (doc: Doc) => { this.dataVizDoc = doc; }; - @observable - public view: DocumentView | undefined = undefined; @action public setView = (docView: DocumentView) => { this.view = docView; }; - @observable - public target: Doc | undefined = undefined; @action public setTarget = (doc: Doc) => { this.target = doc; }; - @observable - public visible: boolean = false; @action public setVisible = (vis: boolean) => { this.visible = vis; }; - constructor(props: SchemaCSVPopUpProps) { - super(props); - makeObservable(this); - SchemaCSVPopUp.Instance = this; - } - - dataBox = () => { - return ( - <div style={{ display: 'flex', flexDirection: 'column', gap: '1rem' }}> - {this.heading('Schema Table as Data Visualization Doc')} - <div className="image-content-wrapper"> - <div className="img-wrapper"> - <div className="img-container" onPointerDown={e => this.drag(e)}> - <img width={150} height={150} src={'/assets/dataVizBox.png'} /> - </div> + dataBox = () => ( + <div style={{ display: 'flex', flexDirection: 'column', gap: '1rem' }}> + {this.heading('Schema Table as Data Visualization Doc')} + <div className="image-content-wrapper"> + <div className="img-wrapper"> + <div className="img-container" onPointerDown={e => this.drag(e)}> + <img width={150} height={150} src="/assets/dataVizBox.png" /> </div> </div> </div> - ); - }; + </div> + ); heading = (headingText: string) => ( <div className="summary-heading"> @@ -78,24 +77,22 @@ export class SchemaCSVPopUp extends React.Component<SchemaCSVPopUpProps> { setupMoveUpEvents( {}, e, - e => { + moveEv => { const sourceAnchorCreator = () => this.dataVizDoc!; - const targetCreator = (annotationOn: Doc | undefined) => { + const targetCreator = () => { const embedding = Doc.MakeEmbedding(this.dataVizDoc!); return embedding; }; - if (this.view && sourceAnchorCreator && !Utils.isClick(e.clientX, e.clientY, downX, downY, Date.now())) { - DragManager.StartAnchorAnnoDrag(e.target instanceof HTMLElement ? [e.target] : [], new DragManager.AnchorAnnoDragData(this.view, sourceAnchorCreator, targetCreator), downX, downY, { - dragComplete: e => { - this.setVisible(false); - }, + if (this.view && sourceAnchorCreator && !ClientUtils.isClick(moveEv.clientX, moveEv.clientY, downX, downY, Date.now())) { + DragManager.StartAnchorAnnoDrag(moveEv.target instanceof HTMLElement ? [moveEv.target] : [], new DragManager.AnchorAnnoDragData(this.view, sourceAnchorCreator, targetCreator), downX, downY, { + dragComplete: () => this.setVisible(false), }); return true; } return false; }, emptyFunction, - action(e => {}) + action(() => {}) ); }; diff --git a/src/client/views/nodes/DataVizBox/components/Histogram.tsx b/src/client/views/nodes/DataVizBox/components/Histogram.tsx index 6672603f3..79b3e9541 100644 --- a/src/client/views/nodes/DataVizBox/components/Histogram.tsx +++ b/src/client/views/nodes/DataVizBox/components/Histogram.tsx @@ -12,7 +12,7 @@ import { Cast, DocCast, StrCast } from '../../../../../fields/Types'; import { Docs } from '../../../../documents/Documents'; import { undoable } from '../../../../util/UndoManager'; import { ObservableReactComponent } from '../../../ObservableReactComponent'; -import { PinProps, PresBox } from '../../trails'; +import { PinProps, PinDocView } from '../../../PinFuncs'; import { scaleCreatorNumerical, yAxisCreator } from '../utils/D3Utils'; import './Chart.scss'; @@ -63,14 +63,13 @@ export class Histogram extends ObservableReactComponent<HistogramProps> { @computed get _histogramData() { if (this._props.axes.length < 1) return []; if (this._props.axes.length < 2) { - var ax0 = this._props.axes[0]; - if (!/[A-Za-z-:]/.test(this._props.records[0][ax0])){ + const ax0 = this._props.axes[0]; + if (!/[A-Za-z-:]/.test(this._props.records[0][ax0])) { this.numericalXData = true; } return this._tableData.map(record => ({ [ax0]: record[this._props.axes[0]] })); } - var ax0 = this._props.axes[0]; - var ax1 = this._props.axes[1]; + const [ax0, ax1] = this._props.axes; if (!/[A-Za-z-:]/.test(this._props.records[0][ax0])) { this.numericalXData = true; } @@ -81,11 +80,11 @@ export class Histogram extends ObservableReactComponent<HistogramProps> { } @computed get defaultGraphTitle() { - var ax0 = this._props.axes[0]; - var ax1 = this._props.axes.length > 1 ? this._props.axes[1] : undefined; + const [ax0, ax1] = this._props.axes; if (this._props.axes.length < 2 || !ax1 || !/\d/.test(this._props.records[0][ax1]) || !this.numericalYData) { return ax0 + ' Histogram'; - } else return ax0 + ' by ' + ax1 + ' Histogram'; + } + return ax0 + ' by ' + ax1 + ' Histogram'; } @computed get parentViz() { @@ -111,14 +110,13 @@ export class Histogram extends ObservableReactComponent<HistogramProps> { ); } - @action - restoreView = (data: Doc) => {}; + restoreView = () => {}; // create a document anchor that stores whatever is needed to reconstruct the viewing state (selection,zoom,etc) getAnchor = (pinProps?: PinProps) => { const anchor = Docs.Create.ConfigDocument({ title: 'histogram doc selection' + this._currSelected, }); - PresBox.pinDocView(anchor, { pinDocLayout: pinProps?.pinDocLayout, pinData: pinProps?.pinData }, this._props.Document); + PinDocView(anchor, { pinDocLayout: pinProps?.pinDocLayout, pinData: pinProps?.pinData }, this._props.Document); return anchor; }; @@ -132,36 +130,36 @@ export class Histogram extends ObservableReactComponent<HistogramProps> { // cleans data by converting numerical data to numbers and taking out empty cells data = (dataSet: any) => { - var validData = dataSet.filter((d: { [x: string]: unknown }) => !Object.keys(dataSet[0]).some(key => !d[key] || Number.isNaN(d[key]))); + const validData = dataSet.filter((d: { [x: string]: unknown }) => !Object.keys(dataSet[0]).some(key => !d[key] || isNaN(d[key]))); const field = dataSet[0] ? Object.keys(dataSet[0])[0] : undefined; return !field ? [] : validData.map((d: { [x: string]: any }) => !this.numericalXData // ? d[field] - : +d[field!].replace(/\$/g, '').replace(/\%/g, '').replace(/\</g, '') + : +d[field!].replace(/\$/g, '').replace(/%/g, '').replace(/</g, '') ); }; // outlines the bar selected / hovered over highlightSelectedBar = (changeSelectedVariables: boolean, svg: any, eachRectWidth: any, pointerX: any, xAxisTitle: any, yAxisTitle: any, histDataSet: any) => { - var sameAsCurrent: boolean; - var barCounter = -1; + let sameAsCurrent: boolean; + let barCounter = -1; const selected = svg.selectAll('.histogram-bar').filter((d: any) => { barCounter++; // uses the order of bars and width of each bar to find which one the pointer is over if (barCounter * eachRectWidth <= pointerX && pointerX <= (barCounter + 1) * eachRectWidth) { - var showSelected = this.numericalYData - ? this._histogramData.filter((data: { [x: string]: any }) => StrCast(data[xAxisTitle]).replace(/\$/g, '').replace(/\%/g, '').replace(/\</g, '') == d[0])[0] - : histDataSet.filter((data: { [x: string]: any }) => data[xAxisTitle].replace(/\$/g, '').replace(/\%/g, '').replace(/\</g, '') == d[0])[0]; + let showSelected = this.numericalYData + ? this._histogramData.filter((data: { [x: string]: any }) => StrCast(data[xAxisTitle]).replace(/$/g, '').replace(/%/g, '').replace(/</g, '') === d[0])[0] + : histDataSet.filter((data: { [x: string]: any }) => data[xAxisTitle].replace(/$/g, '').replace(/%/g, '').replace(/</g, '') === d[0])[0]; if (this.numericalXData) { // calculating frequency - if (d[0] && d[1] && d[0] != d[1]) { + if (d[0] && d[1] && d[0] !== d[1]) { showSelected = { [xAxisTitle]: d3.min(d) + ' to ' + d3.max(d), frequency: d.length }; } else if (!this.numericalYData) showSelected = { [xAxisTitle]: showSelected[xAxisTitle], frequency: d.length }; } if (changeSelectedVariables) { // for when a bar is selected - not just hovered over - sameAsCurrent = this._currSelected ? showSelected[xAxisTitle] == this._currSelected![xAxisTitle] && showSelected[yAxisTitle] == this._currSelected![yAxisTitle] : false; + sameAsCurrent = this._currSelected ? showSelected[xAxisTitle] === this._currSelected![xAxisTitle] && showSelected[yAxisTitle] === this._currSelected![yAxisTitle] : false; this._currSelected = sameAsCurrent ? undefined : showSelected; this.selectedData = sameAsCurrent ? undefined : d; } else this.hoverOverData = d; @@ -184,16 +182,16 @@ export class Histogram extends ObservableReactComponent<HistogramProps> { const xAxisTitle = Object.keys(dataSet[0])[0]; const yAxisTitle = this.numericalYData ? Object.keys(dataSet[0])[1] : 'frequency'; const uniqueArr: unknown[] = [...new Set(data)]; - var numBins = this.numericalXData && Number.isInteger(data[0]) ? this.rangeVals.xMax! - this.rangeVals.xMin! : uniqueArr.length; - var translateXAxis = !this.numericalXData || numBins < this.maxBins ? width / (numBins + 1) / 2 : 0; + let numBins = this.numericalXData && Number.isInteger(data[0]) ? this.rangeVals.xMax! - this.rangeVals.xMin! : uniqueArr.length; + let translateXAxis = !this.numericalXData || numBins < this.maxBins ? width / (numBins + 1) / 2 : 0; if (numBins > this.maxBins) numBins = this.maxBins; const startingPoint = this.numericalXData ? this.rangeVals.xMin! : 0; const endingPoint = this.numericalXData ? this.rangeVals.xMax! : numBins; // converts data into Objects - var histDataSet = dataSet.filter((d: { [x: string]: unknown }) => !Object.keys(dataSet[0]).some(key => !d[key] || Number.isNaN(d[key]))); + let histDataSet = dataSet.filter((d: { [x: string]: unknown }) => !Object.keys(dataSet[0]).some(key => !d[key] || isNaN(d[key]))); if (!this.numericalXData) { - var histStringDataSet: { [x: string]: unknown }[] = []; + const histStringDataSet: { [x: string]: unknown }[] = []; if (this.numericalYData) { for (let i = 0; i < dataSet.length; i++) { histStringDataSet.push({ [yAxisTitle]: dataSet[i][yAxisTitle], [xAxisTitle]: dataSet[i][xAxisTitle] }); @@ -203,15 +201,15 @@ export class Histogram extends ObservableReactComponent<HistogramProps> { histStringDataSet.push({ [yAxisTitle]: 0, [xAxisTitle]: uniqueArr[i] }); } for (let i = 0; i < data.length; i++) { - let barData = histStringDataSet.filter(each => each[xAxisTitle] == data[i]); - histStringDataSet.filter(each => each[xAxisTitle] == data[i])[0][yAxisTitle] = Number(barData[0][yAxisTitle]) + 1; + const barData = histStringDataSet.filter(each => each[xAxisTitle] === data[i]); + histStringDataSet.filter(each => each[xAxisTitle] === data[i])[0][yAxisTitle] = Number(barData[0][yAxisTitle]) + 1; } } histDataSet = histStringDataSet; } // initial graph and binning data for histogram - var svg = (this._histogramSvg = d3 + const svg = (this._histogramSvg = d3 .select(this._histogramRef.current) .append('svg') .attr('class', 'graph') @@ -219,23 +217,21 @@ export class Histogram extends ObservableReactComponent<HistogramProps> { .attr('height', height + this._props.margin.top + this._props.margin.bottom) .append('g') .attr('transform', 'translate(' + this._props.margin.left + ',' + this._props.margin.top + ')')); - var x = d3 + let x = d3 .scaleLinear() .domain(this.numericalXData ? [startingPoint!, endingPoint!] : [0, numBins]) .range([0, width]); - var histogram = d3 + const histogram = d3 .histogram() - .value(function (d) { - return d; - }) + .value(d => d) .domain([startingPoint!, endingPoint!]) .thresholds(x.ticks(numBins)); - var bins = histogram(data); - var eachRectWidth = width / bins.length; - var graphStartingPoint = bins[0].x1 && bins[1] ? bins[0].x1! - (bins[1].x1! - bins[1].x0!) : 0; + const bins = histogram(data); + let eachRectWidth = width / bins.length; + const graphStartingPoint = bins[0].x1 && bins[1] ? bins[0].x1! - (bins[1].x1! - bins[1].x0!) : 0; bins[0].x0 = graphStartingPoint; x = x.domain([graphStartingPoint, endingPoint]).range([0, Number.isInteger(this.rangeVals.xMin!) ? width - eachRectWidth : width]); - var xAxis; + let xAxis; // more calculations based on bins // x-axis @@ -244,9 +240,9 @@ export class Histogram extends ObservableReactComponent<HistogramProps> { // uniqueArr.sort() histDataSet.sort(); for (let i = 0; i < data.length; i++) { - var index = 0; + let index = 0; for (let j = 0; j < uniqueArr.length; j++) { - if (uniqueArr[j] == data[i]) { + if (uniqueArr[j] === data[i]) { index = j; } } @@ -254,7 +250,9 @@ export class Histogram extends ObservableReactComponent<HistogramProps> { } bins.pop(); eachRectWidth = width / bins.length; - bins.forEach(d => (d.x0 = d.x0!)); + bins.forEach(d => { + d.x0 = d.x0!; + }); xAxis = d3 .axisBottom(x) .ticks(bins.length > 1 ? bins.length - 1 : 1) @@ -264,12 +262,12 @@ export class Histogram extends ObservableReactComponent<HistogramProps> { x.domain([0, bins.length - 1]); translateXAxis = eachRectWidth / 2; } else { - var allSame = true; - for (var i = 0; i < bins.length; i++) { + let allSame = true; + for (let i = 0; i < bins.length; i++) { if (bins[i] && bins[i][0]) { - var compare = bins[i][0]; + const compare = bins[i][0]; for (let j = 1; j < bins[i].length; j++) { - if (bins[i][j] != compare) allSame = false; + if (bins[i][j] !== compare) allSame = false; } } } @@ -278,8 +276,8 @@ export class Histogram extends ObservableReactComponent<HistogramProps> { eachRectWidth = width / bins.length; } else { eachRectWidth = width / (bins.length + 1); - var tickDiff = bins.length >= 2 ? bins[bins.length - 2].x1! - bins[bins.length - 2].x0! : 0; - var curDomain = x.domain(); + const tickDiff = bins.length >= 2 ? bins[bins.length - 2].x1! - bins[bins.length - 2].x0! : 0; + const curDomain = x.domain(); x.domain([curDomain[0], curDomain[0] + tickDiff * bins.length]); } @@ -287,16 +285,13 @@ export class Histogram extends ObservableReactComponent<HistogramProps> { x.range([0, width - eachRectWidth]); } // y-axis - const maxFrequency = this.numericalYData - ? d3.max(histDataSet, function (d: any) { - return d[yAxisTitle] ? Number(d[yAxisTitle]!.replace(/\$/g, '').replace(/\%/g, '').replace(/\</g, '')) : 0; - }) - : d3.max(bins, function (d) { - return d.length; - }); - var y = d3.scaleLinear().range([height, 0]); + const maxFrequency = this.numericalYData ? + d3.max(histDataSet, (d: any) => (d[yAxisTitle] ? Number(d[yAxisTitle]!.replace(/\$/g, '') + .replace(/%/g, '').replace(/</g, '')) : 0)) : + d3.max(bins, d => d.length); // prettier-ignore + const y = d3.scaleLinear().range([height, 0]); y.domain([0, +maxFrequency!]); - var yAxis = d3.axisLeft(y).ticks(maxFrequency!); + const yAxis = d3.axisLeft(y).ticks(maxFrequency!); if (this.numericalYData) { const yScale = scaleCreatorNumerical(0, Number(maxFrequency), height, 0); yAxisCreator(svg.append('g'), width, yScale); @@ -311,18 +306,17 @@ export class Histogram extends ObservableReactComponent<HistogramProps> { const onPointClick = action((e: any) => this.highlightSelectedBar(true, svg, eachRectWidth, d3.pointer(e)[0], xAxisTitle, yAxisTitle, histDataSet)); const onHover = action((e: any) => { this.highlightSelectedBar(false, svg, eachRectWidth, d3.pointer(e)[0], xAxisTitle, yAxisTitle, histDataSet); + // eslint-disable-next-line no-use-before-define updateHighlights(); }); - const mouseOut = action((e: any) => { + const mouseOut = action(() => { this.hoverOverData = undefined; + // eslint-disable-next-line no-use-before-define updateHighlights(); }); const updateHighlights = () => { - const hoverOverBar = this.hoverOverData; - const selectedData = this.selectedData; - svg.selectAll('rect').attr('class', function (d: any) { - return (hoverOverBar && hoverOverBar[0] == d[0]) || (selectedData && selectedData[0] == d[0]) ? 'histogram-bar hover' : 'histogram-bar'; - }); + const { hoverOverData: hoverOverBar, selectedData } = this; + svg.selectAll('rect').attr('class', (d: any) => ((hoverOverBar && hoverOverBar[0] === d[0]) || (selectedData && selectedData[0] === d[0]) ? 'histogram-bar hover' : 'histogram-bar')); }; svg.on('click', onPointClick).on('mouseover', onHover).on('mouseout', mouseOut); @@ -332,7 +326,7 @@ export class Histogram extends ObservableReactComponent<HistogramProps> { .style('text-anchor', 'middle') .text(xAxisTitle); svg.append('text') - .attr('transform', 'rotate(-90)' + ' ' + 'translate( 0, ' + -10 + ')') + .attr('transform', 'rotate(-90) translate( 0, ' + -10 + ')') .attr('x', -(height / 2)) .attr('y', -20) .style('text-anchor', 'middle') @@ -340,7 +334,7 @@ export class Histogram extends ObservableReactComponent<HistogramProps> { d3.format('.0f'); // draw bars - var selected = this.selectedData; + const selected = this.selectedData; svg.selectAll('rect') .data(bins) .enter() @@ -348,49 +342,34 @@ export class Histogram extends ObservableReactComponent<HistogramProps> { .attr( 'transform', this.numericalYData - ? function (d) { - const eachData = histDataSet.filter((data: { [x: string]: number }) => { - return data[xAxisTitle] == d[0]; - }); - const length = eachData.length ? eachData[0][yAxisTitle].replace(/\$/g, '').replace(/\%/g, '').replace(/\</g, '') : 0; + ? d => { + const eachData = histDataSet.filter((hData: { [x: string]: number }) => hData[xAxisTitle] === d[0]); + const length = eachData.length ? eachData[0][yAxisTitle].replace(/\$/g, '').replace(/%/g, '').replace(/</g, '') : 0; return 'translate(' + x(d.x0!) + ',' + y(length) + ')'; } - : function (d) { - return 'translate(' + x(d.x0!) + ',' + y(d.length) + ')'; - } + : d => 'translate(' + x(d.x0!) + ',' + y(d.length) + ')' ) .attr( 'height', this.numericalYData - ? function (d) { - const eachData = histDataSet.filter((data: { [x: string]: number }) => { - return data[xAxisTitle] == d[0]; - }); - const length = eachData.length ? eachData[0][yAxisTitle].replace(/\$/g, '').replace(/\%/g, '').replace(/\</g, '') : 0; + ? d => { + const eachData = histDataSet.filter((hData: { [x: string]: number }) => hData[xAxisTitle] === d[0]); + const length = eachData.length ? eachData[0][yAxisTitle].replace(/\$/g, '').replace(/%/g, '').replace(/</g, '') : 0; return height - y(length); } - : function (d) { - return height - y(d.length); - } + : d => height - y(d.length) ) .attr('width', eachRectWidth) - .attr( - 'class', - selected - ? function (d) { - return selected && selected[0] === d[0] ? 'histogram-bar hover' : 'histogram-bar'; - } - : function (d) { - return 'histogram-bar'; - } - ) + .attr('class', selected ? d => (selected && selected[0] === d[0] ? 'histogram-bar hover' : 'histogram-bar') : () => 'histogram-bar') .attr('fill', d => { - var barColor; + let barColor; const barColors = StrListCast(this._props.layoutDoc.dataViz_histogram_barColors).map(each => each.split('::')); barColors.forEach(each => { - if (d[0] && d[0].toString() && each[0] == d[0].toString()) barColor = each[1]; + // eslint-disable-next-line prefer-destructuring + if (d[0] && d[0].toString() && each[0] === d[0].toString()) barColor = each[1]; else { const range = StrCast(each[0]).split(' to '); + // eslint-disable-next-line prefer-destructuring if (Number(range[0]) <= d[0] && d[0] <= Number(range[1])) barColor = each[1]; } }); @@ -400,7 +379,7 @@ export class Histogram extends ObservableReactComponent<HistogramProps> { @action changeSelectedColor = (color: string) => { this.curBarSelected.attr('fill', color); - const barName = StrCast(this._currSelected[this._props.axes[0]].replace(/\$/g, '').replace(/\%/g, '').replace(/\</g, '')); + const barName = StrCast(this._currSelected[this._props.axes[0]].replace(/\$/g, '').replace(/%/g, '').replace(/</g, '')); const barColors = Cast(this._props.layoutDoc.dataViz_histogram_barColors, listSpec('string'), null); barColors.forEach(each => each.split('::')[0] === barName && barColors.splice(barColors.indexOf(each), 1)); @@ -409,22 +388,24 @@ export class Histogram extends ObservableReactComponent<HistogramProps> { @action eraseSelectedColor = () => { this.curBarSelected.attr('fill', this._props.layoutDoc.dataViz_histogram_defaultColor); - const barName = StrCast(this._currSelected[this._props.axes[0]].replace(/\$/g, '').replace(/\%/g, '').replace(/\</g, '')); + const barName = StrCast(this._currSelected[this._props.axes[0]].replace(/\$/g, '').replace(/%/g, '').replace(/</g, '')); const barColors = Cast(this._props.layoutDoc.dataViz_histogram_barColors, listSpec('string'), null); barColors.forEach(each => each.split('::')[0] === barName && barColors.splice(barColors.indexOf(each), 1)); }; updateBarColors = () => { - var svg = this._histogramSvg; + const svg = this._histogramSvg; if (svg) svg.selectAll('rect').attr('fill', (d: any) => { - var barColor; + let barColor; const barColors = StrListCast(this._props.layoutDoc.dataViz_histogram_barColors).map(each => each.split('::')); barColors.forEach(each => { - if (d[0] && d[0].toString() && each[0] == d[0].toString()) barColor = each[1]; + // eslint-disable-next-line prefer-destructuring + if (d[0] && d[0].toString() && each[0] === d[0].toString()) barColor = each[1]; else { const range = StrCast(each[0]).split(' to '); + // eslint-disable-next-line prefer-destructuring if (Number(range[0]) <= d[0] && d[0] <= Number(range[1])) barColor = each[1]; } }); @@ -435,39 +416,41 @@ export class Histogram extends ObservableReactComponent<HistogramProps> { render() { this.updateBarColors(); this._histogramData; - var curSelectedBarName = ''; - var titleAccessor: any = 'dataViz_histogram_title'; - if (this._props.axes.length == 2) titleAccessor = titleAccessor + this._props.axes[0] + '-' + this._props.axes[1]; - else if (this._props.axes.length > 0) titleAccessor = titleAccessor + this._props.axes[0]; + let curSelectedBarName = ''; + let titleAccessor: any = 'dataViz_histogram_title'; + if (this._props.axes.length === 2) titleAccessor = titleAccessor + this._props.axes[0] + '-' + this._props.axes[1]; + else if (this._props.axes.length > 0) titleAccessor += this._props.axes[0]; if (!this._props.layoutDoc[titleAccessor]) this._props.layoutDoc[titleAccessor] = this.defaultGraphTitle; if (!this._props.layoutDoc.dataViz_histogram_defaultColor) this._props.layoutDoc.dataViz_histogram_defaultColor = '#69b3a2'; if (!this._props.layoutDoc.dataViz_histogram_barColors) this._props.layoutDoc.dataViz_histogram_barColors = new List<string>(); - var selected = 'none'; + let selected = 'none'; if (this._currSelected) { - curSelectedBarName = StrCast(this._currSelected![this._props.axes[0]].replace(/\$/g, '').replace(/\%/g, '').replace(/\</g, '')); + curSelectedBarName = StrCast(this._currSelected![this._props.axes[0]].replace(/\$/g, '').replace(/%/g, '').replace(/</g, '')); selected = '{ '; - Object.keys(this._currSelected).forEach(key => + Object.keys(this._currSelected).forEach(key => { key // ? (selected += key + ': ' + this._currSelected[key] + ', ') - : '' - ); + : ''; + }); selected = selected.substring(0, selected.length - 2) + ' }'; - if (this._props.titleCol!="" && (!this._currSelected["frequency"] || this._currSelected["frequency"]<10)){ - selected+= "\n" + this._props.titleCol + ": " + if (this._props.titleCol !== '' && (!this._currSelected.frequency || this._currSelected.frequency < 10)) { + selected += '\n' + this._props.titleCol + ': '; this._tableData.forEach(each => { - if (this._currSelected[this._props.axes[0]]==each[this._props.axes[0]]) { - if (this._props.axes[1]){ - if (this._currSelected[this._props.axes[1]]==each[this._props.axes[1]]) selected+= each[this._props.titleCol] + ", "; - } - else selected+= each[this._props.titleCol] + ", "; + if (this._currSelected[this._props.axes[0]] === each[this._props.axes[0]]) { + if (this._props.axes[1]) { + if (this._currSelected[this._props.axes[1]] === each[this._props.axes[1]]) selected += each[this._props.titleCol] + ', '; + } else selected += each[this._props.titleCol] + ', '; } - }) - selected = selected.slice(0,-1).slice(0,-1); + }); + selected = selected.slice(0, -1).slice(0, -1); } } - var selectedBarColor; - var barColors = StrListCast(this._props.layoutDoc.histogramBarColors).map(each => each.split('::')); - barColors.forEach(each => each[0] === curSelectedBarName && (selectedBarColor = each[1])); + let selectedBarColor; + const barColors = StrListCast(this._props.layoutDoc.histogramBarColors).map(each => each.split('::')); + barColors.forEach(each => { + // eslint-disable-next-line prefer-destructuring + each[0] === curSelectedBarName && (selectedBarColor = each[1]); + }); if (this._histogramData.length > 0 || !this.parentViz) { return this._props.axes.length >= 1 ? ( @@ -476,45 +459,51 @@ export class Histogram extends ObservableReactComponent<HistogramProps> { <EditableText val={StrCast(this._props.layoutDoc[titleAccessor])} setVal={undoable( - action(val => (this._props.layoutDoc[titleAccessor] = val as string)), + action(val => { + this._props.layoutDoc[titleAccessor] = val as string; + }), 'Change Graph Title' )} - color={'black'} + color="black" size={Size.LARGE} fillWidth /> <ColorPicker - tooltip={'Change Default Bar Color'} + tooltip="Change Default Bar Color" type={Type.SEC} icon={<FaFillDrip />} selectedColor={StrCast(this._props.layoutDoc.dataViz_histogram_defaultColor)} - setFinalColor={undoable(color => (this._props.layoutDoc.dataViz_histogram_defaultColor = color), 'Change Default Bar Color')} - setSelectedColor={undoable(color => (this._props.layoutDoc.dataViz_histogram_defaultColor = color), 'Change Default Bar Color')} + setFinalColor={undoable(color => { + this._props.layoutDoc.dataViz_histogram_defaultColor = color; + }, 'Change Default Bar Color')} + setSelectedColor={undoable(color => { + this._props.layoutDoc.dataViz_histogram_defaultColor = color; + }, 'Change Default Bar Color')} size={Size.XSMALL} /> </div> <div ref={this._histogramRef} /> - {selected != 'none' ? ( - <div className={'selected-data'}> + {selected !== 'none' ? ( + <div className="selected-data"> Selected: {selected} <ColorPicker - tooltip={'Change Bar Color'} + tooltip="Change Bar Color" type={Type.SEC} icon={<FaFillDrip />} - selectedColor={selectedBarColor ? selectedBarColor : this.curBarSelected.attr('fill')} + selectedColor={selectedBarColor || this.curBarSelected.attr('fill')} setFinalColor={undoable(color => this.changeSelectedColor(color), 'Change Selected Bar Color')} setSelectedColor={undoable(color => this.changeSelectedColor(color), 'Change Selected Bar Color')} size={Size.XSMALL} /> <IconButton - icon={<FontAwesomeIcon icon={'eraser'} />} + icon={<FontAwesomeIcon icon="eraser" />} size={Size.XSMALL} - color={'black'} + color="black" type={Type.SEC} - tooltip={'Revert to the default bar color'} + tooltip="Revert to the default bar color" onClick={undoable( action(() => this.eraseSelectedColor()), 'Change Selected Bar Color' @@ -524,7 +513,7 @@ export class Histogram extends ObservableReactComponent<HistogramProps> { ) : null} </div> ) : ( - <span className="chart-container"> {'first use table view to select a column to graph'}</span> + <span className="chart-container"> first use table view to select a column to graph</span> ); } // when it is a brushed table and the incoming table doesn't have any rows selected diff --git a/src/client/views/nodes/DataVizBox/components/LineChart.tsx b/src/client/views/nodes/DataVizBox/components/LineChart.tsx index e093ec648..bc35ab8c8 100644 --- a/src/client/views/nodes/DataVizBox/components/LineChart.tsx +++ b/src/client/views/nodes/DataVizBox/components/LineChart.tsx @@ -3,18 +3,19 @@ import * as d3 from 'd3'; import { IReactionDisposer, action, computed, makeObservable, observable, reaction } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; -import { Doc, DocListCast, NumListCast, StrListCast } from '../../../../../fields/Doc'; +import { Doc, DocListCast, NumListCast } from '../../../../../fields/Doc'; import { List } from '../../../../../fields/List'; import { listSpec } from '../../../../../fields/Schema'; import { Cast, DocCast, StrCast } from '../../../../../fields/Types'; import { Docs } from '../../../../documents/Documents'; -import { DocumentManager } from '../../../../util/DocumentManager'; import { undoable } from '../../../../util/UndoManager'; +import {} from '../../../DocComponent'; import { ObservableReactComponent } from '../../../ObservableReactComponent'; -import { PinProps, PresBox } from '../../trails'; +import { PinProps, PinDocView } from '../../../PinFuncs'; import { DataVizBox } from '../DataVizBox'; import { createLineGenerator, drawLine, minMaxRange, scaleCreatorNumerical, xAxisCreator, xGrid, yAxisCreator, yGrid } from '../utils/D3Utils'; import './Chart.scss'; +import { DocumentView } from '../../DocumentView'; export interface DataPoint { x: number; @@ -62,7 +63,6 @@ export class LineChart extends ObservableReactComponent<LineChartProps> { return !this.parentViz ? this._props.records : this._tableDataIds.map(rowId => this._props.records[rowId]); } @computed get _lineChartData() { - var guids = StrListCast(this._props.layoutDoc.dataViz_rowIds); if (this._props.axes.length <= 1) return []; return this._tableData.map(record => ({ x: Number(record[this._props.axes[0]]), y: Number(record[this._props.axes[1]]) })).sort((a, b) => (a.x < b.x ? -1 : 1)); } @@ -71,7 +71,7 @@ export class LineChart extends ObservableReactComponent<LineChartProps> { } @computed get parentViz() { return DocCast(this._props.Document.dataViz_parentViz); - // return LinkManager.Instance.getAllRelatedLinks(this._props.Document) // out of all links + // return LinkManager.Links(this._props.Document) // out of all links // .filter(link => { // return link.link_anchor_1 == this._props.Document.dataViz_parentViz; // }) // get links where this chart doc is the target of the link @@ -80,7 +80,7 @@ export class LineChart extends ObservableReactComponent<LineChartProps> { @computed get incomingHighlited() { // return selected x and y axes // otherwise, use the selection of whatever is linked to us - const incomingVizBox = DocumentManager.Instance.getFirstDocumentView(this.parentViz)?.ComponentView as DataVizBox; + const incomingVizBox = DocumentView.getFirstDocumentView(this.parentViz)?.ComponentView as DataVizBox; const highlitedRowIds = NumListCast(incomingVizBox?.layoutDoc?.dataViz_highlitedRows); return this._tableData.filter((record, i) => highlitedRowIds.includes(this._tableDataIds[i])); // get all the datapoints they have selected field set by incoming anchor } @@ -102,7 +102,7 @@ export class LineChart extends ObservableReactComponent<LineChartProps> { ); this._disposers.annos = reaction( () => DocListCast(this._props.dataDoc[this._props.fieldKey + '_annotations']), - annotations => { + (/* annotations */) => { // modify how d3 renders so that anything in this annotations list would be potentially highlighted in some way // could be blue colored to make it look like anchor // this.drawAnnotations() @@ -175,9 +175,9 @@ export class LineChart extends ObservableReactComponent<LineChartProps> { getAnchor = (pinProps?: PinProps) => { const anchor = Docs.Create.ConfigDocument({ // - title: 'line doc selection' + this._currSelected?.x, + title: 'line doc selection' + (this._currSelected?.x ?? ''), }); - PresBox.pinDocView(anchor, { pinDocLayout: pinProps?.pinDocLayout, pinData: pinProps?.pinData }, this._props.Document); + PinDocView(anchor, { pinDocLayout: pinProps?.pinDocLayout, pinData: pinProps?.pinData }, this._props.Document); anchor.config_dataVizSelection = this._currSelected ? new List<number>([this._currSelected.x, this._currSelected.y]) : undefined; return anchor; }; @@ -191,11 +191,12 @@ export class LineChart extends ObservableReactComponent<LineChartProps> { } @computed get defaultGraphTitle() { - var ax0 = this._props.axes[0]; - var ax1 = this._props.axes.length > 1 ? this._props.axes[1] : undefined; + const ax0 = this._props.axes[0]; + const ax1 = this._props.axes.length > 1 ? this._props.axes[1] : undefined; if (this._props.axes.length < 2 || !/\d/.test(this._props.records[0][ax0]) || !ax1) { return ax0 + ' Line Chart'; - } else return ax1 + ' by ' + ax0 + ' Line Chart'; + } + return ax1 + ' by ' + ax0 + ' Line Chart'; } setupTooltip() { @@ -215,9 +216,11 @@ export class LineChart extends ObservableReactComponent<LineChartProps> { @action setCurrSelected(x?: number, y?: number) { // TODO: nda - get rid of svg element in the list? - if (this._currSelected && this._currSelected.x == x && this._currSelected.y == y) this._currSelected = undefined; + if (this._currSelected && this._currSelected.x === x && this._currSelected.y === y) this._currSelected = undefined; else this._currSelected = x !== undefined && y !== undefined ? { x, y } : undefined; - this._props.records.forEach(record => record[this._props.axes[0]] === x && record[this._props.axes[1]] === y && (record.selected = true)); + this._props.records.forEach(record => { + record[this._props.axes[0]] === x && record[this._props.axes[1]] === y && (record.selected = true); + }); } drawDataPoints(data: DataPoint[], idx: number, xScale: d3.ScaleLinear<number, number, never>, yScale: d3.ScaleLinear<number, number, never>) { @@ -241,13 +244,13 @@ export class LineChart extends ObservableReactComponent<LineChartProps> { d3.select(this._lineChartRef.current).select('svg').remove(); d3.select(this._lineChartRef.current).select('.tooltip').remove(); - var { xMin, xMax, yMin, yMax } = rangeVals; + let { xMin, xMax, yMin, yMax } = rangeVals; if (xMin === undefined || xMax === undefined || yMin === undefined || yMax === undefined) { return; } // adding svg - const margin = this._props.margin; + const { margin } = this._props; const svg = (this._lineChartSvg = d3 .select(this._lineChartRef.current) .append('svg') @@ -257,18 +260,19 @@ export class LineChart extends ObservableReactComponent<LineChartProps> { .append('g') .attr('transform', `translate(${margin.left}, ${margin.top})`)); - var validSecondData; - if (this._props.axes.length>2){ // for when there are 2 lines on the chart - var next = this._tableData.map(record => ({ x: Number(record[this._props.axes[0]]), y: Number(record[this._props.axes[2]]) })).sort((a, b) => (a.x < b.x ? -1 : 1)); + let validSecondData; + if (this._props.axes.length > 2) { + // for when there are 2 lines on the chart + const next = this._tableData.map(record => ({ x: Number(record[this._props.axes[0]]), y: Number(record[this._props.axes[2]]) })).sort((a, b) => (a.x < b.x ? -1 : 1)); validSecondData = next.filter(d => { - if (!d.x || Number.isNaN(d.x) || !d.y || Number.isNaN(d.y)) return false; + if (!d.x || isNaN(d.x) || !d.y || isNaN(d.y)) return false; return true; }); - var secondDataRange = minMaxRange([validSecondData]); - if (secondDataRange.xMax!>xMax) xMax = secondDataRange.xMax; - if (secondDataRange.yMax!>yMax) yMax = secondDataRange.yMax; - if (secondDataRange.xMin!<xMin) xMin = secondDataRange.xMin; - if (secondDataRange.yMin!<yMin) yMin = secondDataRange.yMin; + const secondDataRange = minMaxRange([validSecondData]); + if (secondDataRange.xMax! > xMax) xMax = secondDataRange.xMax; + if (secondDataRange.yMax! > yMax) yMax = secondDataRange.yMax; + if (secondDataRange.xMin! < xMin) xMin = secondDataRange.xMin; + if (secondDataRange.yMin! < yMin) yMin = secondDataRange.yMin; } // creating the x and y scales @@ -285,40 +289,34 @@ export class LineChart extends ObservableReactComponent<LineChartProps> { if (validSecondData) { drawLine(svg.append('path'), validSecondData, lineGen, true); this.drawDataPoints(validSecondData, 0, xScale, yScale); - svg.append('path').attr("stroke", "red"); + svg.append('path').attr('stroke', 'red'); // legend - var color = d3.scaleOrdinal() - .range(["black", "blue"]) - .domain([this._props.axes[1], this._props.axes[2]]) - svg.selectAll("mydots") + const color: any = d3.scaleOrdinal().range(['black', 'blue']).domain([this._props.axes[1], this._props.axes[2]]); + svg.selectAll('mydots') .data([this._props.axes[1], this._props.axes[2]]) .enter() - .append("circle") - .attr("cx", 5) - .attr("cy", function(d,i){ return -30 + i*15}) - .attr("r", 7) - .style("fill", function(d){ return color(d)}) - svg.selectAll("mylabels") + .append('circle') + .attr('cx', 5) + .attr('cy', (d, i) => -30 + i * 15) + .attr('r', 7) + .style('fill', d => color(d)); + svg.selectAll('mylabels') .data([this._props.axes[1], this._props.axes[2]]) .enter() - .append("text") - .attr("x", 25) - .attr("y", function(d,i){ return -30 + i*15}) - .style("fill", function(d){ return color(d)}) - .text(function(d){ return d}) - .attr("text-anchor", "left") - .style("alignment-baseline", "middle") + .append('text') + .attr('x', 25) + .attr('y', (d, i) => -30 + i * 15) + .style('fill', d => color(d)) + .text(d => d) + .attr('text-anchor', 'left') + .style('alignment-baseline', 'middle'); } // get valid data points const data = dataSet[0]; - var validData = data.filter(d => { - Object.keys(data[0]).map(key => { - if (!d[key] || Number.isNaN(d[key])) return false; - }); - return true; - }); + const keys = Object.keys(data[0]); + const validData = data.filter(d => !keys.some(key => isNaN(d[key]))); // draw the plot line drawLine(svg.append('path'), validData, lineGen, false); @@ -345,7 +343,7 @@ export class LineChart extends ObservableReactComponent<LineChartProps> { const x0 = bisect(data, xScale.invert(xPos - 5)); // shift x by -5 so that you can reach points on the left-side axis const d0 = data[x0]; // find .circle-d1 with data-x = d0.x and data-y = d0.y - const selected = svg.selectAll('.datapoint').filter((d: any) => d['data-x'] === d0.x && d['data-y'] === d0.y); + svg.selectAll('.datapoint').filter((d: any) => d['data-x'] === d0.x && d['data-y'] === d0.y); this.setCurrSelected(d0.x, d0.y); this.updateTooltip(higlightFocusPt, xScale, d0, yScale, tooltip); }); @@ -368,7 +366,7 @@ export class LineChart extends ObservableReactComponent<LineChartProps> { .style('text-anchor', 'middle') .text(this._props.axes[0]); svg.append('text') - .attr('transform', 'rotate(-90)' + ' ' + 'translate( 0, ' + -10 + ')') + .attr('transform', 'rotate(-90) translate(0, -10)') .attr('x', -(height / 2)) .attr('y', -30) .attr('height', 20) @@ -394,57 +392,60 @@ export class LineChart extends ObservableReactComponent<LineChartProps> { } render() { - var titleAccessor: any = 'dataViz_lineChart_title'; - if (this._props.axes.length == 2) titleAccessor = titleAccessor + this._props.axes[0] + '-' + this._props.axes[1]; - else if (this._props.axes.length > 0) titleAccessor = titleAccessor + this._props.axes[0]; + let titleAccessor: any = 'dataViz_lineChart_title'; + if (this._props.axes.length === 2) titleAccessor = titleAccessor + this._props.axes[0] + '-' + this._props.axes[1]; + else if (this._props.axes.length > 0) titleAccessor += this._props.axes[0]; if (!this._props.layoutDoc[titleAccessor]) this._props.layoutDoc[titleAccessor] = this.defaultGraphTitle; const selectedPt = this._currSelected ? `{ ${this._props.axes[0]}: ${this._currSelected.x} ${this._props.axes[1]}: ${this._currSelected.y} }` : 'none'; - var selectedTitle = ""; - if (this._currSelected && this._props.titleCol){ - selectedTitle+= "\n" + this._props.titleCol + ": " + let selectedTitle = ''; + if (this._currSelected && this._props.titleCol) { + selectedTitle += '\n' + this._props.titleCol + ': '; this._tableData.forEach(each => { - var mapThisEntry = false; - if (this._currSelected.x==each[this._props.axes[0]] && this._currSelected.y==each[this._props.axes[1]]) mapThisEntry = true; - else if (this._currSelected.y==each[this._props.axes[0]] && this._currSelected.x==each[this._props.axes[1]]) mapThisEntry = true; - if (mapThisEntry) selectedTitle += each[this._props.titleCol] + ", "; - }) - selectedTitle = selectedTitle.slice(0,-1).slice(0,-1); + let mapThisEntry = false; + if (this._currSelected.x === each[this._props.axes[0]] && this._currSelected.y === each[this._props.axes[1]]) mapThisEntry = true; + else if (this._currSelected.y === each[this._props.axes[0]] && this._currSelected.x === each[this._props.axes[1]]) mapThisEntry = true; + if (mapThisEntry) selectedTitle += each[this._props.titleCol] + ', '; + }); + selectedTitle = selectedTitle.slice(0, -1).slice(0, -1); } - if (this._lineChartData.length > 0 || !this.parentViz || this.parentViz.length == 0) { + if (this._lineChartData.length > 0 || !this.parentViz || this.parentViz.length === 0) { return this._props.axes.length >= 2 && /\d/.test(this._props.records[0][this._props.axes[0]]) && /\d/.test(this._props.records[0][this._props.axes[1]]) ? ( <div className="chart-container" style={{ width: this._props.width + this._props.margin.right }}> <div className="graph-title"> <EditableText val={StrCast(this._props.layoutDoc[titleAccessor])} setVal={undoable( - action(val => (this._props.layoutDoc[titleAccessor] = val as string)), + action(val => { + this._props.layoutDoc[titleAccessor] = val as string; + }), 'Change Graph Title' )} - color={'black'} + color="black" size={Size.LARGE} fillWidth /> </div> <div ref={this._lineChartRef} /> - {selectedPt != 'none' ? ( - <div className={'selected-data'}> + {selectedPt !== 'none' ? ( + <div className="selected-data"> {`Selected: ${selectedPt}`} {`${selectedTitle}`} <Button - onClick={e => { + onClick={() => { this._props.vizBox.sidebarBtnDown; this._props.vizBox.sidebarAddDocument; - }}></Button> + }} + /> </div> ) : null} </div> ) : ( - <span className="chart-container"> {'first use table view to select two numerical axes to plot'}</span> - ); - } else - return ( - // when it is a brushed table and the incoming table doesn't have any rows selected - <div className="chart-container">Selected rows of data from the incoming DataVizBox to display.</div> + <span className="chart-container"> first use table view to select two numerical axes to plot</span> ); + } + return ( + // when it is a brushed table and the incoming table doesn't have any rows selected + <div className="chart-container">Selected rows of data from the incoming DataVizBox to display.</div> + ); } } diff --git a/src/client/views/nodes/DataVizBox/components/PieChart.tsx b/src/client/views/nodes/DataVizBox/components/PieChart.tsx index fc23f47de..ef6d1d412 100644 --- a/src/client/views/nodes/DataVizBox/components/PieChart.tsx +++ b/src/client/views/nodes/DataVizBox/components/PieChart.tsx @@ -12,7 +12,7 @@ import { Cast, DocCast, StrCast } from '../../../../../fields/Types'; import { Docs } from '../../../../documents/Documents'; import { undoable } from '../../../../util/UndoManager'; import { ObservableReactComponent } from '../../../ObservableReactComponent'; -import { PinProps, PresBox } from '../../trails'; +import { PinProps, PinDocView } from '../../../PinFuncs'; import './Chart.scss'; export interface PieChartProps { @@ -74,8 +74,8 @@ export class PieChart extends ObservableReactComponent<PieChartProps> { } @computed get defaultGraphTitle() { - var ax0 = this._props.axes[0]; - var ax1 = this._props.axes.length > 1 ? this._props.axes[1] : undefined; + const ax0 = this._props.axes[0]; + const ax1 = this._props.axes.length > 1 ? this._props.axes[1] : undefined; if (this._props.axes.length < 2 || !/\d/.test(this._props.records[0][ax0]) || !ax1) { return ax0 + ' Pie Chart'; } @@ -84,7 +84,7 @@ export class PieChart extends ObservableReactComponent<PieChartProps> { @computed get parentViz() { return DocCast(this._props.Document.dataViz_parentViz); - // return LinkManager.Instance.getAllRelatedLinks(this._props.Document) // out of all links + // return LinkManager.Links(this._props.Document) // out of all links // .filter(link => link.link_anchor_1 == this._props.Document.dataViz_parentViz) // get links where this chart doc is the target of the link // .map(link => DocCast(link.link_anchor_1)); // then return the source of the link } @@ -101,14 +101,14 @@ export class PieChart extends ObservableReactComponent<PieChartProps> { } @action - restoreView = (data: Doc) => {}; + restoreView = (/* data: Doc */) => {}; // create a document anchor that stores whatever is needed to reconstruct the viewing state (selection,zoom,etc) getAnchor = (pinProps?: PinProps) => { const anchor = Docs.Create.ConfigDocument({ // title: 'piechart doc selection' + this._currSelected, }); - PresBox.pinDocView(anchor, { pinDocLayout: pinProps?.pinDocLayout, pinData: pinProps?.pinData }, this._props.Document); + PinDocView(anchor, { pinDocLayout: pinProps?.pinDocLayout, pinData: pinProps?.pinData }, this._props.Document); return anchor; }; @@ -122,30 +122,30 @@ export class PieChart extends ObservableReactComponent<PieChartProps> { // cleans data by converting numerical data to numbers and taking out empty cells data = (dataSet: any) => { - const validData = dataSet.filter((d: { [x: string]: unknown }) => !Object.keys(dataSet[0]).some(key => !d[key] || Number.isNaN(d[key]))); + const validData = dataSet.filter((d: { [x: string]: unknown }) => !Object.keys(dataSet[0]).some(key => !d[key] || isNaN(d[key]))); const field = dataSet[0] ? Object.keys(dataSet[0])[0] : undefined; return !field ? undefined : validData.map((d: { [x: string]: any }) => this.byCategory ? d[field] // - : +d[field].replace(/\$/g, '').replace(/\%/g, '').replace(/\#/g, '').replace(/\</g, '') + : +d[field].replace(/\$/g, '').replace(/%/g, '').replace(/#/g, '').replace(/</g, '') ); }; // outlines the slice selected / hovered over highlightSelectedSlice = (changeSelectedVariables: boolean, svg: any, arc: any, radius: any, pointer: any, pieDataSet: any) => { - var index = -1; - var sameAsCurrent: boolean; + let index = -1; + let sameAsCurrent: boolean; const selected = svg.selectAll('.slice').filter((d: any) => { index++; - var p1 = [0, 0]; // center of pie - var p3 = [arc.centroid(d)[0] * 2, arc.centroid(d)[1] * 2]; // outward peak of arc - var p2 = [radius * Math.sin(d.startAngle), -radius * Math.cos(d.startAngle)]; // start of arc - var p4 = [radius * Math.sin(d.endAngle), -radius * Math.cos(d.endAngle)]; // end of arc + const p1 = [0, 0]; // center of pie + const p3 = [arc.centroid(d)[0] * 2, arc.centroid(d)[1] * 2]; // outward peak of arc + const p2 = [radius * Math.sin(d.startAngle), -radius * Math.cos(d.startAngle)]; // start of arc + const p4 = [radius * Math.sin(d.endAngle), -radius * Math.cos(d.endAngle)]; // end of arc // draw an imaginary horizontal line from the pointer to see how many times it crosses a slice edge - var lineCrossCount = 0; + let lineCrossCount = 0; // if for all 4 lines if (Math.min(p1[1], p2[1]) <= pointer[1] && pointer[1] <= Math.max(p1[1], p2[1])) { // within y bounds @@ -160,13 +160,13 @@ export class PieChart extends ObservableReactComponent<PieChartProps> { if (Math.min(p4[1], p1[1]) <= pointer[1] && pointer[1] <= Math.max(p4[1], p1[1])) { if (pointer[0] <= ((pointer[1] - p4[1]) * (p1[0] - p4[0])) / (p1[1] - p4[1]) + p4[0]) lineCrossCount++; } - if (lineCrossCount % 2 != 0) { + if (lineCrossCount % 2 !== 0) { // inside the slice of it crosses an odd number of edges - var showSelected = this.byCategory ? pieDataSet[index] : this._pieChartData[index]; + const showSelected = this.byCategory ? pieDataSet[index] : this._pieChartData[index]; if (changeSelectedVariables) { // for when a bar is selected - not just hovered over sameAsCurrent = this._currSelected - ? showSelected[Object.keys(showSelected)[0]] == this._currSelected![Object.keys(showSelected)[0]] && showSelected[Object.keys(showSelected)[1]] == this._currSelected![Object.keys(showSelected)[1]] + ? showSelected[Object.keys(showSelected)[0]] === this._currSelected![Object.keys(showSelected)[0]] && showSelected[Object.keys(showSelected)[1]] === this._currSelected![Object.keys(showSelected)[1]] : this._currSelected === showSelected; this._currSelected = sameAsCurrent ? undefined : showSelected; this.selectedData = sameAsCurrent ? undefined : d; @@ -186,104 +186,100 @@ export class PieChart extends ObservableReactComponent<PieChartProps> { d3.select(this._piechartRef.current).select('svg').remove(); d3.select(this._piechartRef.current).select('.tooltip').remove(); - var percentField = Object.keys(dataSet[0])[0]; - var descriptionField = Object.keys(dataSet[0])[1]!; - var radius = Math.min(width, height - this._props.margin.top - this._props.margin.bottom) / 2; + let percentField = Object.keys(dataSet[0])[0]; + let descriptionField = Object.keys(dataSet[0])[1]!; + const radius = Math.min(width, height - this._props.margin.top - this._props.margin.bottom) / 2; // converts data into Objects - var data = this.data(dataSet); - var pieDataSet = dataSet.filter((d: { [x: string]: unknown }) => !Object.keys(dataSet[0]).some(key => !d[key] || Number.isNaN(d[key]))); + let data = this.data(dataSet); + let pieDataSet = dataSet.filter((d: { [x: string]: unknown }) => !Object.keys(dataSet[0]).some(key => !d[key] || isNaN(d[key]))); if (this.byCategory) { - let uniqueCategories = [...new Set(data)]; - var pieStringDataSet: { frequency: number }[] = []; + const uniqueCategories = [...new Set(data)]; + const pieStringDataSet: { frequency: number }[] = []; for (let i = 0; i < uniqueCategories.length; i++) { pieStringDataSet.push({ frequency: 0, [percentField]: uniqueCategories[i] }); } for (let i = 0; i < data.length; i++) { - let sliceData = pieStringDataSet.filter((each: any) => each[percentField] == data[i]); - sliceData[0].frequency = sliceData[0].frequency + 1; + // eslint-disable-next-line no-loop-func + const sliceData = pieStringDataSet.filter((each: any) => each[percentField] === data[i]); + sliceData[0].frequency += 1; } pieDataSet = pieStringDataSet; - percentField = Object.keys(pieDataSet[0])[0]; - descriptionField = Object.keys(pieDataSet[0])[1]!; + [percentField, descriptionField] = Object.keys(pieDataSet[0]); data = this.data(pieStringDataSet); } - var trackDuplicates: { [key: string]: any } = {}; - data.forEach((eachData: any) => (!trackDuplicates[eachData] ? (trackDuplicates[eachData] = 0) : null)); + let trackDuplicates: { [key: string]: any } = {}; + data.forEach((eachData: any) => { + !trackDuplicates[eachData] ? (trackDuplicates[eachData] = 0) : null; + }); // initial chart - var svg = (this._piechartSvg = d3 + const svg = (this._piechartSvg = d3 .select(this._piechartRef.current) .append('svg') .attr('class', 'graph') .attr('width', width + this._props.margin.right + this._props.margin.left) .attr('height', height + this._props.margin.top + this._props.margin.bottom) .append('g')); - let g = svg.append('g').attr('transform', 'translate(' + (width / 2 + this._props.margin.left) + ',' + height / 2 + ')'); - var pie = d3.pie(); - var arc = d3.arc().innerRadius(0).outerRadius(radius); + const g = svg.append('g').attr('transform', 'translate(' + (width / 2 + this._props.margin.left) + ',' + height / 2 + ')'); + const pie = d3.pie(); + const arc = d3.arc().innerRadius(0).outerRadius(radius); + const updateHighlights = () => { + const hoverOverSlice = this.hoverOverData; + const { selectedData } = this; + svg.selectAll('path').attr('class', (d: any) => + (selectedData && d.startAngle === selectedData.startAngle && d.endAngle === selectedData.endAngle) || (hoverOverSlice && d.startAngle === hoverOverSlice.startAngle && d.endAngle === hoverOverSlice.endAngle) ? 'slice hover' : 'slice' + ); + }; // click/hover const onPointClick = action((e: any) => this.highlightSelectedSlice(true, svg, arc, radius, d3.pointer(e), pieDataSet)); const onHover = action((e: any) => { this.highlightSelectedSlice(false, svg, arc, radius, d3.pointer(e), pieDataSet); updateHighlights(); }); - const mouseOut = action((e: any) => { + const mouseOut = action(() => { this.hoverOverData = undefined; updateHighlights(); }); - const updateHighlights = () => { - const hoverOverSlice = this.hoverOverData; - const selectedData = this.selectedData; - svg.selectAll('path').attr('class', function (d: any) { - return (selectedData && d.startAngle == selectedData.startAngle && d.endAngle == selectedData.endAngle) || (hoverOverSlice && d.startAngle == hoverOverSlice.startAngle && d.endAngle == hoverOverSlice.endAngle) - ? 'slice hover' - : 'slice'; - }); - }; // drawing the slices - var selected = this.selectedData; - var arcs = g.selectAll('arc').data(pie(data)).enter().append('g'); + const selected = this.selectedData; + const arcs = g.selectAll('arc').data(pie(data)).enter().append('g'); const possibleDataPointVals: { [x: string]: any }[] = []; pieDataSet.forEach((each: { [x: string]: any | { valueOf(): number } }) => { - var dataPointVal: { [x: string]: any } = {}; + const dataPointVal: { [x: string]: any } = {}; dataPointVal[percentField] = each[percentField]; if (descriptionField) dataPointVal[descriptionField] = each[descriptionField]; try { - dataPointVal[percentField] = Number(dataPointVal[percentField].replace(/\$/g, '').replace(/\%/g, '').replace(/\#/g, '').replace(/\</g, '')); - } catch (error) {} + dataPointVal[percentField] = Number(dataPointVal[percentField].replace(/\$/g, '').replace(/%/g, '').replace(/#/g, '').replace(/</g, '')); + } catch (error) { + /* empty */ + } possibleDataPointVals.push(dataPointVal); }); const sliceColors = StrListCast(this._props.layoutDoc.dataViz_pie_sliceColors).map(each => each.split('::')); arcs.append('path') .attr('fill', (d, i) => { - var dataPoint; + let dataPoint; const possibleDataPoints = possibleDataPointVals.filter((pval: any) => pval[percentField] === Number(d.data)); - if (possibleDataPoints.length == 1) dataPoint = possibleDataPoints[0]; + if (possibleDataPoints.length === 1) [dataPoint] = possibleDataPoints; else { dataPoint = possibleDataPoints[trackDuplicates[d.data.toString()]]; trackDuplicates[d.data.toString()] = trackDuplicates[d.data.toString()] + 1; } - var sliceColor; + let sliceColor; if (dataPoint) { const sliceTitle = dataPoint[this._props.axes[0]]; - const accessByName = StrCast(sliceTitle) ? StrCast(sliceTitle).replace(/\$/g, '').replace(/\%/g, '').replace(/\#/g, '').replace(/\</g, '') : sliceTitle; - sliceColors.forEach(each => each[0] == accessByName && (sliceColor = each[1])); + const accessByName = StrCast(sliceTitle) ? StrCast(sliceTitle).replace(/\$/g, '').replace(/%/g, '').replace(/#/g, '').replace(/</g, '') : sliceTitle; + sliceColors.forEach(each => { + // eslint-disable-next-line prefer-destructuring + each[0] === accessByName && (sliceColor = each[1]); + }); } return sliceColor ? StrCast(sliceColor) : d3.schemeSet3[i] ? d3.schemeSet3[i] : d3.schemeSet3[i % d3.schemeSet3.length]; }) - .attr( - 'class', - selected - ? function (d) { - return selected && d.startAngle == selected.startAngle && d.endAngle == selected.endAngle ? 'slice hover' : 'slice'; - } - : function (d) { - return 'slice'; - } - ) + .attr('class', selected ? d => (selected && d.startAngle === selected.startAngle && d.endAngle === selected.endAngle ? 'slice hover' : 'slice') : () => 'slice') // @ts-ignore .attr('d', arc) .on('click', onPointClick) @@ -292,20 +288,22 @@ export class PieChart extends ObservableReactComponent<PieChartProps> { // adding labels trackDuplicates = {}; - data.forEach((eachData: any) => (!trackDuplicates[eachData] ? (trackDuplicates[eachData] = 0) : null)); + data.forEach((eachData: any) => { + !trackDuplicates[eachData] ? (trackDuplicates[eachData] = 0) : null; + }); arcs.size() < 100 && arcs .append('text') - .attr('transform', function (d) { - var centroid = arc.centroid(d as unknown as d3.DefaultArcObject); - var heightOffset = (centroid[1] / radius) * Math.abs(centroid[1]); + .attr('transform', d => { + const centroid = arc.centroid(d as unknown as d3.DefaultArcObject); + const heightOffset = (centroid[1] / radius) * Math.abs(centroid[1]); return 'translate(' + (centroid[0] + centroid[0] / (radius * 0.02)) + ',' + (centroid[1] + heightOffset) + ')'; }) .attr('text-anchor', 'middle') - .text(function (d) { - var dataPoint; + .text(d => { + let dataPoint; const possibleDataPoints = possibleDataPointVals.filter((pval: any) => pval[percentField] === Number(d.data)); - if (possibleDataPoints.length == 1) dataPoint = pieDataSet[possibleDataPointVals.indexOf(possibleDataPoints[0])]; + if (possibleDataPoints.length === 1) dataPoint = pieDataSet[possibleDataPointVals.indexOf(possibleDataPoints[0])]; else { dataPoint = pieDataSet[possibleDataPointVals.indexOf(possibleDataPoints[trackDuplicates[d.data.toString()]])]; trackDuplicates[d.data.toString()] = trackDuplicates[d.data.toString()] + 1; @@ -317,11 +315,11 @@ export class PieChart extends ObservableReactComponent<PieChartProps> { @action changeSelectedColor = (color: string) => { this.curSliceSelected.attr('fill', color); const sliceTitle = this._currSelected[this._props.axes[0]]; - const sliceName = StrCast(sliceTitle) ? StrCast(sliceTitle).replace(/\$/g, '').replace(/\%/g, '').replace(/\#/g, '').replace(/\</g, '') : sliceTitle; + const sliceName = StrCast(sliceTitle) ? StrCast(sliceTitle).replace(/\$/g, '').replace(/%/g, '').replace(/#/g, '').replace(/</g, '') : sliceTitle; const sliceColors = Cast(this._props.layoutDoc.dataViz_pie_sliceColors, listSpec('string'), null); - sliceColors.map(each => { - if (each.split('::')[0] == sliceName) sliceColors.splice(sliceColors.indexOf(each), 1); + sliceColors.forEach(each => { + if (each.split('::')[0] === sliceName) sliceColors.splice(sliceColors.indexOf(each), 1); }); sliceColors.push(StrCast(sliceName + '::' + color)); }; @@ -332,39 +330,39 @@ export class PieChart extends ObservableReactComponent<PieChartProps> { }; render() { - var titleAccessor: any = 'dataViz_pie_title'; - if (this._props.axes.length == 2) titleAccessor = titleAccessor + this._props.axes[0] + '-' + this._props.axes[1]; - else if (this._props.axes.length > 0) titleAccessor = titleAccessor + this._props.axes[0]; + let titleAccessor: any = 'dataViz_pie_title'; + if (this._props.axes.length === 2) titleAccessor = titleAccessor + this._props.axes[0] + '-' + this._props.axes[1]; + else if (this._props.axes.length > 0) titleAccessor += this._props.axes[0]; if (!this._props.layoutDoc[titleAccessor]) this._props.layoutDoc[titleAccessor] = this.defaultGraphTitle; if (!this._props.layoutDoc.dataViz_pie_sliceColors) this._props.layoutDoc.dataViz_pie_sliceColors = new List<string>(); - var selected: string; - var curSelectedSliceName = ''; + let selected: string; + let curSelectedSliceName = ''; if (this._currSelected) { selected = '{ '; const sliceTitle = this._currSelected[this._props.axes[0]]; - curSelectedSliceName = StrCast(sliceTitle) ? StrCast(sliceTitle).replace(/\$/g, '').replace(/\%/g, '').replace(/\#/g, '').replace(/\</g, '') : sliceTitle; - Object.keys(this._currSelected).map(key => { - key != '' ? (selected += key + ': ' + this._currSelected[key] + ', ') : ''; + curSelectedSliceName = StrCast(sliceTitle) ? StrCast(sliceTitle).replace(/\$/g, '').replace(/%/g, '').replace(/#/g, '').replace(/</g, '') : sliceTitle; + Object.keys(this._currSelected).forEach(key => { + key !== '' ? (selected += key + ': ' + this._currSelected[key] + ', ') : ''; }); selected = selected.substring(0, selected.length - 2); selected += ' }'; - if (this._props.titleCol!="" && (!this._currSelected["frequency"] || this._currSelected["frequency"]<10)){ - selected+= "\n" + this._props.titleCol + ": " + if (this._props.titleCol !== '' && (!this._currSelected.frequency || this._currSelected.frequency < 10)) { + selected += '\n' + this._props.titleCol + ': '; this._tableData.forEach(each => { - if (this._currSelected[this._props.axes[0]]==each[this._props.axes[0]]) { - if (this._props.axes[1]){ - if (this._currSelected[this._props.axes[1]]==each[this._props.axes[1]]) selected+= each[this._props.titleCol] + ", "; - } - else selected+= each[this._props.titleCol] + ", "; + if (this._currSelected[this._props.axes[0]] === each[this._props.axes[0]]) { + if (this._props.axes[1]) { + if (this._currSelected[this._props.axes[1]] === each[this._props.axes[1]]) selected += each[this._props.titleCol] + ', '; + } else selected += each[this._props.titleCol] + ', '; } - }) - selected = selected.slice(0,-1).slice(0,-1); + }); + selected = selected.slice(0, -1).slice(0, -1); } } else selected = 'none'; - var selectedSliceColor; - var sliceColors = StrListCast(this._props.layoutDoc.dataViz_pie_sliceColors).map(each => each.split('::')); + let selectedSliceColor; + const sliceColors = StrListCast(this._props.layoutDoc.dataViz_pie_sliceColors).map(each => each.split('::')); sliceColors.forEach(each => { - if (each[0] == curSelectedSliceName!) selectedSliceColor = each[1]; + // eslint-disable-next-line prefer-destructuring + if (each[0] === curSelectedSliceName!) selectedSliceColor = each[1]; }); if (this._pieChartData.length > 0 || !this.parentViz) { @@ -374,30 +372,32 @@ export class PieChart extends ObservableReactComponent<PieChartProps> { <EditableText val={StrCast(this._props.layoutDoc[titleAccessor])} setVal={undoable( - action(val => (this._props.layoutDoc[titleAccessor] = val as string)), + action(val => { + this._props.layoutDoc[titleAccessor] = val as string; + }), 'Change Graph Title' )} - color={'black'} + color="black" size={Size.LARGE} fillWidth /> </div> {this._props.axes.length === 1 && /\d/.test(this._props.records[0][this._props.axes[0]]) ? ( - <div className={'asHistogram-checkBox'} style={{ width: this._props.width }}> + <div className="asHistogram-checkBox" style={{ width: this._props.width }}> <Checkbox color="primary" onChange={this.changeHistogramCheckBox} checked={this._props.layoutDoc.dataViz_pie_asHistogram as boolean} /> Organize data as histogram </div> ) : null} <div ref={this._piechartRef} /> - {selected != 'none' ? ( - <div className={'selected-data'}> + {selected !== 'none' ? ( + <div className="selected-data"> Selected: {selected} <ColorPicker - tooltip={'Change Slice Color'} + tooltip="Change Slice Color" type={Type.SEC} icon={<FaFillDrip />} - selectedColor={selectedSliceColor ? selectedSliceColor : this.curSliceSelected.attr('fill')} + selectedColor={selectedSliceColor || this.curSliceSelected.attr('fill')} setFinalColor={undoable(color => this.changeSelectedColor(color), 'Change Selected Slice Color')} setSelectedColor={undoable(color => this.changeSelectedColor(color), 'Change Selected Slice Color')} size={Size.XSMALL} @@ -406,12 +406,12 @@ export class PieChart extends ObservableReactComponent<PieChartProps> { ) : null} </div> ) : ( - <span className="chart-container"> {'first use table view to select a column to graph'}</span> - ); - } else - return ( - // when it is a brushed table and the incoming table doesn't have any rows selected - <div className="chart-container">Selected rows of data from the incoming DataVizBox to display.</div> + <span className="chart-container"> first use table view to select a column to graph</span> ); + } + return ( + // when it is a brushed table and the incoming table doesn't have any rows selected + <div className="chart-container">Selected rows of data from the incoming DataVizBox to display.</div> + ); } } diff --git a/src/client/views/nodes/DataVizBox/components/TableBox.tsx b/src/client/views/nodes/DataVizBox/components/TableBox.tsx index 67e1c67bd..5cd77e274 100644 --- a/src/client/views/nodes/DataVizBox/components/TableBox.tsx +++ b/src/client/views/nodes/DataVizBox/components/TableBox.tsx @@ -1,19 +1,25 @@ +/* eslint-disable jsx-a11y/no-noninteractive-tabindex */ +/* eslint-disable jsx-a11y/no-static-element-interactions */ import { Button, Type } from 'browndash-components'; import { IReactionDisposer, action, computed, makeObservable, observable, reaction } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; -import { Utils, emptyFunction, setupMoveUpEvents } from '../../../../../Utils'; +import { ClientUtils, setupMoveUpEvents } from '../../../../../ClientUtils'; +import { emptyFunction } from '../../../../../Utils'; import { Doc, Field, NumListCast } from '../../../../../fields/Doc'; +import { DocData } from '../../../../../fields/DocSymbols'; import { List } from '../../../../../fields/List'; import { listSpec } from '../../../../../fields/Schema'; import { Cast, DocCast } from '../../../../../fields/Types'; import { DragManager } from '../../../../util/DragManager'; +import { undoable } from '../../../../util/UndoManager'; import { ObservableReactComponent } from '../../../ObservableReactComponent'; import { DocumentView } from '../../DocumentView'; import { DataVizView } from '../DataVizBox'; import './Chart.scss'; -import { undoable } from '../../../../util/UndoManager'; + const { DATA_VIZ_TABLE_ROW_HEIGHT } = require('../../../global/globalCssVariables.module.scss'); // prettier-ignore + interface TableBoxProps { Document: Doc; layoutDoc: Doc; @@ -77,7 +83,7 @@ export class TableBox extends ObservableReactComponent<TableBoxProps> { } @computed get columns() { - return this._tableData.length ? Array.from(Object.keys(this._tableData[0])).filter(header => header != '' && header != undefined) : []; + return this._tableData.length ? Array.from(Object.keys(this._tableData[0])).filter(header => header !== '' && header !== undefined) : []; } // updates the 'dataViz_selectedRows' and 'dataViz_highlightedRows' fields to no longer include rows that aren't in the table @@ -114,15 +120,13 @@ export class TableBox extends ObservableReactComponent<TableBoxProps> { if (highlited?.includes(rowId)) highlited.splice(highlited.indexOf(rowId), 1); else highlited?.push(rowId); if (!selected?.includes(rowId)) selected?.push(rowId); - } else { + } else if (selected?.includes(rowId)) { // selecting a row - if (selected?.includes(rowId)) { - if (highlited?.includes(rowId)) highlited.splice(highlited.indexOf(rowId), 1); - selected.splice(selected.indexOf(rowId), 1); - } else selected?.push(rowId); - } + if (highlited?.includes(rowId)) highlited.splice(highlited.indexOf(rowId), 1); + selected.splice(selected.indexOf(rowId), 1); + } else selected?.push(rowId); e.stopPropagation(); - this.hasRowsToFilter = selected.length > 0 ? true : false; + this.hasRowsToFilter = selected.length > 0; }; columnPointerDown = (e: React.PointerEvent, col: string) => { @@ -131,7 +135,7 @@ export class TableBox extends ObservableReactComponent<TableBoxProps> { setupMoveUpEvents( {}, e, - e => { + moveEv => { // dragging off a column to create a brushed DataVizBox const sourceAnchorCreator = () => this._props.docView?.()!.Document!; const targetCreator = (annotationOn: Doc | undefined) => { @@ -145,15 +149,13 @@ export class TableBox extends ObservableReactComponent<TableBoxProps> { embedding.pieSliceColors = Field.Copy(this._props.layoutDoc.pieSliceColors); return embedding; }; - if (this._props.docView?.() && !Utils.isClick(e.clientX, e.clientY, downX, downY, Date.now())) { - DragManager.StartAnchorAnnoDrag(e.target instanceof HTMLElement ? [e.target] : [], new DragManager.AnchorAnnoDragData(this._props.docView()!, sourceAnchorCreator, targetCreator), downX, downY, { - dragComplete: e => { - if (!e.aborted && e.annoDragData && e.annoDragData.linkSourceDoc && e.annoDragData.dropDocument && e.linkDocument) { - e.linkDocument.link_displayLine = true; - e.linkDocument.link_matchEmbeddings = true; - e.linkDocument.link_displayArrow = true; - // e.annoDragData.linkSourceDoc.followLinkToggle = e.annoDragData.dropDocument.annotationOn === this._props.Document; - // e.annoDragData.linkSourceDoc.followLinkZoom = false; + if (this._props.docView?.() && !ClientUtils.isClick(moveEv.clientX, moveEv.clientY, downX, downY, Date.now())) { + DragManager.StartAnchorAnnoDrag(moveEv.target instanceof HTMLElement ? [moveEv.target] : [], new DragManager.AnchorAnnoDragData(this._props.docView()!, sourceAnchorCreator, targetCreator), downX, downY, { + dragComplete: completeEv => { + if (!completeEv.aborted && completeEv.annoDragData && completeEv.annoDragData.linkSourceDoc && completeEv.annoDragData.dropDocument && completeEv.linkDocument) { + completeEv.linkDocument[DocData].link_matchEmbeddings = true; + completeEv.linkDocument[DocData].stroke_startMarker = true; + this._props.docView?.()?._props.addDocument?.(completeEv.linkDocument); } }, }); @@ -162,10 +164,10 @@ export class TableBox extends ObservableReactComponent<TableBoxProps> { return false; }, emptyFunction, - action(e => { - if (e.shiftKey || this.settingTitle) { + action(moveEv => { + if (moveEv.shiftKey || this.settingTitle) { if (this.settingTitle) this.settingTitle = false; - if (this._props.titleCol == col) this._props.titleCol = ''; + if (this._props.titleCol === col) this._props.titleCol = ''; else this._props.titleCol = col; this._props.selectTitleCol(this._props.titleCol); } else { @@ -183,16 +185,16 @@ export class TableBox extends ObservableReactComponent<TableBoxProps> { * These functions handle the filtering popup for when the "filter" button is pressed to select rows */ filter = undoable((e: any) => { - var start: any; - var end: any; - if (this.filteringType == 'Range') { + let start: any; + let end: any; + if (this.filteringType === 'Range') { start = (this.filteringVal[0] as Number) ? Number(this.filteringVal[0]) : this.filteringVal[0]; end = (this.filteringVal[1] as Number) ? Number(this.filteringVal[1]) : this.filteringVal[0]; } this._tableDataIds.forEach(rowID => { - if (this.filteringType == 'Value') { - if (this._props.records[rowID][this.filteringColumn] == this.filteringVal[0]) { + if (this.filteringType === 'Value') { + if (this._props.records[rowID][this.filteringColumn] === this.filteringVal[0]) { if (!NumListCast(this._props.layoutDoc.dataViz_selectedRows).includes(rowID)) { this.tableRowClick(e, rowID); } @@ -229,12 +231,12 @@ export class TableBox extends ObservableReactComponent<TableBoxProps> { this.filteringVal[1] = e.target.value; }); @computed get renderFiltering() { - if (this.filteringColumn === '') this.filteringColumn = this.columns[0]; + if (this.filteringColumn === '') [this.filteringColumn] = this.columns; return ( <div className="tableBox-filterPopup" style={{ right: this._props.width * 0.05 }}> <div className="tableBox-filterPopup-selectColumn"> Column: - <select className="tableBox-filterPopup-selectColumn-each" value={this.filteringColumn != '' ? this.filteringColumn : this.columns[0]} onChange={e => this.setFilterColumn(e)}> + <select className="tableBox-filterPopup-selectColumn-each" value={this.filteringColumn !== '' ? this.filteringColumn : this.columns[0]} onChange={e => this.setFilterColumn(e)}> {this.columns.map(column => ( <option className="" key={column} value={column}> {' '} @@ -245,17 +247,17 @@ export class TableBox extends ObservableReactComponent<TableBoxProps> { </div> <div className="tableBox-filterPopup-setValue"> <select className="tableBox-filterPopup-setValue-each" value={this.filteringType} onChange={e => this.setFilterType(e)}> - <option className="" key={'Value'} value={'Value'}> + <option className="" key="Value" value="Value"> {' '} {'Value'}{' '} </option> - <option className="" key={'Range'} value={'Range'}> + <option className="" key="Range" value="Range"> {' '} {'Range'}{' '} </option> </select> : - {this.filteringType == 'Value' ? ( + {this.filteringType === 'Value' ? ( <input className="tableBox-filterPopup-setValue-input" defaultValue="" @@ -301,7 +303,7 @@ export class TableBox extends ObservableReactComponent<TableBoxProps> { )} </div> <div className="tableBox-filterPopup-setFilter"> - <Button onClick={action(e => this.filter(e))} text="Set Filter" type={Type.SEC} color={'black'} /> + <Button onClick={action(e => this.filter(e))} text="Set Filter" type={Type.SEC} color="black" /> </div> </div> ); @@ -322,11 +324,25 @@ export class TableBox extends ObservableReactComponent<TableBoxProps> { }}> <div className="tableBox-selectButtons"> <div className="tableBox-selectTitle"> - <Button onClick={action(() => (this.settingTitle = !this.settingTitle))} text="Select Title Column" type={Type.SEC} color={'black'} /> + <Button + onClick={action(() => { + this.settingTitle = !this.settingTitle; + })} + text="Select Title Column" + type={Type.SEC} + color="black" + /> </div> <div className="tableBox-filtering"> {this.filtering ? this.renderFiltering : null} - <Button onClick={action(() => (this.filtering = !this.filtering))} text="Filter" type={Type.SEC} color={'black'} /> + <Button + onClick={action(() => { + this.filtering = !this.filtering; + })} + text="Filter" + type={Type.SEC} + color="black" + /> <div className="tableBox-filterAll"> {this.hasRowsToFilter ? ( <Button @@ -336,7 +352,7 @@ export class TableBox extends ObservableReactComponent<TableBoxProps> { })} text="Deselect All" type={Type.SEC} - color={'black'} + color="black" tooltip="Select rows to be displayed in any DataViz boxes dragged off of this one." /> ) : ( @@ -347,7 +363,7 @@ export class TableBox extends ObservableReactComponent<TableBoxProps> { })} text="Select All" type={Type.SEC} - color={'black'} + color="black" tooltip="Select rows to be displayed in any DataViz boxes dragged off of this one." /> )} @@ -391,7 +407,7 @@ export class TableBox extends ObservableReactComponent<TableBoxProps> { ? 'darkgreen' : this._props.axes.length > 2 && this._props.axes.lastElement() === col ? 'darkred' - : this._props.axes.lastElement() === col || (this._props.axes.length > 2 && this._props.axes[1] == col) + : this._props.axes.lastElement() === col || (this._props.axes.length > 2 && this._props.axes[1] === col) ? 'darkblue' : undefined, background: this.settingTitle @@ -400,7 +416,7 @@ export class TableBox extends ObservableReactComponent<TableBoxProps> { ? '#E3fbdb' : this._props.axes.length > 2 && this._props.axes.lastElement() === col ? '#Fbdbdb' - : this._props.axes.lastElement() === col || (this._props.axes.length > 2 && this._props.axes[1] == col) + : this._props.axes.lastElement() === col || (this._props.axes.length > 2 && this._props.axes[1] === col) ? '#c6ebf7' : undefined, fontWeight: 'bolder', @@ -424,11 +440,11 @@ export class TableBox extends ObservableReactComponent<TableBoxProps> { background: NumListCast(this._props.layoutDoc.dataViz_highlitedRows).includes(rowId) ? 'lightYellow' : NumListCast(this._props.layoutDoc.dataViz_selectedRows).includes(rowId) ? 'lightgrey' : '', }}> {this.columns.map(col => { - var colSelected = false; - if (this._props.axes.length > 2) colSelected = this._props.axes[0] == col || this._props.axes[1] == col || this._props.axes[2] == col; - else if (this._props.axes.length > 1) colSelected = this._props.axes[0] == col || this._props.axes[1] == col; - else if (this._props.axes.length > 0) colSelected = this._props.axes[0] == col; - if (this._props.titleCol == col) colSelected = true; + let colSelected = false; + if (this._props.axes.length > 2) colSelected = this._props.axes[0] === col || this._props.axes[1] === col || this._props.axes[2] === col; + else if (this._props.axes.length > 1) colSelected = this._props.axes[0] === col || this._props.axes[1] === col; + else if (this._props.axes.length > 0) colSelected = this._props.axes[0] === col; + if (this._props.titleCol === col) colSelected = true; return ( <td key={this.columns.indexOf(col)} style={{ border: colSelected ? '3px solid black' : '1px solid black', fontWeight: colSelected ? 'bolder' : 'normal' }}> <div className="tableBox-cell">{this._props.records[rowId][col]}</div> @@ -443,10 +459,10 @@ export class TableBox extends ObservableReactComponent<TableBoxProps> { </div> </div> ); - } else - return ( - // when it is a brushed table and the incoming table doesn't have any rows selected - <div className="chart-container">Selected rows of data from the incoming DataVizBox to display.</div> - ); + } + return ( + // when it is a brushed table and the incoming table doesn't have any rows selected + <div className="chart-container">Selected rows of data from the incoming DataVizBox to display.</div> + ); } } diff --git a/src/client/views/nodes/DataVizBox/utils/D3Utils.ts b/src/client/views/nodes/DataVizBox/utils/D3Utils.ts index 336935d23..be05c3529 100644 --- a/src/client/views/nodes/DataVizBox/utils/D3Utils.ts +++ b/src/client/views/nodes/DataVizBox/utils/D3Utils.ts @@ -5,11 +5,11 @@ import { DataPoint } from '../components/LineChart'; export const minMaxRange = (dataPts: DataPoint[][]) => { // find the max and min of all the data points - const yMin = d3.min(dataPts, d => d3.min(d, d => Number(d.y))); - const yMax = d3.max(dataPts, d => d3.max(d, d => Number(d.y))); + const yMin = d3.min(dataPts, d => d3.min(d, m => Number(m.y))); + const yMax = d3.max(dataPts, d => d3.max(d, m => Number(m.y))); - const xMin = d3.min(dataPts, d => d3.min(d, d => Number(d.x))); - const xMax = d3.max(dataPts, d => d3.max(d, d => Number(d.x))); + const xMin = d3.min(dataPts, d => d3.min(d, m => Number(m.x))); + const xMax = d3.max(dataPts, d => d3.max(d, m => Number(m.x))); return { xMin, xMax, yMin, yMax }; }; @@ -20,18 +20,15 @@ export const scaleCreatorCategorical = (labels: string[], range: number[]) => { return scale; }; -export const scaleCreatorNumerical = (domA: number, domB: number, rangeA: number, rangeB: number) => { - return d3.scaleLinear().domain([domA, domB]).range([rangeA, rangeB]); -}; +export const scaleCreatorNumerical = (domA: number, domB: number, rangeA: number, rangeB: number) => d3.scaleLinear().domain([domA, domB]).range([rangeA, rangeB]); -export const createLineGenerator = (xScale: d3.ScaleLinear<number, number, never>, yScale: d3.ScaleLinear<number, number, never>) => { +export const createLineGenerator = (xScale: d3.ScaleLinear<number, number, never>, yScale: d3.ScaleLinear<number, number, never>) => // TODO: nda - look into the different types of curves - return d3 + d3 .line<DataPoint>() .x(d => xScale(d.x)) .y(d => yScale(d.y)) .curve(d3.curveMonotoneX); -}; export const xAxisCreator = (g: d3.Selection<SVGGElement, unknown, null, undefined>, height: number, xScale: d3.ScaleLinear<number, number, never>) => { g.attr('class', 'x-axis').attr('transform', `translate(0,${height})`).call(d3.axisBottom(xScale).tickSize(15)); @@ -48,7 +45,7 @@ export const xGrid = (g: d3.Selection<SVGGElement, unknown, null, undefined>, he d3 .axisBottom(scale) .tickSize(-height) - .tickFormat((a, b) => '') + .tickFormat((/* a, b */) => '') ); }; @@ -57,10 +54,16 @@ export const yGrid = (g: d3.Selection<SVGGElement, unknown, null, undefined>, wi d3 .axisLeft(scale) .tickSize(-width) - .tickFormat((a, b) => '') + .tickFormat((/* a, b */) => '') ); }; export const drawLine = (p: d3.Selection<SVGPathElement, unknown, null, undefined>, dataPts: DataPoint[], lineGen: d3.Line<DataPoint>, extra: boolean) => { - p.datum(dataPts).attr('fill', 'none').attr('stroke', 'rgba(53, 162, 235, 0.5)').attr('stroke-width', 2).attr('stroke', extra? 'blue' : 'black').attr('class', 'line').attr('d', lineGen); + p.datum(dataPts) + .attr('fill', 'none') + .attr('stroke', 'rgba(53, 162, 235, 0.5)') + .attr('stroke-width', 2) + .attr('stroke', extra ? 'blue' : 'black') + .attr('class', 'line') + .attr('d', lineGen); }; diff --git a/src/client/views/nodes/DocumentContentsView.tsx b/src/client/views/nodes/DocumentContentsView.tsx index e729e2fa2..ec9db8480 100644 --- a/src/client/views/nodes/DocumentContentsView.tsx +++ b/src/client/views/nodes/DocumentContentsView.tsx @@ -1,9 +1,11 @@ +/* eslint-disable react/require-default-props */ import { computed, makeObservable } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; import JsxParser from 'react-jsx-parser'; import * as XRegExp from 'xregexp'; -import { OmitKeys, Without, emptyPath } from '../../../Utils'; +import { OmitKeys } from '../../../ClientUtils'; +import { Without, emptyPath } from '../../../Utils'; import { Doc, Opt } from '../../../fields/Doc'; import { AclPrivate, DocData } from '../../../fields/DocSymbols'; import { ScriptField } from '../../../fields/ScriptField'; @@ -17,10 +19,7 @@ import { CollectionView } from '../collections/CollectionView'; import { CollectionFreeFormView } from '../collections/collectionFreeForm/CollectionFreeFormView'; import { CollectionSchemaView } from '../collections/collectionSchema/CollectionSchemaView'; import { SchemaRowBox } from '../collections/collectionSchema/SchemaRowBox'; -import { PresElementBox } from '../nodes/trails/PresElementBox'; import { SearchBox } from '../search/SearchBox'; -import { DashWebRTCVideo } from '../webcam/DashWebRTCVideo'; -import { YoutubeBox } from './../../apis/youtube/YoutubeBox'; import { AudioBox } from './AudioBox'; import { ComparisonBox } from './ComparisonBox'; import { DataVizBox } from './DataVizBox/DataVizBox'; @@ -32,7 +31,6 @@ import { FunctionPlotBox } from './FunctionPlotBox'; import { ImageBox } from './ImageBox'; import { KeyValueBox } from './KeyValueBox'; import { LabelBox } from './LabelBox'; -import { LinkAnchorBox } from './LinkAnchorBox'; import { LinkBox } from './LinkBox'; import { LoadingBox } from './LoadingBox'; import { MapBox } from './MapBox/MapBox'; @@ -47,6 +45,7 @@ import { WebBox } from './WebBox'; import { FormattedTextBox } from './formattedText/FormattedTextBox'; import { ImportElementBox } from './importBox/ImportElementBox'; import { PresBox } from './trails/PresBox'; +import { PresElementBox } from './trails/PresElementBox'; type BindingProps = Without<FieldViewProps, 'fieldKey'>; export interface JsxBindings { @@ -71,8 +70,8 @@ interface HTMLtagProps { children?: JSX.Element[]; } -//"<HTMLdiv borderRadius='100px' onClick={this.bannerColor=this.bannerColor==='red'?'green':'red'} overflow='hidden' position='absolute' width='100%' height='100%' transform='rotate({2*this.x+this.y}deg)'> <ImageBox {...props} fieldKey={'data'}/> <HTMLspan width='200px' top='0' height='35px' textAlign='center' paddingTop='10px' transform='translate(-40px, 45px) rotate(-45deg)' position='absolute' color='{this.bannerColor===`green`?`light`:`dark`}blue' backgroundColor='{this.bannerColor===`green`?`dark`:`light`}blue'> {this.title}</HTMLspan></HTMLdiv>" -//"<HTMLdiv borderRadius='100px' overflow='hidden' position='absolute' width='100%' height='100%' +// "<HTMLdiv borderRadius='100px' onClick={this.bannerColor=this.bannerColor==='red'?'green':'red'} overflow='hidden' position='absolute' width='100%' height='100%' transform='rotate({2*this.x+this.y}deg)'> <ImageBox {...props} fieldKey={'data'}/> <HTMLspan width='200px' top='0' height='35px' textAlign='center' paddingTop='10px' transform='translate(-40px, 45px) rotate(-45deg)' position='absolute' color='{this.bannerColor===`green`?`light`:`dark`}blue' backgroundColor='{this.bannerColor===`green`?`dark`:`light`}blue'> {this.title}</HTMLspan></HTMLdiv>" +// "<HTMLdiv borderRadius='100px' overflow='hidden' position='absolute' width='100%' height='100%' // transform='rotate({2*this.x+this.y}deg)' // onClick = { this.bannerColor = this.bannerColor === 'red' ? 'green' : 'red' } > // <ImageBox {...props} fieldKey={'data'}/> @@ -85,22 +84,21 @@ interface HTMLtagProps { // </HTMLdiv>" @observer export class HTMLtag extends React.Component<HTMLtagProps> { - click = (e: React.MouseEvent) => { + click = () => { const clickScript = (this.props as any).onClick as Opt<ScriptField>; - clickScript?.script.run({ this: this.props.Document, self: this.props.Document, scale: this.props.scaling }); + clickScript?.script.run({ this: this.props.Document, scale: this.props.scaling }); }; onInput = (e: React.FormEvent<HTMLDivElement>) => { const onInputScript = (this.props as any).onInput as Opt<ScriptField>; - onInputScript?.script.run({ this: this.props.Document, self: this.props.Document, value: (e.target as any).textContent }); + onInputScript?.script.run({ this: this.props.Document, value: (e.target as any).textContent }); }; render() { const style: { [key: string]: any } = {}; const divKeys = OmitKeys(this.props, ['children', 'dragStarting', 'dragEnding', 'htmltag', 'scaling', 'Document', 'key', 'onInput', 'onClick', '__proto__']).omit; - const replacer = (match: any, expr: string, offset: any, string: any) => { + const replacer = (match: any, expr: string) => // bcz: this executes a script to convert a property expression string: { script } into a value - return (ScriptField.MakeFunction(expr, { self: Doc.name, this: Doc.name, scale: 'number' })?.script.run({ self: this.props.Document, this: this.props.Document, scale: this.props.scaling }).result as string) || ''; - }; - Object.keys(divKeys).map((prop: string) => { + (ScriptField.MakeFunction(expr, { this: Doc.name, scale: 'number' })?.script.run({ this: this.props.Document, scale: this.props.scaling }).result as string) || ''; + Object.keys(divKeys).forEach((prop: string) => { const p = (this.props as any)[prop] as string; style[prop] = p?.replace(/{([^.'][^}']+)}/g, replacer); }); @@ -122,7 +120,6 @@ export class DocumentContentsView extends ObservableReactComponent<DocumentConte super(props); makeObservable(this); } - @computed get layout(): string { TraceMobx(); if (this._props.LayoutTemplateString) return this._props.LayoutTemplateString; @@ -158,7 +155,7 @@ export class DocumentContentsView extends ObservableReactComponent<DocumentConte 'dontCenter', 'DataTransition', 'contextMenuItems', - //'onClick', // don't need to omit this since it will be set + // 'onClick', // don't need to omit this since it will be set 'onDoubleClickScript', 'onPointerDownScript', 'onPointerUpScript', @@ -187,21 +184,16 @@ export class DocumentContentsView extends ObservableReactComponent<DocumentConte let layoutFrame = this.layout; // 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; - }; + const replacer = (match: any, prefix: string, expr: string, postfix: string) => prefix + ((ScriptField.MakeFunction(expr, { 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}'`; - }; + const replacer2 = (match: any, p1: string) => `<HTMLtag Document={props.Document} scaling='${this._props.NativeDimScaling?.() || 1}' htmltag='${p1}'`; layoutFrame = layoutFrame.replace(/<HTML([a-zA-Z0-9_-]+)/g, replacer2); // replace /HTML<tag> with </HTMLdiv> as in: </HTMLdiv> becomes </HTMLtag> - const replacer3 = (match: any, p1: string, offset: any, string: any) => { - return `</HTMLtag`; - }; + const replacer3 = (/* match: any, p1: string, offset: any, string: any */) => `</HTMLtag`; + layoutFrame = layoutFrame.replace(/<\/HTML([a-zA-Z0-9_-]+)/g, replacer3); // add onClick function to props @@ -211,7 +203,7 @@ export class DocumentContentsView extends ObservableReactComponent<DocumentConte const code = XRegExp.matchRecursive(splits[1], '{', '}', '', { valueNames: ['between', 'left', 'match', 'right', 'between'] }); layoutFrame = splits[0] + ` ${func}={props.${func}} ` + splits[1].substring(code[1].end + 1); const script = code[1].value.replace(/^‘/, '').replace(/’$/, ''); // ‘’ are not valid quotes in javascript so get rid of them -- they may be present to make it easier to write complex scripts - see headerTemplate in currentUserUtils.ts - return ScriptField.MakeScript(script, { this: Doc.name, self: Doc.name, scale: 'number', value: 'string' }); + return ScriptField.MakeScript(script, { this: Doc.name, scale: 'number', value: 'string' }); } return undefined; // add input function to props @@ -250,12 +242,9 @@ export class DocumentContentsView extends ObservableReactComponent<DocumentConte AudioBox, RecordingBox, PresBox, - YoutubeBox, PresElementBox, SearchBox, FunctionPlotBox, - DashWebRTCVideo, - LinkAnchorBox, InkingStroke, LinkBox, ScriptingBox, @@ -272,7 +261,7 @@ export class DocumentContentsView extends ObservableReactComponent<DocumentConte }} bindings={bindings} jsx={layoutFrame} - showWarnings={true} + showWarnings onError={(test: any) => { console.log('DocumentContentsView:' + test, bindings, layoutFrame); }} diff --git a/src/client/views/nodes/DocumentIcon.tsx b/src/client/views/nodes/DocumentIcon.tsx index 4a22766cc..23d934edb 100644 --- a/src/client/views/nodes/DocumentIcon.tsx +++ b/src/client/views/nodes/DocumentIcon.tsx @@ -3,12 +3,11 @@ import { action, makeObservable, observable } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; import { factory } from 'typescript'; -import { Field } from '../../../fields/Doc'; +import { FieldType } from '../../../fields/Doc'; import { Id } from '../../../fields/FieldSymbols'; -import { DocumentManager } from '../../util/DocumentManager'; +import { StrCast } from '../../../fields/Types'; import { Transformer, ts } from '../../util/Scripting'; -import { SettingsManager } from '../../util/SettingsManager'; -import { LightboxView } from '../LightboxView'; +import { SnappingManager } from '../../util/SnappingManager'; import { ObservableReactComponent } from '../ObservableReactComponent'; import { DocumentView } from './DocumentView'; @@ -24,26 +23,23 @@ export class DocumentIcon extends ObservableReactComponent<DocumentIconProps> { makeObservable(this); } - static get DocViews() { - return LightboxView.LightboxDoc ? DocumentManager.Instance.DocumentViews.filter(v => LightboxView.Contains(v)) : DocumentManager.Instance.DocumentViews; - } render() { - const view = this._props.view; - const { left, top, right, bottom } = view.getBounds || { left: 0, top: 0, right: 0, bottom: 0 }; + const { view } = this._props; + const { left, top, right } = view.getBounds || { left: 0, top: 0, right: 0, bottom: 0 }; return ( <div className="documentIcon-outerDiv" - onPointerEnter={action(e => (this._hovered = true))} - onPointerLeave={action(e => (this._hovered = false))} + onPointerEnter={action(() => { this._hovered = true; })} // prettier-ignore + onPointerLeave={action(() => { this._hovered = false; })} // prettier-ignore style={{ pointerEvents: 'all', opacity: this._hovered ? 0.3 : 1, position: 'absolute', - background: SettingsManager.userBackgroundColor, + background: SnappingManager.userBackgroundColor, transform: `translate(${(left + right) / 2}px, ${top}px)`, }}> - <Tooltip title={<>{this._props.view.Document.title}</>}> + <Tooltip title={<div>{StrCast(this._props.view.Document?.title)}</div>}> <p>d{this._props.index}</p> </Tooltip> </div> @@ -56,40 +52,40 @@ export class DocumentIconContainer extends React.Component { public static getTransformer(): Transformer { const usedDocuments = new Set<number>(); return { - transformer: context => { - return root => { - function visit(node: ts.Node) { - node = ts.visitEachChild(node, visit, context); + transformer: context => root => { + function visit(nodeIn: ts.Node) { + const node = ts.visitEachChild(nodeIn, visit, context); - if (ts.isIdentifier(node)) { - const isntPropAccess = !ts.isPropertyAccessExpression(node.parent) || node.parent.expression === node; - const isntPropAssign = !ts.isPropertyAssignment(node.parent) || node.parent.name !== node; - const isntParameter = !ts.isParameter(node.parent); - if (isntPropAccess && isntPropAssign && isntParameter && !(node.text in globalThis)) { - const match = node.text.match(/d([0-9]+)/); - if (match) { - const m = parseInt(match[1]); - const doc = DocumentIcon.DocViews[m].Document; - usedDocuments.add(m); - return factory.createIdentifier(`idToDoc("${doc[Id]}")`); - } + if (ts.isIdentifier(node)) { + const isntPropAccess = !ts.isPropertyAccessExpression(node.parent) || node.parent.expression === node; + const isntPropAssign = !ts.isPropertyAssignment(node.parent) || node.parent.name !== node; + const isntParameter = !ts.isParameter(node.parent); + if (isntPropAccess && isntPropAssign && isntParameter && !(node.text in globalThis)) { + const match = node.text.match(/d([0-9]+)/); + if (match) { + const m = parseInt(match[1]); + const doc = DocumentView.allViews()[m].Document; + usedDocuments.add(m); + return factory.createIdentifier(`idToDoc("${doc[Id]}")`); } } - - return node; } - return ts.visitNode(root, visit); - }; + + return node; + } + return ts.visitNode(root, visit); }, getVars() { - const docs = DocumentIcon.DocViews; - const capturedVariables: { [name: string]: Field } = {}; - usedDocuments.forEach(index => (capturedVariables[`d${index}`] = docs.length > index ? docs[index].Document : `d${index}`)); + const docs = DocumentView.allViews(); + const capturedVariables: { [name: string]: FieldType } = {}; + usedDocuments.forEach(index => { + capturedVariables[`d${index}`] = docs.length > index ? docs[index].Document : `d${index}`; + }); return capturedVariables; }, }; } render() { - return DocumentIcon.DocViews.map((dv, i) => <DocumentIcon key={i} index={i} view={dv} />); + return DocumentView.allViews().map((dv, i) => <DocumentIcon key={dv.DocUniqueId} index={i} view={dv} />); } } diff --git a/src/client/views/nodes/DocumentLinksButton.tsx b/src/client/views/nodes/DocumentLinksButton.tsx index 2a68d2bf6..0c5156339 100644 --- a/src/client/views/nodes/DocumentLinksButton.tsx +++ b/src/client/views/nodes/DocumentLinksButton.tsx @@ -1,23 +1,23 @@ +/* eslint-disable jsx-a11y/no-static-element-interactions */ +/* eslint-disable jsx-a11y/click-events-have-key-events */ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { Tooltip } from '@mui/material'; import { action, computed, makeObservable, observable, runInAction } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; -import { StopEvent, emptyFunction, returnFalse, setupMoveUpEvents } from '../../../Utils'; +import { StopEvent, returnFalse, setupMoveUpEvents } from '../../../ClientUtils'; +import { emptyFunction } from '../../../Utils'; import { Doc } from '../../../fields/Doc'; -import { StrCast } from '../../../fields/Types'; -import { DocUtils } from '../../documents/Documents'; +import { DocUtils } from '../../documents/DocUtils'; import { DragManager } from '../../util/DragManager'; -import { Hypothesis } from '../../util/HypothesisUtils'; import { LinkManager } from '../../util/LinkManager'; import { UndoManager, undoBatch } from '../../util/UndoManager'; import { ObservableReactComponent } from '../ObservableReactComponent'; +import { PinProps } from '../PinFuncs'; import './DocumentLinksButton.scss'; import { DocumentView } from './DocumentView'; import { LinkDescriptionPopup } from './LinkDescriptionPopup'; import { TaskCompletionBox } from './TaskCompletedBox'; -import { PinProps } from './trails'; -import { DocData } from '../../../fields/DocSymbols'; interface DocumentLinksButtonProps { View: DocumentView; @@ -25,20 +25,22 @@ interface DocumentLinksButtonProps { AlwaysOn?: boolean; InMenu?: boolean; OnHover?: boolean; - StartLink?: boolean; //whether the link HAS been started (i.e. now needs to be completed) + StartLink?: boolean; // whether the link HAS been started (i.e. now needs to be completed) ShowCount?: boolean; 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 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; + // eslint-disable-next-line no-use-before-define public static _instance: DocButtonState | undefined; public static get Instance() { + // eslint-disable-next-line no-return-assign return DocButtonState._instance ?? (DocButtonState._instance = new DocButtonState()); } constructor() { @@ -49,7 +51,7 @@ export class DocButtonState { export class DocumentLinksButton extends ObservableReactComponent<DocumentLinksButtonProps> { private _linkButton = React.createRef<HTMLDivElement>(); public static get StartLink() { return DocButtonState.Instance.StartLink; } // prettier-ignore - public static set StartLink(value) { runInAction(() => (DocButtonState.Instance.StartLink = value)); } // 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; @@ -87,12 +89,14 @@ export class DocumentLinksButton extends ObservableReactComponent<DocumentLinksB e, this.onLinkButtonMoved, emptyFunction, - action((e, doubleTap) => { + action((clickEv, doubleTap) => { doubleTap && DocumentView.showBackLinks(this._props.View.Document); }), undefined, undefined, - action(() => (DocButtonState.Instance.LinkEditorDocView = this._props.View)) + action(() => { + DocButtonState.Instance.LinkEditorDocView = this._props.View; + }) ); }; @@ -102,9 +106,9 @@ export class DocumentLinksButton extends ObservableReactComponent<DocumentLinksB e, this.onLinkButtonMoved, emptyFunction, - action((e, doubleTap) => { + action((clickEv, doubleTap) => { if (doubleTap && this._props.InMenu && this._props.StartLink) { - //action(() => Doc.BrushDoc(this._props.View.Document)); + // action(() => Doc.BrushDoc(this._props.View.Document)); if (DocumentLinksButton.StartLink === this._props.View.Document) { DocumentLinksButton.StartLink = undefined; DocumentLinksButton.StartLinkView = undefined; @@ -118,7 +122,7 @@ export class DocumentLinksButton extends ObservableReactComponent<DocumentLinksB }; @undoBatch - onLinkClick = (e: React.MouseEvent): void => { + onLinkClick = (): void => { if (this._props.InMenu && this._props.StartLink) { DocumentLinksButton.AnnotationId = undefined; DocumentLinksButton.AnnotationUri = undefined; @@ -126,7 +130,7 @@ export class DocumentLinksButton extends ObservableReactComponent<DocumentLinksB DocumentLinksButton.StartLink = undefined; DocumentLinksButton.StartLinkView = undefined; } else { - //if this LinkButton's Document is undefined + // if this LinkButton's Document is undefined DocumentLinksButton.StartLink = this._props.View.Document; DocumentLinksButton.StartLinkView = this._props.View; } @@ -139,7 +143,7 @@ export class DocumentLinksButton extends ObservableReactComponent<DocumentLinksB e, returnFalse, emptyFunction, - action(e => DocumentLinksButton.finishLinkClick(e.clientX, e.clientY, DocumentLinksButton.StartLink, this._props.View.Document, true, this._props.View)) + action(clickEv => DocumentLinksButton.finishLinkClick(clickEv.clientX, clickEv.clientY, DocumentLinksButton.StartLink, this._props.View.Document, true, this._props.View)) ); }; @@ -151,25 +155,17 @@ export class DocumentLinksButton extends ObservableReactComponent<DocumentLinksB DocumentLinksButton.StartLinkView = undefined; DocumentLinksButton.AnnotationId = undefined; DocumentLinksButton.AnnotationUri = undefined; - //!this._props.StartLink + // !this._props.StartLink } else if (startLink !== endLink) { + // eslint-disable-next-line no-param-reassign endLink = endLinkView?.ComponentView?.getAnchor?.(true, pinProps) || endLink; + // eslint-disable-next-line no-param-reassign startLink = DocumentLinksButton.StartLinkView?.ComponentView?.getAnchor?.(true) || startLink; const linkDoc = DocUtils.MakeLink(startLink, endLink, { link_relationship: DocumentLinksButton.AnnotationId ? 'hypothes.is annotation' : undefined }); LinkManager.Instance.currentLink = linkDoc; if (linkDoc) { - if (DocumentLinksButton.AnnotationId && DocumentLinksButton.AnnotationUri) { - // if linking from a Hypothes.is annotation - const linkDocData = linkDoc[DocData]; - linkDocData.linksToAnnotation = true; - linkDocData.annotationId = DocumentLinksButton.AnnotationId; - linkDocData.annotationUri = DocumentLinksButton.AnnotationUri; - const dashHyperlink = Doc.globalServerPath(startIsAnnotation ? endLink : startLink); - Hypothesis.makeLink(StrCast(startIsAnnotation ? endLink.title : startLink.title), dashHyperlink, DocumentLinksButton.AnnotationId, startIsAnnotation ? startLink : endLink); // edit annotation to add a Dash hyperlink to the linked doc - } - TaskCompletionBox.textDisplayed = 'Link Created'; TaskCompletionBox.popupX = screenX; TaskCompletionBox.popupY = screenY - 133; @@ -192,7 +188,9 @@ export class DocumentLinksButton extends ObservableReactComponent<DocumentLinksB } setTimeout( - action(() => (TaskCompletionBox.taskCompleted = false)), + action(() => { + TaskCompletionBox.taskCompleted = false; + }), 2500 ); } @@ -242,13 +240,13 @@ export class DocumentLinksButton extends ObservableReactComponent<DocumentLinksB 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.Document ? ( //if the origin node is not this node - <div className={'documentLinksButton-endLink'} ref={this._linkButton} onPointerDown={DocumentLinksButton.StartLink && this.completeLink}> + {!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> ) : null} @@ -262,7 +260,7 @@ export class DocumentLinksButton extends ObservableReactComponent<DocumentLinksB const buttonTitle = 'Tap to view links; double tap to open link collection'; 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 + // 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 : ( <div className="documentLinksButton-wrapper" diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx index 21c7f3079..3191e04db 100644 --- a/src/client/views/nodes/DocumentView.tsx +++ b/src/client/views/nodes/DocumentView.tsx @@ -1,12 +1,16 @@ +/* eslint-disable no-use-before-define */ +/* eslint-disable react/jsx-props-no-spreading */ +/* eslint-disable jsx-a11y/no-static-element-interactions */ import { IconProp } from '@fortawesome/fontawesome-svg-core'; import { Howl } from 'howler'; import { IReactionDisposer, action, computed, makeObservable, observable, reaction, runInAction } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; import { Bounce, Fade, Flip, JackInTheBox, Roll, Rotate, Zoom } from 'react-awesome-reveal'; -import { DivWidth, Utils, emptyFunction, isTargetChildOf as isParentOf, lightOrDark, returnEmptyString, returnFalse, returnTrue, returnVal, simulateMouseClick } from '../../../Utils'; -import { Doc, DocListCast, Field, Opt, StrListCast } from '../../../fields/Doc'; -import { AclPrivate, Animation, AudioPlay, DocData, DocViews } from '../../../fields/DocSymbols'; +import { ClientUtils, DivWidth, isTargetChildOf as isParentOf, lightOrDark, returnFalse, returnVal, simulateMouseClick } from '../../../ClientUtils'; +import { Utils, emptyFunction, emptyPath } from '../../../Utils'; +import { Doc, DocListCast, Field, FieldType, Opt, StrListCast } from '../../../fields/Doc'; +import { AclAdmin, AclEdit, AclPrivate, Animation, AudioPlay, DocData, DocViews } from '../../../fields/DocSymbols'; import { Id } from '../../../fields/FieldSymbols'; import { InkTool } from '../../../fields/InkField'; import { List } from '../../../fields/List'; @@ -16,22 +20,17 @@ import { ScriptField } from '../../../fields/ScriptField'; import { BoolCast, Cast, DocCast, NumCast, ScriptCast, StrCast } from '../../../fields/Types'; import { AudioField } from '../../../fields/URLField'; import { GetEffectiveAcl, TraceMobx } from '../../../fields/util'; +import { AudioAnnoState } from '../../../server/SharedMediaTypes'; import { DocServer } from '../../DocServer'; -import { Networking } from '../../Network'; -import { GooglePhotos } from '../../apis/google_docs/GooglePhotosClientUtils'; +import { DocUtils, FollowLinkScript } from '../../documents/DocUtils'; import { CollectionViewType, DocumentType } from '../../documents/DocumentTypes'; -import { DocUtils, Docs } from '../../documents/Documents'; -import { DictationManager } from '../../util/DictationManager'; -import { DocumentManager } from '../../util/DocumentManager'; -import { DragManager, dropActionType } from '../../util/DragManager'; +import { Docs } from '../../documents/Documents'; +import { DragManager } from '../../util/DragManager'; +import { dropActionType } from '../../util/DropActionTypes'; import { MakeTemplate, makeUserTemplateButton } from '../../util/DropConverter'; -import { FollowLinkScript } from '../../util/LinkFollower'; -import { LinkManager } from '../../util/LinkManager'; +import { UPDATE_SERVER_CACHE } from '../../util/LinkManager'; import { ScriptingGlobals } from '../../util/ScriptingGlobals'; import { SearchUtil } from '../../util/SearchUtil'; -import { SelectionManager } from '../../util/SelectionManager'; -import { SettingsManager } from '../../util/SettingsManager'; -import { SharingManager } from '../../util/SharingManager'; import { SnappingManager } from '../../util/SnappingManager'; import { UndoManager, undoBatch, undoable } from '../../util/UndoManager'; import { ContextMenu } from '../ContextMenu'; @@ -39,53 +38,18 @@ import { ContextMenuProps } from '../ContextMenuItem'; import { DocComponent, ViewBoxInterface } from '../DocComponent'; import { EditableView } from '../EditableView'; import { FieldsDropdown } from '../FieldsDropdown'; -import { GestureOverlay } from '../GestureOverlay'; import { LightboxView } from '../LightboxView'; -import { AudioAnnoState, StyleProp } from '../StyleProvider'; +import { PinProps } from '../PinFuncs'; +import { StyleProp } from '../StyleProp'; import { DocumentContentsView, ObserverJsxParser } from './DocumentContentsView'; import { DocumentLinksButton } from './DocumentLinksButton'; import './DocumentView.scss'; import { FieldViewProps, FieldViewSharedProps } from './FieldView'; -import { KeyValueBox } from './KeyValueBox'; -import { LinkAnchorBox } from './LinkAnchorBox'; +import { FocusViewOptions } from './FocusViewOptions'; +import { OpenWhere } from './OpenWhere'; import { FormattedTextBox } from './formattedText/FormattedTextBox'; import { PresEffect, PresEffectDirection } from './trails'; -interface Window { - MediaRecorder: MediaRecorder; -} -declare class MediaRecorder { - constructor(e: any); // whatever MediaRecorder has -} -export enum OpenWhereMod { - none = '', - left = 'left', - right = 'right', - top = 'top', - bottom = 'bottom', - keyvalue = 'keyValue', -} -export enum OpenWhere { - lightbox = 'lightbox', - add = 'add', - addLeft = 'add:left', - addRight = 'add:right', - addBottom = 'add:bottom', - close = 'close', - toggle = 'toggle', - toggleRight = 'toggle:right', - replace = 'replace', - replaceRight = 'replace:right', - replaceLeft = 'replace:left', - inParent = 'inParent', - inParentFromScreen = 'inParentFromScreen', - overlay = 'overlay', - addRightKeyvalue = 'add:right:keyValue', -} - -export function returnEmptyDocViewList() { - return [] as DocumentView[]; -} export interface DocumentViewProps extends FieldViewSharedProps { hideDecorations?: boolean; // whether to suppress all DocumentDecorations when doc is selected hideResizeHandles?: boolean; // whether to suppress resized handles on doc decorations when this document is selected @@ -112,17 +76,20 @@ export interface DocumentViewProps extends FieldViewSharedProps { dragConfig?: (data: DragManager.DocumentDragData) => void; dragStarting?: () => void; dragEnding?: () => void; + + parent?: any; } @observer export class DocumentViewInternal extends DocComponent<FieldViewProps & DocumentViewProps>() { // this makes mobx trace() statements more descriptive public get displayName() { return 'DocumentViewInternal(' + this.Document.title + ')'; } // prettier-ignore public static SelectAfterContextMenu = true; // whether a document should be selected after it's contextmenu is triggered. + /** * This function is filled in by MainView to allow non-viewBox views to add Docs as tabs without * needing to know about/reference MainView */ - public static addDocTabFunc: (doc: Doc, location: OpenWhere) => boolean = returnFalse; + public static addDocTabFunc: (doc: Doc | Doc[], location: OpenWhere) => boolean = returnFalse; private _disposers: { [name: string]: IReactionDisposer } = {}; private _doubleClickTimeout: NodeJS.Timeout | undefined; @@ -146,7 +113,7 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document @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 ) @observable _isContentActive: boolean | undefined = undefined; @observable _pointerEvents: 'none' | 'all' | 'visiblePainted' | undefined = undefined; - @observable _componentView: Opt<ViewBoxInterface> = undefined; // needs to be accessed from DocumentView wrapper class + @observable _componentView: Opt<ViewBoxInterface<FieldViewProps>> = undefined; // needs to be accessed from DocumentView wrapper class @observable _animateScaleTime: Opt<number> = undefined; // milliseconds for animating between views. defaults to 300 if not uset @observable _animateScalingTo = 0; @@ -169,7 +136,7 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document @computed get borderPath() { return this.style(this.Document, StyleProp.BorderPath); } // prettier-ignore @computed get onClickHandler() { - return this._props.onClickScript?.() ?? this._props.onBrowseClickScript?.() ?? ScriptCast(this.Document.onClick, ScriptCast(this.layoutDoc.onClick)); + return this._props.onClickScript?.() ?? ScriptCast(this.Document.onClick, ScriptCast(this.layoutDoc.onClick)); } @computed get onDoubleClickHandler() { return this._props.onDoubleClickScript?.() ?? ScriptCast(this.layoutDoc.onDoubleClick, ScriptCast(this.Document.onDoubleClick)); @@ -183,23 +150,23 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document @computed get disableClickScriptFunc() { const onScriptDisable = this._props.onClickScriptDisable ?? this._componentView?.onClickScriptDisable?.() ?? this.layoutDoc.onClickScriptDisable; - // prettier-ignore return ( + // eslint-disable-next-line no-use-before-define DocumentView.LongPress || onScriptDisable === 'always' || (onScriptDisable !== 'never' && (this.rootSelected() || this._componentView?.isAnyChildContentActive?.())) - ); + ); // prettier-ignore } @computed get _rootSelected() { return this._props.isSelected() || BoolCast(this._props.TemplateDataDocument && this._props.rootSelected?.()); } - /// 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 + /// disable pointer events on content when there's an enabled onClick script (and not in explore mode) and the contents aren't forced active, or if contents are marked inactive @computed get _contentPointerEvents() { TraceMobx(); return this._props.contentPointerEvents ?? ((!this.disableClickScriptFunc && // this.onClickHandler && - !this._props.onBrowseClickScript?.() && + !SnappingManager.ExploreMode && !this.layoutDoc.layout_isSvg && this.isContentActive() !== true) || this.isContentActive() === false) @@ -211,11 +178,10 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document // anchors that are not rendered as DocumentViews (marked as 'layout_unrendered' with their 'annotationOn' set to this document). e.g., // - PDF text regions are rendered as an Annotations without generating a DocumentView, ' // - RTF selections are rendered via Prosemirror and have a mark which contains the Document ID for the annotation link - // - and links to PDF/Web docs at a certain scroll location never create an explicit view. - // For each of these, we create LinkAnchorBox's on the border of the DocumentView. + // - and links to PDF/Web docs at a certain scroll location never create an explicit anchor view. @computed get directLinks() { TraceMobx(); - return LinkManager.Instance.getAllRelatedLinks(this.Document).filter( + return Doc.Links(this.Document).filter( link => (link.link_matchEmbeddings ? link.link_anchor_1 === this.Document : Doc.AreProtosEqual(link.link_anchor_1 as Doc, this.Document)) || (link.link_matchEmbeddings ? link.link_anchor_2 === this.Document : Doc.AreProtosEqual(link.link_anchor_2 as Doc, this.Document)) || @@ -225,11 +191,11 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document } @computed get _allLinks(): Doc[] { TraceMobx(); - return LinkManager.Instance.getAllRelatedLinks(this.Document).filter(link => !link.link_matchEmbeddings || link.link_anchor_1 === this.Document || link.link_anchor_2 === this.Document); + return Doc.Links(this.Document).filter(link => !link.link_matchEmbeddings || link.link_anchor_1 === this.Document || link.link_anchor_2 === this.Document); } @computed get filteredLinks() { - return DocUtils.FilterDocs(this.directLinks, this._props.childFilters?.() ?? [], []).filter(d => d.link_displayLine || Doc.UserDoc().showLinkLines); + return DocUtils.FilterDocs(this.directLinks, this._props.childFilters?.() ?? [], []); } componentWillUnmount() { @@ -237,7 +203,9 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document } componentDidMount() { - runInAction(() => (this._mounted = true)); + runInAction(() => { + this._mounted = true; + }); this.setupHandlers(); this._disposers.contentActive = reaction( () => @@ -249,19 +217,23 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document : Doc.ActiveTool !== InkTool.None || SnappingManager.CanEmbed || this.rootSelected() || this.Document.forceActive || this._componentView?.isAnyChildContentActive?.() || this._props.isContentActive() ? true : undefined, - active => (this._isContentActive = active), + active => { + this._isContentActive = active; + }, { fireImmediately: true } ); this._disposers.pointerevents = reaction( () => this.style(this.Document, StyleProp.PointerEvents), - pointerevents => (this._pointerEvents = pointerevents), + pointerevents => { + this._pointerEvents = pointerevents; + }, { fireImmediately: true } ); } preDrop = (e: Event, de: DragManager.DropEvent, dropAction: dropActionType) => { const dragData = de.complete.docDragData; if (dragData && this.isContentActive() && !this.props.dontRegisterView) { - dragData.dropAction = dropAction ? dropAction : dragData.dropAction; + dragData.dropAction = dropAction || dragData.dropAction; e.stopPropagation(); } }; @@ -281,7 +253,7 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document startDragging(x: number, y: number, dropAction: dropActionType, hideSource = false) { const docView = this._docView; if (this._mainCont.current && docView) { - const views = SelectionManager.Views.filter(dv => dv.ContentDiv); + const views = DocumentView.Selected().filter(dv => dv.ContentDiv); const selected = views.length > 1 && views.some(dv => dv.Document === this.Document) ? views : [docView]; const dragData = new DragManager.DocumentDragData(selected.map(dv => dv.Document)); const screenXf = docView.screenToViewTransform(); @@ -290,8 +262,9 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document dragData.dropAction = dropAction; dragData.removeDocument = this._props.removeDocument; dragData.moveDocument = this._props.moveDocument; - dragData.draggedViews = [docView]; - dragData.canEmbed = this.Document.dragAction ?? this._props.dragAction ? true : false; + dragData.dragEnding = () => docView.props.dragEnding?.(); + dragData.dragStarting = () => docView.props.dragStarting?.(); + dragData.canEmbed = !!(this.Document.dragAction ?? this._props.dragAction); (this._props.dragConfig ?? this._componentView?.dragConfig)?.(dragData); DragManager.StartDocumentDrag( selected.map(dv => dv.ContentDiv!), @@ -308,10 +281,25 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document if (!StrCast(this.layoutDoc._layout_showTitle)) this.layoutDoc._layout_showTitle = 'title'; setTimeout(() => this._titleRef.current?.setIsFocused(true)); // use timeout in case title wasn't shown to allow re-render so that titleref will be defined }; + onBrowseClick = (e: React.MouseEvent) => { + const browseTransitionTime = 500; + DocumentView.DeselectAll(); + DocumentView.showDocument(this.Document, { zoomScale: 0.8, willZoomCentered: true }, (focused: boolean) => { + const options: FocusViewOptions = { pointFocus: { X: e.clientX, Y: e.clientY }, zoomTime: browseTransitionTime }; + if (!focused && this._docView) { + this._docView + .docViewPath() + .reverse() + .forEach(cont => cont.ComponentView?.focus?.(cont.Document, options)); + Doc.linkFollowHighlight(this.Document, false); + } + }); + e.stopPropagation(); + }; onClick = action((e: React.MouseEvent | React.PointerEvent) => { if (this._props.isGroupActive?.() === 'child' && !this._props.isDocumentActive?.()) return; const documentView = this._docView; - if (documentView && !this.Document.ignoreClick && this._props.renderDepth >= 0 && Utils.isClick(e.clientX, e.clientY, this._downX, this._downY, this._downTime)) { + if (documentView && !this.Document.ignoreClick && this._props.renderDepth >= 0 && ClientUtils.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); @@ -333,7 +321,7 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document UndoManager.RunInBatch(() => this.onDoubleClickHandler.script.run(scriptProps, console.log).result?.select && this._props.select(false), 'on double click: ' + this.Document.title); } else if (!Doc.IsSystem(this.Document) && defaultDblclick !== 'ignore') { UndoManager.RunInBatch(() => LightboxView.Instance.AddDocTab(this.Document, OpenWhere.lightbox), 'double tap'); - SelectionManager.DeselectAll(); + DocumentView.DeselectAll(); Doc.UnBrushDoc(this.Document); } else { this._singleClickFunc?.(); @@ -368,6 +356,7 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document if ((clickFunc && waitFordblclick !== 'never') || waitFordblclick === 'always') { this._doubleClickTimeout && clearTimeout(this._doubleClickTimeout); this._doubleClickTimeout = setTimeout(this._singleClickFunc, 300); + // eslint-disable-next-line no-use-before-define } else if (!DocumentView.LongPress) { this._singleClickFunc(); this._singleClickFunc = undefined; @@ -380,8 +369,9 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document onPointerDown = (e: React.PointerEvent): void => { if (this._props.isGroupActive?.() === 'child' && !this._props.isDocumentActive?.()) return; - this._longPressSelector = setTimeout(() => (DocumentView.LongPress && this._props.select(false), 1000)); - if (!GestureOverlay.DownDocView) GestureOverlay.DownDocView = this._docView; + // eslint-disable-next-line no-use-before-define + this._longPressSelector = setTimeout(() => DocumentView.LongPress && this._props.select(false), 1000); + if (!DocumentView.DownDocView) DocumentView.DownDocView = this._docView; this._downX = e.clientX; this._downY = e.clientY; @@ -389,10 +379,8 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document 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.onBrowseClickScript?.() && + if ((this._props.isDocumentActive?.() || this._props.isContentActive?.()) && + !SnappingManager.ExploreMode && !this.Document.ignoreClick && e.button === 0 && !Doc.IsInMyOverlay(this.layoutDoc) @@ -404,7 +392,7 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document if (!this.layoutDoc._lockedPosition && (!this.isContentActive() || BoolCast(this.layoutDoc._dragWhenActive, this._props.dragWhenActive))) { document.addEventListener('pointermove', this.onPointerMove); } - } + } // prettier-ignore document.addEventListener('pointerup', this.onPointerUp); } }; @@ -412,7 +400,7 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document onPointerMove = (e: PointerEvent): void => { if (e.buttons !== 1 || [InkTool.Highlighter, InkTool.Pen, InkTool.Write].includes(Doc.ActiveTool)) return; - if (!Utils.isClick(e.clientX, e.clientY, this._downX, this._downY, Date.now())) { + if (!ClientUtils.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) && dropActionType.embed) || ((this.Document.dragAction || this._props.dragAction || undefined) as dropActionType)); @@ -430,14 +418,15 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document if (this.onPointerUpHandler?.script) { this.onPointerUpHandler.script.run({ this: this.Document }, console.log); - } else if (e.button === 0 && Utils.isClick(e.clientX, e.clientY, this._downX, this._downY, this._downTime)) { - this._doubleTap = (this.onDoubleClickHandler?.script || this.Document.defaultDoubleClick !== 'ignore') && Date.now() - this._lastTap < Utils.CLICK_TIME; + } else if (e.button === 0 && ClientUtils.isClick(e.clientX, e.clientY, this._downX, this._downY, this._downTime)) { + this._doubleTap = (this.onDoubleClickHandler?.script || this.Document.defaultDoubleClick !== 'ignore') && Date.now() - this._lastTap < ClientUtils.CLICK_TIME; if (!this.isContentActive()) this._lastTap = Date.now(); // don't want to process the start of a double tap if the doucment is selected } + // eslint-disable-next-line no-use-before-define if (DocumentView.LongPress) e.preventDefault(); }; - toggleFollowLink = undoable((zoom?: boolean, setTargetToggle?: boolean): void => { + toggleFollowLink = undoable((): void => { const hadOnClick = this.Document.onClick; this.noOnClick(); this.Document.onClick = hadOnClick ? undefined : FollowLinkScript(); @@ -458,19 +447,17 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document }, 'default on click'); deleteClicked = undoable(() => this._props.removeDocument?.(this.Document), 'delete doc'); - setToggleDetail = undoable( - (scriptFieldKey: 'onClick') => - (this.Document[scriptFieldKey] = ScriptField.MakeScript( - `toggleDetail(documentView, "${StrCast(this.Document.layout_fieldKey) - .replace('layout_', '') - .replace(/^layout$/, 'detail')}")`, - { documentView: 'any' } - )), - 'set toggle detail' - ); + setToggleDetail = undoable((scriptFieldKey: 'onClick') => { + this.Document[scriptFieldKey] = ScriptField.MakeScript( + `toggleDetail(documentView, "${StrCast(this.Document.layout_fieldKey) + .replace('layout_', '') + .replace(/^layout$/, 'detail')}")`, + { documentView: 'any' } + ); + }, 'set toggle detail'); drop = undoable((e: Event, de: DragManager.DropEvent) => { - if (this._props.dontRegisterView || this._props.LayoutTemplateString?.includes(LinkAnchorBox.name)) return false; + if (this._props.dontRegisterView) return false; if (this.Document === Doc.ActiveDashboard) { e.stopPropagation(); e.preventDefault(); @@ -491,7 +478,7 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document if (linkDoc) { de.complete.linkDocument = linkDoc; linkDoc.layout_isSvg = true; - DocumentManager.LinkCommonAncestor(linkDoc)?.ComponentView?.addDocument?.(linkDoc); + DocumentView.linkCommonAncestor(linkDoc)?.ComponentView?.addDocument?.(linkDoc); } } e.stopPropagation(); @@ -505,7 +492,7 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document const input = document.createElement('input'); input.type = 'file'; input.accept = '.zip'; - input.onchange = _e => { + input.onchange = () => { if (input.files) { const batch = UndoManager.StartBatch('importing'); Doc.importDocument(input.files[0]).then(doc => { @@ -523,7 +510,7 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document if (e && this.layoutDoc.layout_hideContextMenu && Doc.noviceMode) { e.preventDefault(); e.stopPropagation(); - //!this._props.isSelected(true) && SelectionManager.SelectView(this.DocumentView(), false); + // !this._props.isSelected(true) && DocumentView.SelectView(this.DocumentView(), false); } // the touch onContextMenu is button 0, the pointer onContextMenu is button 2 if (e) { @@ -535,7 +522,7 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document e.stopPropagation(); e.persist(); - if (!navigator.userAgent.includes('Mozilla') && (Math.abs(this._downX - e?.clientX) > 3 || Math.abs(this._downY - e?.clientY) > 3)) { + if (!navigator.userAgent.includes('Mozilla') && (Math.abs(this._downX - (e?.clientX ?? 0)) > 3 || Math.abs(this._downY - (e?.clientY ?? 0)) > 3)) { return; } } @@ -583,11 +570,15 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document if (this._props.bringToFront) { 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: () => DocumentView.Selected().forEach(dv => dv._props.bringToFront?.(dv.Document, false)), icon: 'arrow-up' }); + zorderItems.push({ description: 'Send to Back', event: () => DocumentView.Selected().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))), + event: undoBatch( + action(() => { + this.layoutDoc._keepZWhenDragged = !this.layoutDoc._keepZWhenDragged; + }) + ), icon: 'hand-point-up', }); !zorders && cm.addItem({ description: 'Z Order...', addDivider: true, noexpand: true, subitems: zorderItems, icon: 'layer-group' }); @@ -597,14 +588,14 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document const existingOnClick = cm.findByDescription('OnClick...'); const onClicks: ContextMenuProps[] = existingOnClick && 'subitems' in existingOnClick ? existingOnClick.subitems : []; - onClicks.push({ description: 'Enter Portal', event: undoable(e => DocUtils.makeIntoPortal(this.Document, this.layoutDoc, this._allLinks), 'make into portal'), icon: 'window-restore' }); + onClicks.push({ description: 'Enter Portal', event: undoable(() => DocUtils.makeIntoPortal(this.Document, this.layoutDoc, this._allLinks), 'make into portal'), icon: 'window-restore' }); !Doc.noviceMode && onClicks.push({ description: 'Toggle Detail', event: this.setToggleDetail, icon: 'concierge-bell' }); if (!this.Document.annotationOn) { 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.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) { + } else if (Doc.Links(this.Document).length) { onClicks.push({ description: 'Restore On Click default', event: () => this.noOnClick(), icon: 'link' }); onClicks.push({ description: 'Follow Link on Click', event: () => this.followLinkOnClick(), icon: 'link' }); !existingOnClick && cm.addItem({ description: 'OnClick...', subitems: onClicks, icon: 'mouse-pointer' }); @@ -613,9 +604,9 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document const funcs: ContextMenuProps[] = []; if (!Doc.noviceMode && this.layoutDoc.onDragStart) { - funcs.push({ description: 'Drag an Embedding', icon: 'edit', event: () => this.Document.dragFactory && (this.layoutDoc.onDragStart = ScriptField.MakeFunction('getEmbedding(this.dragFactory)')) }); - funcs.push({ description: 'Drag a Copy', icon: 'edit', event: () => this.Document.dragFactory && (this.layoutDoc.onDragStart = ScriptField.MakeFunction('getCopy(this.dragFactory, true)')) }); - funcs.push({ description: 'Drag Document', icon: 'edit', event: () => (this.layoutDoc.onDragStart = undefined) }); + funcs.push({ description: 'Drag an Embedding', icon: 'edit', event: () => { this.Document.dragFactory && (this.layoutDoc.onDragStart = ScriptField.MakeFunction('getEmbedding(this.dragFactory)')); } }); // prettier-ignore + funcs.push({ description: 'Drag a Copy', icon: 'edit', event: () => { this.Document.dragFactory && (this.layoutDoc.onDragStart = ScriptField.MakeFunction('getCopy(this.dragFactory, true)')); } }); // prettier-ignore + funcs.push({ description: 'Drag Document', icon: 'edit', event: () => { this.layoutDoc.onDragStart = undefined; } }); // prettier-ignore cm.addItem({ description: 'OnDrag...', noexpand: true, subitems: funcs, icon: 'asterisk' }); } @@ -624,14 +615,8 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document if (!Doc.IsSystem(this.Document)) { if (!Doc.noviceMode) { moreItems.push({ description: 'Make View of Metadata Field', event: () => Doc.MakeMetadataFieldTemplate(this.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.Document).data, listSpec(Doc))) { - moreItems.push({ description: 'Export to Google Photos Album', event: () => GooglePhotos.Export.CollectionToAlbum({ collection: this.Document }).then(console.log), icon: 'caret-square-right' }); - moreItems.push({ description: 'Tag Child Images via Google Photos', event: () => GooglePhotos.Query.TagChildImages(this.Document), icon: 'caret-square-right' }); - moreItems.push({ description: 'Write Back Link to Album', event: () => GooglePhotos.Transactions.AddTextEnrichment(this.Document), icon: 'caret-square-right' }); - } - moreItems.push({ description: 'Copy ID', event: () => Utils.CopyText(Doc.globalServerPath(this.Document)), icon: 'fingerprint' }); + moreItems.push({ description: `${this.Document._chromeHidden ? 'Show' : 'Hide'} Chrome`, event: () => { this.Document._chromeHidden = !this.Document._chromeHidden; }, icon: 'project-diagram' }); // prettier-ignore + moreItems.push({ description: 'Copy ID', event: () => ClientUtils.CopyText(Doc.globalServerPath(this.Document)), icon: 'fingerprint' }); } } @@ -639,8 +624,8 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document } 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.Document) }); - (this.Document._type_collection !== CollectionViewType.Docking || !Doc.noviceMode) && constantItems.push({ description: 'Share', event: () => SharingManager.Instance.open(this._docView), icon: 'users' }); + constantItems.push({ description: 'Zip Export', icon: 'download', event: async () => DocUtils.Zip(this.Document) }); + constantItems.push({ description: 'Share', event: () => DocumentView.ShareOpen(this._docView), icon: 'users' }); if (this._props.removeDocument && Doc.ActiveDashboard !== this.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' }); @@ -655,8 +640,8 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document !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; + let documentationDescription: string | undefined; + let documentationLink: string | undefined; switch (this.Document.type) { case DocumentType.COL: documentationDescription = 'See collection documentation'; @@ -690,6 +675,7 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document documentationDescription = 'See DataViz node documentation'; documentationLink = 'https://brown-dash.github.io/Dash-Documentation/documents/dataViz/'; break; + default: } // Add link to help documentation (unless the doc contents have been overriden in which case the documentation isn't relevant) if (!this.docContents && documentationDescription && documentationLink) { @@ -710,8 +696,8 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document panelHeight = () => this._props.PanelHeight() - this.headerMargin; screenToLocalContent = () => this._props.ScreenToLocalTransform().translate(0, -this.headerMargin); onClickFunc = this.disableClickScriptFunc ? undefined : () => this.onClickHandler; - setHeight = (height: number) => !this._props.suppressSetHeight && (this.layoutDoc._height = Math.min(NumCast(this.layoutDoc._maxHeight, Number.MAX_SAFE_INTEGER), height)); - setContentView = action((view: ViewBoxInterface) => (this._componentView = view)); + setHeight = (height: number) => { !this._props.suppressSetHeight && (this.layoutDoc._height = Math.min(NumCast(this.layoutDoc._maxHeight, Number.MAX_SAFE_INTEGER), height)); } // prettier-ignore + setContentView = action((view: ViewBoxInterface<FieldViewProps>) => { this._componentView = view; }); // prettier-ignore isContentActive = (): boolean | undefined => this._isContentActive; childFilters = () => [...this._props.childFilters(), ...StrListCast(this.layoutDoc.childFilters)]; @@ -726,44 +712,18 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document 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?.() ?? [], []); return filtered.some(link => link._link_displayArrow) ? 0 : undefined; } + default: } return this._props.styleProvider?.(doc, props, property); }; - removeLinkByHiding = (link: Doc) => () => (link.link_displayLine = false); - @computed get allLinkEndpoints() { - // the small blue dots that mark the endpoints of links - if (this._componentView instanceof KeyValueBox || this._props.hideLinkAnchors || this.layoutDoc.layout_hideLinkAnchors || this._props.dontRegisterView || this.layoutDoc.layout_unrendered) return null; - return this.filteredLinks.map(link => ( - <div className="documentView-anchorCont" key={link[Id]}> - <DocumentView - {...this._props} - isContentActive={returnFalse} - Document={link} - containerViewPath={this._props.docViewPath} - PanelWidth={this.anchorPanelWidth} - PanelHeight={this.anchorPanelHeight} - dontRegisterView={false} - layout_showTitle={returnEmptyString} - hideCaptions={true} - hideLinkAnchors={true} - layout_fitWidth={returnTrue} - removeDocument={this.removeLinkByHiding(link)} - styleProvider={this.anchorStyleProvider} - LayoutTemplate={undefined} - LayoutTemplateString={LinkAnchorBox.LayoutString(`link_anchor_${LinkManager.anchorIndex(link, this.Document)}`)} - /> - </div> - )); - } - @computed get viewBoxContents() { 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 noBackground = this.Document.isGroup && !this._componentView?.isUnstyledView?.() && (!this.layoutDoc.backgroundColor || this.layoutDoc.backgroundColor === 'transparent'); return ( <div className="documentView-contentsView" @@ -786,38 +746,35 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document setTitleFocus={this.setTitleFocus} hideClickBehaviors={BoolCast(this.Document.hideClickBehaviors)} /> - {this.layoutDoc.layout_hideAllLinks ? null : this.allLinkEndpoints} </div> ); } captionStyleProvider = (doc: Opt<Doc>, props: Opt<FieldViewProps>, property: string) => this._props?.styleProvider?.(doc, props, property + ':caption'); - fieldsDropdown = (placeholder: string) => { - return ( - <div - ref={action((r: any) => r && (this._titleDropDownInnerWidth = DivWidth(r)))} - onPointerDown={action(e => (this._changingTitleField = true))} - style={{ width: 'max-content', background: SettingsManager.userBackgroundColor, color: SettingsManager.userColor, transformOrigin: 'left', transform: `scale(${this.titleHeight / 30 /* height of Dropdown */})` }}> - <FieldsDropdown - Document={this.Document} - placeholder={placeholder} - selectFunc={action((field: string | number) => { - if (this.layoutDoc.layout_showTitle) { - this.layoutDoc._layout_showTitle = field; - } else if (!this._props.layout_showTitle) { - Doc.UserDoc().layout_showTitle = field; - } - this._changingTitleField = false; - })} - menuClose={action(() => (this._changingTitleField = false))} - /> - </div> - ); - }; + fieldsDropdown = (placeholder: string) => ( + <div + ref={action((r: any) => { r && (this._titleDropDownInnerWidth = DivWidth(r));} )} // prettier-ignore + onPointerDown={action(() => { this._changingTitleField = true; })} // prettier-ignore + style={{ width: 'max-content', background: SnappingManager.userBackgroundColor, color: SnappingManager.userColor, transformOrigin: 'left', transform: `scale(${this.titleHeight / 30 /* height of Dropdown */})` }}> + <FieldsDropdown + Document={this.Document} + placeholder={placeholder} + selectFunc={action((field: string | number) => { + if (this.layoutDoc.layout_showTitle) { + this.layoutDoc._layout_showTitle = field; + } else if (!this._props.layout_showTitle) { + Doc.UserDoc().layout_showTitle = field; + } + this._changingTitleField = false; + })} + menuClose={action(() => { this._changingTitleField = false; })} // prettier-ignore + /> + </div> + ); /** * displays a 'title' at the top of a document. The title contents default to the 'title' field, but can be changed to one or more fields by * setting layout_showTitle using the format: field1[:hover] - **/ + * */ @computed get titleView() { const showTitle = this.layout_showTitle?.split(':')[0]; const showTitleHover = this.layout_showTitle?.includes(':hover'); @@ -825,7 +782,9 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document const targetDoc = showTitle?.startsWith('_') ? this.layoutDoc : this.Document; const background = StrCast( this.layoutDoc.layout_headingColor, - StrCast(SharingManager.Instance.users.find(u => u.user.email === this.dataDoc.author)?.sharingDoc.headingColor, StrCast(Doc.SharingDoc().headingColor, SettingsManager.userBackgroundColor)) + // StrCast(SharingManager.Instance.users.find(u => u.user.email === this.dataDoc.author)?.sharingDoc.headingColor, + StrCast(Doc.SharingDoc().headingColor, SnappingManager.userBackgroundColor) + // ) ); const dropdownWidth = this._titleRef.current?._editing || this._changingTitleField ? Math.max(10, (this._titleDropDownInnerWidth * this.titleHeight) / 30) : 0; const sidebarWidthPercent = +StrCast(this.layoutDoc.layout_sidebarWidthPercent).replace('%', ''); @@ -839,7 +798,7 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document position: this.headerMargin ? 'relative' : 'absolute', height: this.titleHeight, width: 100 - sidebarWidthPercent + '%', - color: background === 'transparent' ? SettingsManager.userColor : lightOrDark(background), + color: background === 'transparent' ? SnappingManager.userColor : lightOrDark(background), background, pointerEvents: (!this.disableClickScriptFunc && this.onClickHandler) || this.Document.ignoreClick ? 'none' : this.isContentActive() || this._props.isDocumentActive?.() ? 'all' : undefined, }}> @@ -860,11 +819,11 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document contents={ showTitle .split(';') - .map(field => Field.toJavascriptString(this.Document[field] as Field)) + .map(field => Field.toJavascriptString(this.Document[field] as FieldType)) .join(' \\ ') || '-unset-' } display="block" - oneLine={true} + oneLine fontSize={(this.titleHeight / 15) * 10} GetValue={() => showTitle @@ -880,7 +839,7 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document Doc.UserDoc().layout_showTitle = input?.substring(1) ? input.substring(1) : 'title'; } } else if (showTitle && !showTitle.includes(';') && !showTitle.includes('Date') && showTitle !== 'author') { - KeyValueBox.SetField(targetDoc, showTitle, input); + Doc.SetField(targetDoc, showTitle, input); } return true; })} @@ -905,10 +864,10 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document xPadding={10} fieldKey={this.layout_showCaption} styleProvider={this.captionStyleProvider} - dontRegisterView={true} + dontRegisterView rootSelected={this.rootSelected} - noSidebar={true} - dontScale={true} + noSidebar + dontScale renderDepth={this._props.renderDepth} isContentActive={this.isContentActive} /> @@ -952,8 +911,7 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document render() { TraceMobx(); - const highlighting = this.highlighting; - const borderPath = this.borderPath; + const { highlighting, borderPath } = this; const boxShadow = !highlighting ? this.boxShadow : highlighting && this.borderRounding && highlighting.highlightStyle !== 'dashed' @@ -968,23 +926,22 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document }); return ( + // eslint-disable-next-line jsx-a11y/click-events-have-key-events <div className={`${DocumentView.ROOT_DIV} docView-hack`} ref={this._mainCont} onContextMenu={this.onContextMenu} onPointerDown={this.onPointerDown} - onClick={this.onClick} - onPointerEnter={e => (!SnappingManager.IsDragging || SnappingManager.CanEmbed) && Doc.BrushDoc(this.Document)} - onPointerOver={e => (!SnappingManager.IsDragging || SnappingManager.CanEmbed) && Doc.BrushDoc(this.Document)} + onClick={SnappingManager.ExploreMode ? this.onBrowseClick : this.onClick} + onPointerEnter={() => (!SnappingManager.IsDragging || SnappingManager.CanEmbed) && Doc.BrushDoc(this.Document)} + onPointerOver={() => (!SnappingManager.IsDragging || SnappingManager.CanEmbed) && Doc.BrushDoc(this.Document)} onPointerLeave={e => !isParentOf(this._contentDiv, document.elementFromPoint(e.nativeEvent.x, e.nativeEvent.y)) && Doc.UnBrushDoc(this.Document)} style={{ borderRadius: this.borderRounding, pointerEvents: this._pointerEvents === 'visiblePainted' ? 'none' : this._pointerEvents, // visible painted means that the underlying doc contents are irregular and will process their own pointer events (otherwise, the contents are expected to fill the entire doc view box so we can handle pointer events here) }}> - <> - {this._componentView instanceof KeyValueBox ? renderDoc : DocumentViewInternal.AnimationEffect(renderDoc, this.Document[Animation], this.Document)} - {borderPath?.jsx} - </> + {this._componentView?.isUnstyledView?.() ? renderDoc : DocumentViewInternal.AnimationEffect(renderDoc, this.Document[Animation])} + {borderPath?.jsx} </div> ); } @@ -994,7 +951,7 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document * @param presEffectDoc presentation effects document that specifies the animation effect parameters * @returns a function that will wrap a JSX animation element wrapping any JSX element */ - public static AnimationEffect(renderDoc: JSX.Element, presEffectDoc: Opt<Doc>, root: Doc) { + public static AnimationEffect(renderDoc: JSX.Element, presEffectDoc: Opt<Doc> /* , root: Doc */) { const dir = presEffectDoc?.presentation_effectDirection ?? presEffectDoc?.followLinkAnimDirection; const effectProps = { left: dir === PresEffectDirection.Left, @@ -1005,10 +962,8 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document delay: 0, duration: Cast(presEffectDoc?.presentation_transition, 'number', Cast(presEffectDoc?.followLinkTransitionTime, 'number', null)), }; - //prettier-ignore + // prettier-ignore 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>; @@ -1016,53 +971,52 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document case PresEffect.Bounce: return <Bounce {...effectProps}>{renderDoc}</Bounce>; case PresEffect.Roll: return <Roll {...effectProps}>{renderDoc}</Roll>; case PresEffect.Lightspeed: return <JackInTheBox {...effectProps}>{renderDoc}</JackInTheBox>; + case PresEffect.None: + default: return renderDoc; } } - public static recordAudioAnnotation(dataDoc: Doc, field: string, onRecording?: (stop: () => void) => void, onEnd?: () => void) { - let gumStream: any; - let recorder: any; - navigator.mediaDevices.getUserMedia({ audio: true }).then(function (stream) { - let audioTextAnnos = Cast(dataDoc[field + '_audioAnnotations_text'], listSpec('string'), null); - if (audioTextAnnos) audioTextAnnos.push(''); - else audioTextAnnos = dataDoc[field + '_audioAnnotations_text'] = new List<string>(['']); - DictationManager.Controls.listen({ - interimHandler: value => (audioTextAnnos[audioTextAnnos.length - 1] = value), - continuous: { indefinite: false }, - }).then(results => { - if (results && [DictationManager.Controls.Infringed].includes(results)) { - DictationManager.Controls.stop(); - } - onEnd?.(); - }); - - gumStream = stream; - recorder = new MediaRecorder(stream); - recorder.ondataavailable = async (e: any) => { - const [{ result }] = await Networking.UploadFilesToServer({ file: e.data }); - if (!(result instanceof Error)) { - const audioField = new AudioField(result.accessPaths.agnostic.client); - const audioAnnos = Cast(dataDoc[field + '_audioAnnotations'], listSpec(AudioField), null); - if (audioAnnos) audioAnnos.push(audioField); - else dataDoc[field + '_audioAnnotations'] = new List([audioField]); - } - }; - recorder.start(); - const stopFunc = () => { - recorder.stop(); - DictationManager.Controls.stop(false); - dataDoc.audioAnnoState = AudioAnnoState.stopped; - gumStream.getAudioTracks()[0].stop(); - }; - if (onRecording) onRecording(stopFunc); - else setTimeout(stopFunc, 5000); - }); - } } @observer export class DocumentView extends DocComponent<DocumentViewProps>() { public static ROOT_DIV = 'documentView-effectsWrapper'; - public get displayName() { return 'DocumentView(' + this.Document?.title + ')'; } // prettier-ignore + // Sharing Manager + public static ShareOpen: (target?: DocumentView, targetDoc?: Doc) => void; + // LinkFollower + public static FollowLink: (linkDoc: Opt<Doc>, sourceDoc: Doc, altKey: boolean) => boolean; + // selection funcs + public static DeselectAll: (except?: Doc) => void | undefined; + public static DeselectView: (dv: DocumentView | undefined) => void | undefined; + public static SelectView: (dv: DocumentView | undefined, extendSelection: boolean) => void | undefined; + public static Selected: () => DocumentView[]; + public static SelectedDocs: () => Doc[]; + public static SelectSchemaDoc: (doc: Doc, deselectAllFirst?: boolean) => void; + public static SelectedSchemaDoc: () => Opt<Doc>; + // view mgr funcs + public static activateTabView: (tabDoc: Doc) => boolean; + public static allViews: () => DocumentView[]; + public static addView: (dv: DocumentView) => void | undefined; + public static removeView: (dv: DocumentView) => void | undefined; + public static addViewRenderedCb: (doc: Opt<Doc>, func: (dv: DocumentView) => any) => boolean; + public static getFirstDocumentView: (toFind: Doc) => DocumentView | undefined; + public static getDocumentView: (target: Doc | undefined, preferredCollection?: DocumentView) => Opt<DocumentView>; + public static getContextPath: (doc: Opt<Doc>, includeExistingViews?: boolean) => Doc[]; + public static getLightboxDocumentView: (toFind: Doc) => Opt<DocumentView>; + public static showDocumentView: (targetDocView: DocumentView, options: FocusViewOptions) => Promise<void>; + public static showDocument: ( + targetDoc: Doc, // document to display + optionsIn: FocusViewOptions, // options for how to navigate to target + finished?: (changed: boolean) => void // func called after focusing on target with flag indicating whether anything needed to be done. + ) => Promise<void>; + public static linkCommonAncestor: (link: Doc) => DocumentView | undefined; + // pin func + public static PinDoc: (docIn: Doc | Doc[], pinProps: PinProps) => void; + // gesture + public static DownDocView: DocumentView | undefined; // the first DocView that receives a pointerdown event. used by GestureOverlay to determine the doc a gesture should apply to. + // media playing + @observable public static CurrentlyPlaying: DocumentView[] = []; + + public get displayName() { return 'DocumentView(' + (this.Document?.title??"") + ')'; } // prettier-ignore public ContentRef = React.createRef<HTMLDivElement>(); private _htmlOverlayEffect: Opt<Doc>; private _disposers: { [name: string]: IReactionDisposer } = {}; @@ -1080,9 +1034,6 @@ export class DocumentView extends DocComponent<DocumentViewProps>() { } public ViewGuid = DocumentView.UniquifyId(LightboxView.Contains(this), Utils.GenerateGuid()); // a unique id associated with the main <div>. used by LinkBox's Xanchor to find the arrowhead locations. public DocUniqueId = DocumentView.UniquifyId(LightboxView.Contains(this), this.Document[Id]); - @computed public static get exploreMode() { - return () => (SnappingManager.ExploreMode ? ScriptField.MakeScript('CollectionBrowseClick(documentView, clientX, clientY)', { documentView: 'any', clientX: 'number', clientY: 'number' })! : undefined); - } constructor(props: DocumentViewProps) { super(props); @@ -1100,7 +1051,7 @@ export class DocumentView extends DocComponent<DocumentViewProps>() { @observable public static LongPress = false; @computed private 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.ComponentView?.isUnstyledView?.(); } @computed private get effectiveNativeWidth() { return this.shouldNotScale ? 0 : this.nativeWidth || NumCast(this.layoutDoc.width); @@ -1151,17 +1102,17 @@ export class DocumentView extends DocComponent<DocumentViewProps>() { componentDidMount() { runInAction(() => this.Document[DocViews].add(this)); this._disposers.onViewMounted = reaction(() => ScriptCast(this.Document.onViewMounted)?.script?.run({ this: this.Document }).result, emptyFunction); - !BoolCast(this.Document.dontRegisterView, this._props.dontRegisterView) && DocumentManager.Instance.AddView(this); + !BoolCast(this.Document.dontRegisterView, this._props.dontRegisterView) && DocumentView.addView(this); } componentWillUnmount() { this._viewTimer && clearTimeout(this._viewTimer); runInAction(() => this.Document[DocViews].delete(this)); Object.values(this._disposers).forEach(disposer => disposer?.()); - !BoolCast(this.Document.dontRegisterView, this._props.dontRegisterView) && DocumentManager.Instance.RemoveView(this); + !BoolCast(this.Document.dontRegisterView, this._props.dontRegisterView) && DocumentView.removeView(this); } - public set IsSelected(val) { runInAction(() => (this._selected = val)); } // prettier-ignore + public set IsSelected(val) { runInAction(() => { this._selected = val; }); } // prettier-ignore public get IsSelected() { return this._selected; } // prettier-ignore public get topMost() { return this._props.renderDepth === 0; } // prettier-ignore public get ContentDiv() { return this._docViewInternal?._contentDiv; } // prettier-ignore @@ -1176,7 +1127,7 @@ export class DocumentView extends DocComponent<DocumentViewProps>() { 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 getBounds(): Opt<{ left: number; top: number; right: number; bottom: number; transition?: string }> { @@ -1189,20 +1140,16 @@ export class DocumentView extends DocComponent<DocumentViewProps>() { const xf = this.screenToContentsTransform().scale(this.nativeScaling).inverse(); const [[left, top], [right, bottom]] = [xf.transformPoint(0, 0), xf.transformPoint(this.panelWidth, this.panelHeight)]; - if (this._props.LayoutTemplateString?.includes(LinkAnchorBox.name)) { - const docuBox = this.ContentDiv.getElementsByClassName('linkAnchorBox-cont'); - if (docuBox.length) return { ...docuBox[0].getBoundingClientRect(), transition: undefined }; - } // transition is returned so that the bounds will 'update' at the end of an animated transition. This is needed by xAnchor in LinkBox const transition = this.docViewPath().find((parent: DocumentView) => parent.DataTransition?.() || parent.ComponentView?.viewTransition?.()); return { left, top, right, bottom, transition: transition?.DataTransition?.() || transition?.ComponentView?.viewTransition?.() }; } @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 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 returnVal(this._props.NativeHeight?.(), Doc.NativeHeight(this.layoutDoc, this._props.TemplateDataDocument, !this.layout_fitWidth)); } @computed public get centeringX() { return this._props.dontCenter?.includes('x') ? 0 : this.Xshift; } // prettier-ignore @computed public get centeringY() { return this._props.dontCenter?.includes('y') ? 0 : this.Yshift; } // prettier-ignore @@ -1211,8 +1158,7 @@ export class DocumentView extends DocComponent<DocumentViewProps>() { * path of DocumentViews hat contains this DocumentView (does not includes this DocumentView thouhg) */ public get containerViewPath() { return this._props.containerViewPath; } // prettier-ignore - public get CollectionFreeFormView() { return this.CollectionFreeFormDocumentView?.CollectionFreeFormView; } // prettier-ignore - public get CollectionFreeFormDocumentView() { return this._props.CollectionFreeFormDocumentView?.(); } // prettier-ignore + public get LocalRotation() { return this._props.LocalRotation?.(); } // prettier-ignore public clearViewTransition = () => { this._viewTimer && clearTimeout(this._viewTimer); @@ -1231,18 +1177,18 @@ export class DocumentView extends DocComponent<DocumentViewProps>() { public iconify(finished?: () => void, animateTime?: number) { this.ComponentView?.updateIcon?.(); const animTime = this._docViewInternal?.animateScaleTime(); - runInAction(() => this._docViewInternal && animateTime !== undefined && (this._docViewInternal._animateScaleTime = animateTime)); + runInAction(() => { this._docViewInternal && animateTime !== undefined && (this._docViewInternal._animateScaleTime = animateTime); }); // prettier-ignore const finalFinished = action(() => { finished?.(); this._docViewInternal && (this._docViewInternal._animateScaleTime = animTime); }); - const layout_fieldKey = Cast(this.Document.layout_fieldKey, 'string', null); - if (layout_fieldKey !== 'layout_icon') { + const layoutFieldKey = Cast(this.Document.layout_fieldKey, 'string', null); + if (layoutFieldKey !== 'layout_icon') { this.switchViews(true, 'icon', finalFinished); - if (layout_fieldKey && layout_fieldKey !== 'layout' && layout_fieldKey !== 'layout_icon') this.Document.deiconifyLayout = layout_fieldKey.replace('layout_', ''); + if (layoutFieldKey && layoutFieldKey !== 'layout' && layoutFieldKey !== 'layout_icon') this.Document.deiconifyLayout = layoutFieldKey.replace('layout_', ''); } else { const deiconifyLayout = Cast(this.Document.deiconifyLayout, 'string', null); - this.switchViews(deiconifyLayout ? true : false, deiconifyLayout, finalFinished, true); + this.switchViews(!!deiconifyLayout, deiconifyLayout, finalFinished, true); this.Document.deiconifyLayout = undefined; this._props.bringToFront?.(this.Document); } @@ -1262,7 +1208,7 @@ export class DocumentView extends DocComponent<DocumentViewProps>() { autoplay: true, loop: false, volume: 0.5, - onend: action(() => (self.dataDoc.audioAnnoState = AudioAnnoState.stopped)), + onend: action(() => { self.dataDoc.audioAnnoState = AudioAnnoState.stopped; }), // prettier-ignore }); this.dataDoc.audioAnnoState = AudioAnnoState.playing; break; @@ -1270,6 +1216,7 @@ export class DocumentView extends DocComponent<DocumentViewProps>() { this.dataDoc[AudioPlay]?.stop(); this.dataDoc.audioAnnoState = AudioAnnoState.stopped; break; + default: } } }; @@ -1284,10 +1231,10 @@ export class DocumentView extends DocComponent<DocumentViewProps>() { this._docViewInternal._animateScaleTime = time; } }); - public setAnimEffect = (presEffect: Doc, timeInMs: number, afterTrans?: () => void) => { + public setAnimEffect = (presEffect: Doc, timeInMs: number /* , afterTrans?: () => void */) => { this._animEffectTimer && clearTimeout(this._animEffectTimer); this.Document[Animation] = presEffect; - this._animEffectTimer = setTimeout(() => (this.Document[Animation] = undefined), timeInMs); + this._animEffectTimer = setTimeout(() => { this.Document[Animation] = undefined; }, timeInMs); // prettier-ignore }; public setViewTransition = (transProp: string, timeInMs: number, afterTrans?: () => void, dataTrans = false) => { this._viewTimer = DocumentView.SetViewTransition([this.layoutDoc], transProp, timeInMs, this._viewTimer, afterTrans, dataTrans); @@ -1302,9 +1249,9 @@ export class DocumentView extends DocComponent<DocumentViewProps>() { if (checkResult) { return Doc.UserDoc().defaultTextLayout; } - const view = SelectionManager.Views[0]?._props.renderDepth > 0 ? SelectionManager.Views[0] : undefined; + const view = DocumentView.Selected()[0]?._props.renderDepth > 0 ? DocumentView.Selected()[0] : undefined; undoable(() => { - var tempDoc: Opt<Doc>; + let tempDoc: Opt<Doc>; if (view) { if (!view.layoutDoc.isTemplateDoc) { tempDoc = view.Document; @@ -1322,6 +1269,7 @@ export class DocumentView extends DocComponent<DocumentViewProps>() { } Doc.UserDoc().defaultTextLayout = tempDoc ? new PrefetchProxy(tempDoc) : undefined; }, 'set default template')(); + return undefined; } /** @@ -1335,12 +1283,13 @@ export class DocumentView extends DocComponent<DocumentViewProps>() { const curLayout = StrCast(this.Document.layout_fieldKey).replace('layout_', '').replace('layout', ''); if (!this.Document.layout_default && curLayout !== detailLayoutKeySuffix) this.Document.layout_default = curLayout; const defaultLayout = StrCast(this.Document.layout_default); - if (this.Document.layout_fieldKey === 'layout_' + detailLayoutKeySuffix) this.switchViews(defaultLayout ? true : false, defaultLayout, undefined, true); + if (this.Document.layout_fieldKey === 'layout_' + detailLayoutKeySuffix) this.switchViews(!!defaultLayout, defaultLayout, undefined, true); else this.switchViews(true, detailLayoutKeySuffix, undefined, true); }; public switchViews = (custom: boolean, view: string, finished?: () => void, useExistingLayout = false) => { const batch = UndoManager.StartBatch('switchView:' + view); - runInAction(() => this._docViewInternal && (this._docViewInternal._animateScalingTo = 0.1)); // shrink doc + // shrink doc first.. + runInAction(() => { this._docViewInternal && (this._docViewInternal._animateScalingTo = 0.1); }); // prettier-ignore setTimeout( action(() => { if (useExistingLayout && custom && this.Document['layout_' + view]) { @@ -1348,7 +1297,7 @@ export class DocumentView extends DocComponent<DocumentViewProps>() { } else { this.setCustomView(custom, view); } - this._docViewInternal && (this._docViewInternal._animateScalingTo = 1); // expand it + this._docViewInternal && (this._docViewInternal._animateScalingTo = 1); // now expand it setTimeout( action(() => { this._docViewInternal && (this._docViewInternal._animateScalingTo = 0); @@ -1366,15 +1315,15 @@ export class DocumentView extends DocComponent<DocumentViewProps>() { */ public docViewPath = () => (this.containerViewPath ? [...this.containerViewPath(), this] : [this]); - layout_fitWidthFunc = (doc: Doc) => BoolCast(this.layout_fitWidth); + layout_fitWidthFunc = (/* doc: Doc */) => BoolCast(this.layout_fitWidth); screenToLocalScale = () => this._props.ScreenToLocalTransform().Scale; isSelected = () => this.IsSelected; select = (extendSelection: boolean, focusSelection?: boolean) => { - /*if (this.IsSelected && SelectionManager.Views.length > 1) SelectionManager.DeselectView(this); - else {*/ - SelectionManager.SelectView(this, extendSelection); + // if (this.IsSelected && DocumentView.Selected().length > 1) DocumentView.DeselectView(this); + // else { + DocumentView.SelectView(this, extendSelection); if (focusSelection) { - DocumentManager.Instance.showDocument(this.Document, { + DocumentView.showDocument(this.Document, { willZoomCentered: true, zoomScale: 0.9, zoomTime: 500, @@ -1390,7 +1339,7 @@ export class DocumentView extends DocComponent<DocumentViewProps>() { PanelWidth = () => this.panelWidth; PanelHeight = () => this.panelHeight; NativeDimScaling = () => this.nativeScaling; - hideLinkCount = () => (this.hideLinkButton ? true : false); + hideLinkCount = () => !!this.hideLinkButton; selfView = () => this; /** * @returns Transform to the document view (in the coordinate system of whatever contains the DocumentView) @@ -1413,17 +1362,16 @@ export class DocumentView extends DocComponent<DocumentViewProps>() { ref={r => { const val = r?.style.display !== 'none'; // if the outer overlay has been displayed, trigger the innner div to start it's opacity fade in transition if (r && val !== this._enableHtmlOverlayTransitions) { - setTimeout(action(() => (this._enableHtmlOverlayTransitions = val))); + setTimeout(action(() => { this._enableHtmlOverlayTransitions = val; })); // prettier-ignore } }} style={{ display: !this._htmlOverlayText ? 'none' : undefined }}> <div className="documentView-htmlOverlayInner" style={{ transition: `all 500ms`, opacity: this._enableHtmlOverlayTransitions ? 0.9 : 0 }}> {DocumentViewInternal.AnimationEffect( <div className="webBox-textHighlight"> - <ObserverJsxParser autoCloseVoidElements={true} key={42} onError={(e: any) => console.log('PARSE error', e)} renderInWrapper={false} jsx={StrCast(this._htmlOverlayText)} /> + <ObserverJsxParser autoCloseVoidElements key={42} onError={(e: any) => console.log('PARSE error', e)} renderInWrapper={false} jsx={StrCast(this._htmlOverlayText)} /> </div>, - { ...(this._htmlOverlayEffect ?? {}), presentation_effect: effect ?? PresEffect.Zoom } as any as Doc, - this.Document + { ...(this._htmlOverlayEffect ?? {}), presentation_effect: effect ?? PresEffect.Zoom } as any as Doc )} </div> </div> @@ -1436,7 +1384,15 @@ export class DocumentView extends DocComponent<DocumentViewProps>() { const yshift = Math.abs(this.Yshift) <= 0.001 ? this._props.PanelHeight() : undefined; return ( - <div id={this.ViewGuid} className="contentFittingDocumentView" onPointerEnter={action(() => (this._isHovering = true))} onPointerLeave={action(() => (this._isHovering = false))}> + <div + id={this.ViewGuid} + className="contentFittingDocumentView" + onPointerEnter={action(() => { + this._isHovering = true; + })} + onPointerLeave={action(() => { + this._isHovering = false; + })}> {!this.Document || !this._props.PanelWidth() ? null : ( <div className="contentFittingDocumentView-previewDoc" @@ -1462,14 +1418,16 @@ export class DocumentView extends DocComponent<DocumentViewProps>() { layout_fitWidth={this.layout_fitWidthFunc} ScreenToLocalTransform={this.screenToContentsTransform} focus={this._props.focus || emptyFunction} - ref={action((r: DocumentViewInternal | null) => r && (this._docViewInternal = r))} + ref={action((r: DocumentViewInternal | null) => { + r && (this._docViewInternal = r); + })} /> {this.htmlOverlay()} {this.ComponentView?.infoUI?.()} </div> )} {/* display link count button */} - <DocumentLinksButton hideCount={this.hideLinkCount} View={this} scaling={this.screenToLocalScale} OnHover={true} Bottom={this.topMost} ShowCount={true} /> + <DocumentLinksButton hideCount={this.hideLinkCount} View={this} scaling={this.screenToLocalScale} OnHover Bottom={this.topMost} ShowCount /> </div> ); } @@ -1493,7 +1451,7 @@ export class DocumentView extends DocComponent<DocumentViewProps>() { // shows a stacking view collection (by default, but the user can change) of all documents linked to the source public static showBackLinks(linkAnchor: Doc) { - const docId = Doc.CurrentUserEmail + Doc.GetProto(linkAnchor)[Id] + '-pivotish'; + const docId = ClientUtils.CurrentUserEmail() + Doc.GetProto(linkAnchor)[Id] + '-pivotish'; // prettier-ignore DocServer.GetRefField(docId).then(docx => LightboxView.Instance.SetLightboxDoc( @@ -1504,26 +1462,65 @@ export class DocumentView extends DocComponent<DocumentViewProps>() { } } +export function returnEmptyDocViewList() { + return emptyPath; +} + +// eslint-disable-next-line default-param-last +export function DocFocusOrOpen(docIn: Doc, optionsIn: FocusViewOptions = { willZoomCentered: true, zoomScale: 0, openLocation: OpenWhere.toggleRight }, containingDoc?: Doc) { + let doc = docIn; + const options = optionsIn; + const func = () => { + const cv = DocumentView.getDocumentView(containingDoc); + const dv = DocumentView.getDocumentView(doc, cv); + if (dv && (!containingDoc || dv.containerViewPath?.().lastElement()?.Document === containingDoc)) { + DocumentView.showDocumentView(dv, options).then(() => dv && Doc.linkFollowHighlight(dv.Document)); + } else { + const container = DocCast(containingDoc ?? doc.embedContainer ?? Doc.BestEmbedding(doc)); + const showDoc = !Doc.IsSystem(container) && !cv ? container : doc; + options.toggleTarget = undefined; + DocumentView.showDocument(showDoc, options, () => DocumentView.showDocument(doc, { ...options, openLocation: undefined })).then(() => { + const cvFound = DocumentView.getDocumentView(containingDoc); + const dvFound = DocumentView.getDocumentView(doc, cvFound); + dvFound && Doc.linkFollowHighlight(dvFound.Document); + }); + } + }; + if (Doc.IsDataProto(doc) && Doc.GetEmbeddings(doc).some(embed => embed.hidden && [AclAdmin, AclEdit].includes(GetEffectiveAcl(embed)))) { + doc = Doc.GetEmbeddings(doc).find(embed => embed.hidden && [AclAdmin, AclEdit].includes(GetEffectiveAcl(embed)))!; + } + if (doc.hidden) { + doc.hidden = false; + options.toggleTarget = false; + setTimeout(func); + } else func(); +} +ScriptingGlobals.add(DocFocusOrOpen); + +// eslint-disable-next-line prefer-arrow-callback ScriptingGlobals.add(function deiconifyView(documentView: DocumentView) { documentView.iconify(); documentView.select(false); }); +// eslint-disable-next-line prefer-arrow-callback ScriptingGlobals.add(function deiconifyViewToLightbox(documentView: DocumentView) { - LightboxView.Instance.AddDocTab(documentView.Document, OpenWhere.lightbox, 'layout'); //, 0); + LightboxView.Instance.AddDocTab(documentView.Document, OpenWhere.lightbox, 'layout'); // , 0); }); +// eslint-disable-next-line prefer-arrow-callback ScriptingGlobals.add(function toggleDetail(dv: DocumentView, detailLayoutKeySuffix: string) { dv.toggleDetail(detailLayoutKeySuffix); }); +// eslint-disable-next-line prefer-arrow-callback ScriptingGlobals.add(function updateLinkCollection(linkCollection: Doc, linkSource: Doc) { const collectedLinks = DocListCast(linkCollection[DocData].data); let wid = NumCast(linkSource._width); let embedding: Doc | undefined; - const links = LinkManager.Links(linkSource); + const links = Doc.Links(linkSource); links.forEach(link => { - const other = LinkManager.getOppositeAnchor(link, linkSource); + const other = Doc.getOppositeAnchor(link, linkSource); const otherdoc = DocCast(other?.annotationOn ?? other); if (otherdoc && !collectedLinks?.some(d => Doc.AreProtosEqual(d, otherdoc))) { embedding = Doc.MakeEmbedding(otherdoc); @@ -1534,9 +1531,10 @@ ScriptingGlobals.add(function updateLinkCollection(linkCollection: Doc, linkSour Doc.AddDocToList(Doc.GetProto(linkCollection), 'data', embedding); } }); - embedding && DocServer.UPDATE_SERVER_CACHE(); // if a new embedding was made, update the client's server cache so that it will not come back as a promise + embedding && UPDATE_SERVER_CACHE(); // if a new embedding was made, update the client's server cache so that it will not come back as a promise return links; }); +// eslint-disable-next-line prefer-arrow-callback ScriptingGlobals.add(function updateTagsCollection(collection: Doc) { const tag = StrCast(collection.title).split('-->')[1]; const matchedTags = Array.from(SearchUtil.SearchCollection(Doc.MyFilesystem, tag, false, ['tags']).keys()); diff --git a/src/client/views/nodes/EquationBox.tsx b/src/client/views/nodes/EquationBox.tsx index a557cff4f..32d08fbe7 100644 --- a/src/client/views/nodes/EquationBox.tsx +++ b/src/client/views/nodes/EquationBox.tsx @@ -1,11 +1,14 @@ +/* eslint-disable jsx-a11y/no-static-element-interactions */ import { action, makeObservable, reaction } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; -import { DivHeight, DivWidth } from '../../../Utils'; -import { Id } from '../../../fields/FieldSymbols'; +import { DivHeight, DivWidth } from '../../../ClientUtils'; +import { Doc } from '../../../fields/Doc'; import { NumCast, StrCast } from '../../../fields/Types'; import { TraceMobx } from '../../../fields/util'; -import { DocUtils, Docs } from '../../documents/Documents'; +import { DocUtils } from '../../documents/DocUtils'; +import { DocumentType } from '../../documents/DocumentTypes'; +import { Docs } from '../../documents/Documents'; import { undoBatch } from '../../util/UndoManager'; import { ViewBoxBaseComponent } from '../DocComponent'; import { LightboxView } from '../LightboxView'; @@ -18,7 +21,6 @@ export class EquationBox extends ViewBoxBaseComponent<FieldViewProps>() { public static LayoutString(fieldKey: string) { return FieldView.LayoutString(EquationBox, fieldKey); } - public static SelectOnLoad: string = ''; _ref: React.RefObject<EquationEditor> = React.createRef(); constructor(props: FieldViewProps) { @@ -28,12 +30,12 @@ export class EquationBox extends ViewBoxBaseComponent<FieldViewProps>() { componentDidMount() { this._props.setContentViewBox?.(this); - if (EquationBox.SelectOnLoad === this.Document[Id] && (!LightboxView.LightboxDoc || LightboxView.Contains(this.DocumentView?.()))) { + if (Doc.SelectOnLoad === this.Document && (!LightboxView.LightboxDoc || LightboxView.Contains(this.DocumentView?.()))) { this._props.select(false); this._ref.current!.mathField.focus(); this.dataDoc.text === 'x' && this._ref.current!.mathField.select(); - EquationBox.SelectOnLoad = ''; + Doc.SetSelectOnLoad(undefined); } reaction( () => StrCast(this.dataDoc.text), @@ -42,7 +44,7 @@ export class EquationBox extends ViewBoxBaseComponent<FieldViewProps>() { this._ref.current!.mathField.latex(text); } } - //{ fireImmediately: true } + // { fireImmediately: true } ); reaction( () => this._props.isSelected(), @@ -68,7 +70,7 @@ export class EquationBox extends ViewBoxBaseComponent<FieldViewProps>() { x: NumCast(this.layoutDoc.x), y: NumCast(this.layoutDoc.y) + _height + 10, }); - EquationBox.SelectOnLoad = nextEq[Id]; + Doc.SetSelectOnLoad(nextEq); this._props.addDocument?.(nextEq); e.stopPropagation(); } @@ -88,7 +90,9 @@ export class EquationBox extends ViewBoxBaseComponent<FieldViewProps>() { if (e.key === 'Backspace' && !this.dataDoc.text) this._props.removeDocument?.(this.Document); }; @undoBatch - onChange = (str: string) => (this.dataDoc.text = str); + onChange = (str: string) => { + this.dataDoc.text = str; + }; updateSize = () => { const style = this._ref.current && getComputedStyle(this._ref.current.element.current); @@ -111,7 +115,7 @@ export class EquationBox extends ViewBoxBaseComponent<FieldViewProps>() { const scale = (this._props.NativeDimScaling?.() || 1) * NumCast(this.layoutDoc._freeform_scale, 1); return ( <div - ref={r => this.updateSize()} + ref={() => this.updateSize()} className="equationBox-cont" onPointerDown={e => !e.ctrlKey && e.stopPropagation()} style={{ @@ -122,8 +126,13 @@ export class EquationBox extends ViewBoxBaseComponent<FieldViewProps>() { fontSize: StrCast(this.layoutDoc._text_fontSize), }} onKeyDown={e => e.stopPropagation()}> - <EquationEditor ref={this._ref} value={StrCast(this.dataDoc.text, 'x')} spaceBehavesLikeTab={true} onChange={this.onChange} autoCommands="pi theta sqrt sum prod alpha beta gamma rho" autoOperatorNames="sin cos tan" /> + <EquationEditor ref={this._ref} value={StrCast(this.dataDoc.text, 'x')} spaceBehavesLikeTab onChange={this.onChange} autoCommands="pi theta sqrt sum prod alpha beta gamma rho" autoOperatorNames="sin cos tan" /> </div> ); } } + +Docs.Prototypes.TemplateMap.set(DocumentType.EQUATION, { + layout: { view: EquationBox, dataField: 'text' }, + options: { acl: '', fontSize: '14px', _layout_reflowHorizontal: true, _layout_reflowVertical: true, _layout_nativeDimEditable: true, layout_hideDecorationTitle: true, systemIcon: 'BsCalculatorFill' }, // systemIcon: 'BsSuperscript' + BsSubscript +}); diff --git a/src/client/views/nodes/FaceRectangle.tsx b/src/client/views/nodes/FaceRectangle.tsx index 46bc6eb03..2b66b83fe 100644 --- a/src/client/views/nodes/FaceRectangle.tsx +++ b/src/client/views/nodes/FaceRectangle.tsx @@ -8,11 +8,17 @@ export default class FaceRectangle extends React.Component<{ rectangle: Rectangl @observable private opacity = 0; componentDidMount() { - setTimeout(() => runInAction(() => (this.opacity = 1)), 500); + setTimeout( + () => + runInAction(() => { + this.opacity = 1; + }), + 500 + ); } render() { - const rectangle = this.props.rectangle; + const { rectangle } = this.props; return ( <div style={{ diff --git a/src/client/views/nodes/FieldView.tsx b/src/client/views/nodes/FieldView.tsx index 771856788..8a37000f7 100644 --- a/src/client/views/nodes/FieldView.tsx +++ b/src/client/views/nodes/FieldView.tsx @@ -1,3 +1,6 @@ +/* eslint-disable react/no-unused-prop-types */ +/* eslint-disable react/require-default-props */ +import { computed } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; import { DateField } from '../../../fields/DateField'; @@ -5,34 +8,16 @@ import { Doc, Field, Opt } from '../../../fields/Doc'; import { List } from '../../../fields/List'; import { ScriptField } from '../../../fields/ScriptField'; import { WebField } from '../../../fields/URLField'; -import { dropActionType } from '../../util/DragManager'; +import { dropActionType } from '../../util/DropActionTypes'; import { Transform } from '../../util/Transform'; import { ViewBoxInterface } from '../DocComponent'; -import { CollectionFreeFormDocumentView } from './CollectionFreeFormDocumentView'; -import { DocumentView, OpenWhere } from './DocumentView'; -import { PinProps } from './trails'; -import { computed } from 'mobx'; +import { PinProps } from '../PinFuncs'; +import { DocumentView } from './DocumentView'; +import { FocusViewOptions } from './FocusViewOptions'; +import { OpenWhere } from './OpenWhere'; -export interface FocusViewOptions { - willPan?: boolean; // determines whether to pan to target document - willZoomCentered?: boolean; // determines whether to zoom in on target document. if zoomScale is 0, this just centers the document - zoomScale?: number; // percent of containing frame to zoom into document - zoomTime?: number; - didMove?: boolean; // whether a document was changed during the showDocument process - docTransform?: Transform; // when a document can't be panned and zoomed within its own container (say a group), then we need to continue to move up the render hierarchy to find something that can pan and zoom. when this happens the docTransform must accumulate all the transforms of each level of the hierarchy - instant?: boolean; // whether focus should happen instantly (as opposed to smooth zoom) - preview?: boolean; // whether changes should be previewed by the componentView or written to the document - effect?: Doc; // animation effect for focus - noSelect?: boolean; // whether target should be selected after focusing - playAudio?: boolean; // whether to play audio annotation on focus - playMedia?: boolean; // whether to play start target videos - openLocation?: OpenWhere; // where to open a missing document - zoomTextSelections?: boolean; // whether to display a zoomed overlay of anchor text selections - toggleTarget?: boolean; // whether to toggle target on and off - anchorDoc?: Doc; // doc containing anchor info to apply at end of focus to target doc - easeFunc?: 'linear' | 'ease'; // transition method for scrolling -} export type FocusFuncType = (doc: Doc, options: FocusViewOptions) => Opt<number>; +// eslint-disable-next-line no-use-before-define export type StyleProviderFuncType = (doc: Opt<Doc>, props: Opt<FieldViewProps>, property: string) => any; // // these properties get assigned through the render() method of the DocumentView when it creates this node. @@ -56,11 +41,11 @@ export interface FieldViewSharedProps { disableBrushing?: boolean; // should highlighting for this view be disabled when same document in another view is hovered over. hideClickBehaviors?: boolean; // whether to suppress menu item options for changing click behaviors ignoreUsePath?: boolean; // ignore the usePath field for selecting the fieldKey (eg., on text docs) - CollectionFreeFormDocumentView?: () => CollectionFreeFormDocumentView; + LocalRotation?: () => number | undefined; // amount of rotation applied to freeformdocumentview containing document view containerViewPath?: () => DocumentView[]; fitContentsToBox?: () => boolean; // used by freeformview to fit its contents to its panel. corresponds to _freeform_fitContentsToBox property on a Document isGroupActive?: () => string | undefined; // is this document part of a group that is active - setContentViewBox?: (view: ViewBoxInterface) => any; // called by rendered field's viewBox so that DocumentView can make direct calls to the viewBox + setContentViewBox?: (view: ViewBoxInterface<any>) => any; // called by rendered field's viewBox so that DocumentView can make direct calls to the viewBox PanelWidth: () => number; PanelHeight: () => number; isDocumentActive?: () => boolean | undefined; // whether a document should handle pointer events @@ -74,14 +59,14 @@ export interface FieldViewSharedProps { onDoubleClickScript?: () => ScriptField; onPointerDownScript?: () => ScriptField; onPointerUpScript?: () => ScriptField; - onBrowseClickScript?: () => ScriptField | undefined; + // eslint-disable-next-line no-use-before-define onKey?: (e: React.KeyboardEvent, fieldProps: FieldViewProps) => boolean | undefined; layout_fitWidth?: (doc: Doc) => boolean | undefined; searchFilterDocs: () => Doc[]; layout_showTitle?: () => string; whenChildContentsActiveChanged: (isActive: boolean) => void; rootSelected?: () => boolean; // whether the root of a template has been selected - addDocTab: (doc: Doc, where: OpenWhere) => boolean; + addDocTab: (doc: Doc | Doc[], where: OpenWhere) => boolean; filterAddDocument?: (doc: Doc[]) => boolean; // allows a document that renders a Collection view to filter or modify any documents added to the collection (see PresBox for an example) addDocument?: (doc: Doc | Doc[], annotationKey?: string) => boolean; removeDocument?: (doc: Doc | Doc[], annotationKey?: string) => boolean; @@ -122,7 +107,7 @@ export interface FieldViewProps extends FieldViewSharedProps { @observer export class FieldView extends React.Component<FieldViewProps> { public static LayoutString(fieldType: { name: string }, fieldStr: string) { - return `<${fieldType.name} {...props} fieldKey={'${fieldStr}'}/>`; //e.g., "<ImageBox {...props} fieldKey={'data'} />" + return `<${fieldType.name} {...props} fieldKey={'${fieldStr}'}/>`; // e.g., "<ImageBox {...props} fieldKey={'data'} />" } @computed get fieldval() { return this.props.Document[this.props.fieldKey]; @@ -137,6 +122,6 @@ export class FieldView extends React.Component<FieldViewProps> { if (field instanceof List) return <div> {field.map(f => Field.toString(f)).join(', ')} </div>; if (field instanceof WebField) return <p>{Field.toString(field.url.href)}</p>; if (!(field instanceof Promise)) return <p>{Field.toString(field)}</p>; - return <p> {'Waiting for server...'} </p>; + return <p> Waiting for server... </p>; } } diff --git a/src/client/views/nodes/FocusViewOptions.ts b/src/client/views/nodes/FocusViewOptions.ts new file mode 100644 index 000000000..bb0d2b03c --- /dev/null +++ b/src/client/views/nodes/FocusViewOptions.ts @@ -0,0 +1,24 @@ +import { Doc } from '../../../fields/Doc'; +import { Transform } from '../../util/Transform'; +import { OpenWhere } from './OpenWhere'; + +export interface FocusViewOptions { + willPan?: boolean; // determines whether to pan to target document + willZoomCentered?: boolean; // determines whether to zoom in on target document. if zoomScale is 0, this just centers the document + zoomScale?: number; // percent of containing frame to zoom into document + zoomTime?: number; + didMove?: boolean; // whether a document was changed during the showDocument process + docTransform?: Transform; // when a document can't be panned and zoomed within its own container (say a group), then we need to continue to move up the render hierarchy to find something that can pan and zoom. when this happens the docTransform must accumulate all the transforms of each level of the hierarchy + instant?: boolean; // whether focus should happen instantly (as opposed to smooth zoom) + preview?: boolean; // whether changes should be previewed by the componentView or written to the document + effect?: Doc; // animation effect for focus // bcz: needs to be changed to something more generic than a Doc + noSelect?: boolean; // whether target should be selected after focusing + playAudio?: boolean; // whether to play audio annotation on focus + playMedia?: boolean; // whether to play start target videos + openLocation?: OpenWhere; // where to open a missing document + zoomTextSelections?: boolean; // whether to display a zoomed overlay of anchor text selections + toggleTarget?: boolean; // whether to toggle target on and off + easeFunc?: 'linear' | 'ease'; // transition method for scrolling + pointFocus?: { X: number; Y: number }; // clientX and clientY coordinates to focus on instead of a document target (used by explore mode) + contextPath?: Doc[]; // path of inner documents that will also be focused +} diff --git a/src/client/views/nodes/FontIconBox/ButtonInterface.ts b/src/client/views/nodes/FontIconBox/ButtonInterface.ts index 1c034bfbe..0d0d7b1c3 100644 --- a/src/client/views/nodes/FontIconBox/ButtonInterface.ts +++ b/src/client/views/nodes/FontIconBox/ButtonInterface.ts @@ -1,5 +1,5 @@ -import { Doc } from '../../../../fields/Doc'; import { IconProp } from '@fortawesome/fontawesome-svg-core'; +import { Doc } from '../../../../fields/Doc'; import { ButtonType } from './FontIconBox'; export interface IButtonProps { diff --git a/src/client/views/nodes/FontIconBox/FontIconBox.tsx b/src/client/views/nodes/FontIconBox/FontIconBox.tsx index 57ae92359..5e3bb9fec 100644 --- a/src/client/views/nodes/FontIconBox/FontIconBox.tsx +++ b/src/client/views/nodes/FontIconBox/FontIconBox.tsx @@ -1,23 +1,25 @@ +/* eslint-disable react/jsx-props-no-spreading */ 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 { Button, ColorPicker, Dropdown, DropdownType, IconButton, IListItemProps, MultiToggle, NumberDropdown, NumberDropdownType, Popup, Size, Toggle, ToggleType, Type } from 'browndash-components'; import { action, computed, makeObservable, observable } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; +import { ClientUtils, returnTrue, setupMoveUpEvents } from '../../../../ClientUtils'; import { Doc, DocListCast, StrListCast } from '../../../../fields/Doc'; import { BoolCast, DocCast, NumCast, ScriptCast, StrCast } from '../../../../fields/Types'; -import { emptyFunction, returnTrue, setupMoveUpEvents, Utils } from '../../../../Utils'; +import { emptyFunction } from '../../../../Utils'; +import { Docs } from '../../../documents/Documents'; import { CollectionViewType, DocumentType } from '../../../documents/DocumentTypes'; -import { SelectionManager } from '../../../util/SelectionManager'; -import { SettingsManager } from '../../../util/SettingsManager'; +import { SnappingManager } from '../../../util/SnappingManager'; import { undoable, UndoManager } from '../../../util/UndoManager'; import { ContextMenu } from '../../ContextMenu'; import { ViewBoxBaseComponent } from '../../DocComponent'; import { EditableView } from '../../EditableView'; import { SelectedDocView } from '../../selectedDoc'; -import { StyleProp } from '../../StyleProvider'; -import { OpenWhere } from '../DocumentView'; +import { StyleProp } from '../../StyleProp'; import { FieldView, FieldViewProps } from '../FieldView'; +import { OpenWhere } from '../OpenWhere'; import './FontIconBox.scss'; import TrailsIcon from './TrailsIcon'; @@ -33,7 +35,7 @@ export enum ButtonType { NumberSliderButton = 'numSliderBtn', NumberDropdownButton = 'numDropdownBtn', NumberInlineButton = 'numInlineBtn', - EditableText = 'editableText', + EditText = 'editableText', } export interface ButtonProps extends FieldViewProps { @@ -49,15 +51,6 @@ export class FontIconBox extends ViewBoxBaseComponent<ButtonProps>() { super(props); makeObservable(this); } - // - // This controls whether fontIconButtons will display labels under their icons or not - // - public static get ShowIconLabels() { - return BoolCast(Doc.UserDoc()._showLabel); - } - public static set ShowIconLabels(show: boolean) { - Doc.UserDoc()._showLabel = show; - } @observable noTooltip = false; @@ -82,7 +75,7 @@ export class FontIconBox extends ViewBoxBaseComponent<ButtonProps>() { if (iconFalse) { icon = StrCast(this.dataDoc[this.fieldKey ?? 'iconFalse'] ?? this.dataDoc.icon, 'user') as any; if (icon) return <FontAwesomeIcon className={`fontIconBox-icon-${this.type}`} icon={icon} color={color} />; - else return null; + return null; } icon = StrCast(this.dataDoc[this.fieldKey ?? 'icon'] ?? this.dataDoc.icon, 'user') as any; return !icon ? null : icon === 'pres-trail' ? TrailsIcon(color) : <FontAwesomeIcon className={`fontIconBox-icon-${this.type}`} icon={icon} color={color} />; @@ -108,7 +101,7 @@ export class FontIconBox extends ViewBoxBaseComponent<ButtonProps>() { * - Color button * - Dropdown list * - Number button - **/ + * */ _batch: UndoManager.Batch | undefined = undefined; /** @@ -117,18 +110,13 @@ export class FontIconBox extends ViewBoxBaseComponent<ButtonProps>() { @computed get numberDropdown() { let type: NumberDropdownType; switch (this.type) { - case ButtonType.NumberDropdownButton: - type = 'dropdown'; - break; - case ButtonType.NumberInlineButton: - type = 'input'; - break; + case ButtonType.NumberDropdownButton: type = 'dropdown'; break; + case ButtonType.NumberInlineButton: type = 'input'; break; case ButtonType.NumberSliderButton: - default: - type = 'slider'; + default: type = 'slider'; break; - } - const numScript = (value?: number) => ScriptCast(this.Document.script).script.run({ this: this.Document, self: this.Document, value, _readOnly_: value === undefined }); + } // prettier-ignore + const numScript = (value?: number) => ScriptCast(this.Document.script).script.run({ this: this.Document, value, _readOnly_: value === undefined }); 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))); @@ -136,7 +124,7 @@ export class FontIconBox extends ViewBoxBaseComponent<ButtonProps>() { return ( <NumberDropdown color={color} - background={SettingsManager.userBackgroundColor} + background={SnappingManager.userBackgroundColor} numberDropdownType={type} showPlusMinus={false} tooltip={this.label} @@ -154,12 +142,10 @@ export class FontIconBox extends ViewBoxBaseComponent<ButtonProps>() { setupMoveUpEvents( this, e, - (e: PointerEvent) => { - return ScriptCast(this.Document.onDragScript)?.script.run({ this: this.Document, self: this.Document, value: { doc: value, e } }).result; - }, + () => ScriptCast(this.Document.onDragScript)?.script.run({ this: this.Document, value: { doc: value, e } }).result, emptyFunction, emptyFunction - ); + ); // prettier-ignore return false; }; @@ -173,9 +159,10 @@ export class FontIconBox extends ViewBoxBaseComponent<ButtonProps>() { let text: string | undefined; let getStyle: (val: string) => any = () => {}; let icon: IconProp = 'caret-down'; - const isViewDropdown = script?.script.originalScript.startsWith('setView'); + const isViewDropdown = script?.script.originalScript.startsWith('{ return setView'); if (isViewDropdown) { - const selected = SelectionManager.Docs; + const selected = Array.from(script?.script.run({ _readOnly_: true }).result) as Doc[]; + // const selected = DocumentView.SelectedDocs(); if (selected.lastElement()) { if (StrCast(selected.lastElement().type) === DocumentType.COL) { text = StrCast(selected.lastElement()._type_collection); @@ -183,27 +170,27 @@ export class FontIconBox extends ViewBoxBaseComponent<ButtonProps>() { if (selected.length > 1) { text = selected.length + ' selected'; } else { - text = Utils.cleanDocumentType(StrCast(selected.lastElement().type) as DocumentType); + text = ClientUtils.cleanDocumentType(StrCast(selected.lastElement().type) as DocumentType, '' as CollectionViewType); icon = Doc.toIcon(selected.lastElement()); } return ( <Popup - icon={<FontAwesomeIcon size={'1x'} icon={icon} />} + icon={<FontAwesomeIcon size="1x" icon={icon} />} text={text} type={Type.TERT} - color={SettingsManager.userColor} - background={SettingsManager.userVariantColor} + color={SnappingManager.userColor} + background={SnappingManager.userVariantColor} popup={<SelectedDocView selectedDocs={selected} />} fillWidth /> ); } } else { - return <Button text="None Selected" type={Type.TERT} color={SettingsManager.userColor} background={SettingsManager.userVariantColor} fillWidth inactive />; + return <Button text="None Selected" type={Type.TERT} color={SnappingManager.userColor} background={SnappingManager.userVariantColor} fillWidth inactive />; } noviceList = [CollectionViewType.Freeform, CollectionViewType.Schema, CollectionViewType.Carousel3D, CollectionViewType.Stacking, CollectionViewType.NoteTaking]; } else { - text = script?.script.run({ this: this.Document, self: this.Document, value: '', _readOnly_: true }).result; + text = script?.script.run({ this: this.Document, value: '', _readOnly_: true }).result; // text = StrCast((RichTextMenu.Instance?.TextView?.EditorView ? RichTextMenu.Instance : Doc.UserDoc()).fontFamily); getStyle = (val: string) => ({ fontFamily: val }); } @@ -221,9 +208,9 @@ export class FontIconBox extends ViewBoxBaseComponent<ButtonProps>() { return ( <Dropdown selectedVal={text} - setSelectedVal={undoable(value => script.script.run({ this: this.Document, self: this.Document, value }), `dropdown select ${this.label}`)} - color={SettingsManager.userColor} - background={SettingsManager.userVariantColor} + setSelectedVal={undoable(value => script.script.run({ this: this.Document, value }), `dropdown select ${this.label}`)} + color={SnappingManager.userColor} + background={SnappingManager.userVariantColor} type={Type.TERT} closeOnSelect={false} dropdownType={DropdownType.SELECT} @@ -245,17 +232,17 @@ export class FontIconBox extends ViewBoxBaseComponent<ButtonProps>() { */ @computed get colorButton() { 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 curColor = this.colorScript?.script.run({ this: this.Document, value: undefined, _readOnly_: true }).result ?? 'transparent'; const tooltip: string = StrCast(this.Document.toolTip); return ( <ColorPicker setSelectedColor={value => { if (!this.colorBatch) this.colorBatch = UndoManager.StartBatch(`Set ${tooltip} color`); - this.colorScript?.script.run({ this: this.Document, self: this.Document, value: value, _readOnly_: false }); + this.colorScript?.script.run({ this: this.Document, value: value, _readOnly_: false }); }} setFinalColor={value => { - this.colorScript?.script.run({ this: this.Document, self: this.Document, value: value, _readOnly_: false }); + this.colorScript?.script.run({ this: this.Document, value: value, _readOnly_: false }); this.colorBatch?.end(); this.colorBatch = undefined; }} @@ -263,7 +250,7 @@ export class FontIconBox extends ViewBoxBaseComponent<ButtonProps>() { selectedColor={curColor} type={Type.PRIM} color={color} - background={SettingsManager.userBackgroundColor} + background={SnappingManager.userBackgroundColor} icon={this.Icon(color)!} tooltip={tooltip} label={this.label} @@ -274,8 +261,8 @@ export class FontIconBox extends ViewBoxBaseComponent<ButtonProps>() { // Determine the type of toggle button const tooltip: string = StrCast(this.Document.toolTip); - const script = ScriptCast(this.Document.onClick); - const toggleStatus = script ? script.script.run({ this: this.Document, self: this.Document, value: undefined, _readOnly_: true }).result : false; + // const script = ScriptCast(this.Document.onClick); + // const toggleStatus = script ? script.script.run({ this: this.Document, value: undefined, _readOnly_: true }).result : false; // Colors const color = this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.Color); const items = DocListCast(this.dataDoc.data); @@ -284,17 +271,17 @@ export class FontIconBox extends ViewBoxBaseComponent<ButtonProps>() { tooltip={`Toggle ${tooltip}`} type={Type.PRIM} color={color} - background={SettingsManager.userBackgroundColor} + background={SnappingManager.userBackgroundColor} label={this.label} items={DocListCast(this.dataDoc.data).map(item => ({ icon: <FontAwesomeIcon className={`fontIconBox-icon-${this.type}`} icon={StrCast(item.icon) as any} color={color} />, tooltip: StrCast(item.toolTip), val: StrCast(item.toolType), }))} - selectedVal={StrCast(items.find(itemDoc => ScriptCast(itemDoc.onClick).script.run({ this: itemDoc, self: itemDoc, value: undefined, _readOnly_: true }).result)?.toolType)} + selectedVal={StrCast(items.find(itemDoc => ScriptCast(itemDoc.onClick).script.run({ this: itemDoc, value: undefined, _readOnly_: true }).result)?.toolType)} setSelectedVal={(val: string | number) => { const itemDoc = items.find(item => item.toolType === val); - itemDoc && ScriptCast(itemDoc.onClick).script.run({ this: itemDoc, self: itemDoc, value: val, _readOnly_: false }); + itemDoc && ScriptCast(itemDoc.onClick).script.run({ this: itemDoc, value: val, _readOnly_: false }); }} /> ); @@ -309,10 +296,10 @@ export class FontIconBox extends ViewBoxBaseComponent<ButtonProps>() { const script = ScriptCast(this.Document.onClick); const double = ScriptCast(this.Document.onDoubleClick); - const toggleStatus = script?.script.run({ this: this.Document, self: this.Document, value: undefined, _readOnly_: true }).result ?? false; + const toggleStatus = script?.script.run({ this: 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 backgroundColor = this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.BackgroundColor); return ( <Toggle @@ -322,7 +309,7 @@ export class FontIconBox extends ViewBoxBaseComponent<ButtonProps>() { toggleStatus={toggleStatus} text={buttonText} color={color} - //background={SettingsManager.userBackgroundColor} + // background={SnappingManager.userBackgroundColor} icon={this.Icon(color)!} label={this.label} onPointerDown={e => @@ -331,10 +318,10 @@ export class FontIconBox extends ViewBoxBaseComponent<ButtonProps>() { e, returnTrue, emptyFunction, - action((e, doubleTap) => { - (!doubleTap || !double) && script?.script.run({ this: this.Document, self: this.Document, value: !toggleStatus, _readOnly_: false }); - doubleTap && double?.script.run({ this: this.Document, self: this.Document, value: !toggleStatus, _readOnly_: false }); - this._hackToRecompute = this._hackToRecompute + 1; + action((clickEv, doubleTap) => { + (!doubleTap || !double) && script?.script.run({ this: this.Document, value: !toggleStatus, _readOnly_: false }); + doubleTap && double?.script.run({ this: this.Document, value: !toggleStatus, _readOnly_: false }); + this._hackToRecompute += 1; }) ) } @@ -347,27 +334,22 @@ export class FontIconBox extends ViewBoxBaseComponent<ButtonProps>() { */ @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 tooltip: string = StrCast(this.Document.toolTip); return <IconButton tooltip={tooltip} icon={this.Icon(color)!} label={this.label} />; } @computed get editableText() { - // Script for running the toggle const script = ScriptCast(this.Document.script); - // Function to run the script - const checkResult = script?.script.run({ this: this.Document, self: this.Document, value: '', _readOnly_: true }).result; + const checkResult = script?.script.run({ this: this.Document, value: '', _readOnly_: true }).result; - const setValue = (value: string, shiftDown?: boolean): boolean => script?.script.run({ this: this.Document, self: this.Document, value, _readOnly_: false }).result; - - return <EditableText editing={false} setEditing={(editing: boolean) => {}} />; + const setValue = (value: string): boolean => script?.script.run({ this: this.Document, value, _readOnly_: false }).result; return ( <div className="menuButton editableText"> - <FontAwesomeIcon className={`fontIconBox-icon-${this.type}`} icon={'lock'} /> + <FontAwesomeIcon className={`fontIconBox-icon-${this.type}`} icon="lock" /> <div style={{ width: 'calc(100% - .875em)', paddingLeft: '4px' }}> - <EditableView GetValue={() => script?.script.run({ this: this.Document, self: this.Document, value: '', _readOnly_: true }).result} SetValue={setValue} oneLine={true} contents={checkResult} /> + <EditableView GetValue={() => script?.script.run({ this: this.Document, value: '', _readOnly_: true }).result} SetValue={setValue} oneLine contents={checkResult} /> </div> </div> ); @@ -376,14 +358,14 @@ export class FontIconBox extends ViewBoxBaseComponent<ButtonProps>() { renderButton = () => { 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 scriptFunc = () => ScriptCast(this.Document.onClick)?.script.run({ this: this.Document, _readOnly_: false }); const btnProps = { tooltip, icon: this.Icon(color)!, label: this.label }; // prettier-ignore switch (this.type) { case ButtonType.NumberDropdownButton: case ButtonType.NumberInlineButton: case ButtonType.NumberSliderButton: return this.numberDropdown; - case ButtonType.EditableText: return this.editableText; + case ButtonType.EditText: return this.editableText; case ButtonType.DropdownList: return this.dropdownListButton; case ButtonType.ColorButton: return this.colorButton; case ButtonType.MultiToggleButton: return this.multiToggleButton; @@ -391,9 +373,10 @@ export class FontIconBox extends ViewBoxBaseComponent<ButtonProps>() { case ButtonType.ClickButton:return <IconButton {...btnProps} size={Size.MEDIUM} color={color} />; case ButtonType.ToolButton: return <IconButton {...btnProps} size={Size.LARGE} color={color} />; case ButtonType.TextButton: return <Button {...btnProps} color={color} - background={SettingsManager.userBackgroundColor} text={StrCast(this.dataDoc.buttonText)}/>; + background={SnappingManager.userBackgroundColor} text={StrCast(this.dataDoc.buttonText)}/>; case ButtonType.MenuButton: return <IconButton {...btnProps} color={color} - background={SettingsManager.userBackgroundColor} size={Size.LARGE} tooltipPlacement='right' onPointerDown={scriptFunc} />; + background={SnappingManager.userBackgroundColor} size={Size.LARGE} tooltipPlacement='right' onPointerDown={scriptFunc} />; + default: } return this.defaultButton; }; @@ -406,3 +389,8 @@ export class FontIconBox extends ViewBoxBaseComponent<ButtonProps>() { ); } } + +Docs.Prototypes.TemplateMap.set(DocumentType.FONTICON, { + layout: { view: FontIconBox, dataField: 'icon' }, + options: { acl: '', defaultDoubleClick: 'ignore', waitForDoubleClickToClick: 'never', layout_hideContextMenu: true, layout_hideLinkButton: true, _width: 40, _height: 40 }, +}); diff --git a/src/client/views/nodes/FontIconBox/TrailsIcon.tsx b/src/client/views/nodes/FontIconBox/TrailsIcon.tsx index 09fd6e3ae..76f00b2f4 100644 --- a/src/client/views/nodes/FontIconBox/TrailsIcon.tsx +++ b/src/client/views/nodes/FontIconBox/TrailsIcon.tsx @@ -1,10 +1,11 @@ import * as React from 'react'; -const TrailsIcon = (fill: string) => ( - <svg version="1.0" xmlns="http://www.w3.org/2000/svg" width="30" height="30" viewBox="0 0 1080.000000 1080.000000" preserveAspectRatio="xMidYMid meet"> - <g transform="translate(0.000000,1080.000000) scale(0.100000,-0.100000)" fill={fill} stroke="none"> - <path - d="M665 9253 c-74 -10 -157 -38 -240 -81 -74 -37 -107 -63 -186 -141 +function TrailsIcon(fill: string) { + return ( + <svg version="1.0" xmlns="http://www.w3.org/2000/svg" width="30" height="30" viewBox="0 0 1080.000000 1080.000000" preserveAspectRatio="xMidYMid meet"> + <g transform="translate(0.000000,1080.000000) scale(0.100000,-0.100000)" fill={fill} stroke="none"> + <path + d="M665 9253 c-74 -10 -157 -38 -240 -81 -74 -37 -107 -63 -186 -141 -104 -104 -156 -191 -201 -334 l-23 -72 0 -3215 c0 -3072 1 -3218 18 -3280 10 -36 39 -108 64 -160 40 -82 59 -107 142 -190 81 -81 111 -103 191 -143 52 -26 122 -55 155 -65 57 -16 322 -17 4775 -20 3250 -2 4736 1 4784 8 256 39 486 @@ -14,68 +15,69 @@ const TrailsIcon = (fill: string) => ( -62 -101 -108 -126 l-42 -22 -4435 -3 c-3954 -2 -4440 0 -4481 13 -26 9 -63 33 -87 56 -79 79 -72 -205 -72 3012 0 2156 3 2889 12 2918 20 70 91 136 168 160 14 4 2010 8 4436 8 3710 1 4418 -1 4456 -13z" - /> - <path - d="M7692 7839 c-46 -14 -109 -80 -122 -128 -7 -27 -9 -472 -8 -1443 l3 + /> + <path + d="M7692 7839 c-46 -14 -109 -80 -122 -128 -7 -27 -9 -472 -8 -1443 l3 -1403 24 -38 c13 -21 42 -50 64 -65 l41 -27 816 0 816 0 41 27 c22 15 51 44 64 65 l24 38 0 1425 0 1425 -24 38 c-13 21 -42 50 -64 65 l-41 27 -800 2 c-488 1 -814 -2 -834 -8z" - /> - <path - d="M1982 7699 c-46 -14 -109 -80 -122 -128 -7 -27 -10 -308 -8 -893 l3 + /> + <path + d="M1982 7699 c-46 -14 -109 -80 -122 -128 -7 -27 -10 -308 -8 -893 l3 -853 24 -38 c13 -21 42 -50 64 -65 l41 -27 1386 0 1386 0 41 27 c22 15 51 44 64 65 l24 38 0 876 0 875 -27 41 c-15 22 -44 51 -65 64 l-38 24 -1370 2 c-847 1 -1383 -2 -1403 -8z" - /> - <path - d="M6413 7093 c-13 -2 -23 -9 -23 -15 0 -24 21 -307 26 -343 l5 -40 182 + /> + <path + d="M6413 7093 c-13 -2 -23 -9 -23 -15 0 -24 21 -307 26 -343 l5 -40 182 -1 c200 -1 307 -15 484 -65 57 -16 107 -29 112 -29 5 0 36 75 69 168 33 92 63 175 67 184 6 14 -10 22 -92 48 -126 39 -308 76 -447 89 -106 11 -337 13 -383 4z" - /> - <path - d="M5840 7033 c-63 -8 -238 -29 -388 -47 -150 -18 -274 -35 -276 -37 -2 + /> + <path + d="M5840 7033 c-63 -8 -238 -29 -388 -47 -150 -18 -274 -35 -276 -37 -2 -2 8 -89 23 -194 22 -163 29 -190 44 -193 10 -2 91 6 180 17 89 12 258 32 376 46 118 14 216 27 218 28 7 8 -43 391 -52 392 -5 1 -62 -4 -125 -12z" - /> - <path - d="M4762 4789 c-46 -14 -109 -80 -122 -128 -7 -27 -10 -323 -8 -943 l3 + /> + <path + d="M4762 4789 c-46 -14 -109 -80 -122 -128 -7 -27 -10 -323 -8 -943 l3 -903 24 -38 c13 -21 42 -50 64 -65 l41 -27 926 0 926 0 41 27 c22 15 51 44 64 65 l24 38 0 926 0 925 -27 41 c-15 22 -44 51 -65 64 l-38 24 -910 2 c-557 1 -923 -2 -943 -8z" - /> - <path - d="M8487 4297 c-26 -215 -161 -474 -307 -585 -27 -20 -49 -40 -49 -44 + /> + <path + d="M8487 4297 c-26 -215 -161 -474 -307 -585 -27 -20 -49 -40 -49 -44 -1 -3 49 -79 110 -167 l110 -161 44 31 c176 126 333 350 418 594 30 86 77 282 77 320 0 8 -57 19 -167 34 -93 13 -182 25 -199 28 -31 5 -31 5 -37 -50z" - /> - <path - d="M3965 4233 c-106 -9 -348 -36 -415 -47 -55 -8 -75 -15 -74 -26 1 -20 + /> + <path + d="M3965 4233 c-106 -9 -348 -36 -415 -47 -55 -8 -75 -15 -74 -26 1 -20 56 -374 59 -377 1 -2 46 4 101 12 159 24 409 45 526 45 l108 0 0 200 0 200 -132 -2 c-73 -1 -151 -3 -173 -5z" - /> - <path - d="M3020 4079 c-85 -23 -292 -94 -368 -125 -97 -40 -298 -140 -305 -151 + /> + <path + d="M3020 4079 c-85 -23 -292 -94 -368 -125 -97 -40 -298 -140 -305 -151 -5 -7 172 -315 192 -336 4 -4 41 10 82 32 103 55 272 123 414 165 66 20 125 38 132 41 11 4 -4 70 -78 348 -10 39 -14 41 -69 26z" - /> - <path - d="M6955 3538 c-21 -91 -74 -362 -72 -364 7 -7 260 -44 367 -54 146 -13 + /> + <path + d="M6955 3538 c-21 -91 -74 -362 -72 -364 7 -7 260 -44 367 -54 146 -13 359 -13 475 0 49 6 90 12 91 13 2 1 -12 90 -29 197 -26 155 -36 194 -47 192 -8 -2 -85 -6 -170 -9 -160 -6 -357 7 -505 33 -103 18 -104 18 -110 -8z" - /> - <path - d="M1993 3513 c-52 -67 -71 -106 -98 -198 -35 -122 -44 -284 -21 -415 9 + /> + <path + d="M1993 3513 c-52 -67 -71 -106 -98 -198 -35 -122 -44 -284 -21 -415 9 -51 18 -96 21 -98 4 -5 360 79 375 88 7 4 7 24 0 60 -21 109 -7 244 31 307 l20 31 -146 131 c-80 72 -147 131 -149 131 -2 0 -17 -17 -33 -37z" - /> - <path - d="M2210 2519 c-91 -50 -166 -92 -168 -94 -2 -1 11 -26 28 -54 l32 -51 + /> + <path + d="M2210 2519 c-91 -50 -166 -92 -168 -94 -2 -1 11 -26 28 -54 l32 -51 244 0 c134 0 244 2 244 5 0 3 -23 33 -51 67 -28 35 -72 98 -97 140 -26 43 -51 77 -57 77 -5 0 -84 -41 -175 -90z" - /> - </g> - </svg> -); + /> + </g> + </svg> + ); +} export default TrailsIcon; diff --git a/src/client/views/nodes/FunctionPlotBox.tsx b/src/client/views/nodes/FunctionPlotBox.tsx index a86bdbd79..3d1bd7563 100644 --- a/src/client/views/nodes/FunctionPlotBox.tsx +++ b/src/client/views/nodes/FunctionPlotBox.tsx @@ -7,13 +7,14 @@ import { List } from '../../../fields/List'; import { listSpec } from '../../../fields/Schema'; import { Cast, StrCast } from '../../../fields/Types'; import { TraceMobx } from '../../../fields/util'; -import { DocUtils, Docs } from '../../documents/Documents'; +import { DocUtils } from '../../documents/DocUtils'; +import { DocumentType } from '../../documents/DocumentTypes'; +import { Docs } from '../../documents/Documents'; import { DragManager } from '../../util/DragManager'; import { undoBatch } from '../../util/UndoManager'; import { ViewBoxAnnotatableComponent } from '../DocComponent'; +import { PinDocView, PinProps } from '../PinFuncs'; import { FieldView, FieldViewProps } from './FieldView'; -import { PinProps, PresBox } from './trails'; -import { LinkManager } from '../../util/LinkManager'; @observer export class FunctionPlotBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { @@ -40,15 +41,15 @@ export class FunctionPlotBox extends ViewBoxAnnotatableComponent<FieldViewProps> } getAnchor = (addAsAnnotation: boolean, pinProps?: PinProps) => { const anchor = Docs.Create.ConfigDocument({ annotationOn: this.Document }); - PresBox.pinDocView(anchor, { pinDocLayout: pinProps?.pinDocLayout, pinData: { ...(pinProps?.pinData ?? {}), datarange: true } }, this.Document); + PinDocView(anchor, { pinDocLayout: pinProps?.pinDocLayout, pinData: { ...(pinProps?.pinData ?? {}), datarange: true } }, this.Document); anchor.config_xRange = new List<number>(Array.from(this._plot.options.xAxis.domain)); anchor.config_yRange = new List<number>(Array.from(this._plot.options.yAxis.domain)); if (addAsAnnotation) this.addDocument(anchor); return anchor; }; @computed get graphFuncs() { - const links = LinkManager.Instance.getAllRelatedLinks(this.Document) - .map(d => LinkManager.getOppositeAnchor(d, this.Document)) + const links = Doc.Links(this.Document) + .map(d => Doc.getOppositeAnchor(d, this.Document)) .filter(d => d) .map(d => d!); const funcs = links.concat(DocListCast(this.dataDoc[this.fieldKey])).map(doc => @@ -89,7 +90,7 @@ export class FunctionPlotBox extends ViewBoxAnnotatableComponent<FieldViewProps> drop = (e: Event, de: DragManager.DropEvent) => { if (de.complete.docDragData?.droppedDocuments.length) { const added = de.complete.docDragData.droppedDocuments.reduce((res, doc) => { - ///const ret = res && Doc.AddDocToList(this.dataDoc, this._props.fieldKey, doc); + // const ret = res && Doc.AddDocToList(this.dataDoc, this._props.fieldKey, doc); if (res) { const link = DocUtils.MakeLink(doc, this.Document, { link_relationship: 'function', link_description: 'input' }); link && this._props.addDocument?.(link); @@ -138,3 +139,8 @@ export class FunctionPlotBox extends ViewBoxAnnotatableComponent<FieldViewProps> ); } } + +Docs.Prototypes.TemplateMap.set(DocumentType.FUNCPLOT, { + layout: { view: FunctionPlotBox, dataField: 'data' }, + options: { acl: '', _layout_reflowHorizontal: true, _layout_reflowVertical: true, _layout_nativeDimEditable: true }, +}); diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx index a79dda74e..d4f8b5550 100644 --- a/src/client/views/nodes/ImageBox.tsx +++ b/src/client/views/nodes/ImageBox.tsx @@ -1,57 +1,62 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { Tooltip } from '@mui/material'; import { Colors } from 'browndash-components'; -import { action, computed, IReactionDisposer, makeObservable, observable, ObservableMap, reaction, runInAction } from 'mobx'; +import { action, computed, IReactionDisposer, makeObservable, observable, ObservableMap, reaction } from 'mobx'; import { observer } from 'mobx-react'; import { extname } from 'path'; import * as React from 'react'; -import { Doc, Opt } from '../../../fields/Doc'; +import { ClientUtils, DashColor, returnEmptyString, returnFalse, returnOne, returnZero, setupMoveUpEvents } from '../../../ClientUtils'; +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 { Cast, ImageCast, 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 { Docs, DocUtils } from '../../documents/Documents'; +import { emptyFunction } from '../../../Utils'; +import { Docs } from '../../documents/Documents'; import { DocumentType } from '../../documents/DocumentTypes'; +import { DocUtils } from '../../documents/DocUtils'; import { Networking } from '../../Network'; -import { DocumentManager } from '../../util/DocumentManager'; import { DragManager } from '../../util/DragManager'; import { undoBatch } from '../../util/UndoManager'; -import { ContextMenu } from '../../views/ContextMenu'; import { CollectionFreeFormView } from '../collections/collectionFreeForm/CollectionFreeFormView'; +import { ContextMenu } from '../ContextMenu'; import { ContextMenuProps } from '../ContextMenuItem'; import { ViewBoxAnnotatableComponent, ViewBoxInterface } from '../DocComponent'; import { MarqueeAnnotator } from '../MarqueeAnnotator'; import { OverlayView } from '../OverlayView'; import { AnchorMenu } from '../pdf/AnchorMenu'; -import { StyleProp } from '../StyleProvider'; -import { OpenWhere } from './DocumentView'; -import { FieldView, FieldViewProps, FocusViewOptions } from './FieldView'; +import { PinDocView, PinProps } from '../PinFuncs'; +import { StyleProp } from '../StyleProp'; +import { DocumentView } from './DocumentView'; +import { FieldView, FieldViewProps } from './FieldView'; +import { FocusViewOptions } from './FocusViewOptions'; import './ImageBox.scss'; -import { PinProps, PresBox } from './trails'; +import { OpenWhere } from './OpenWhere'; export class ImageEditorData { + // eslint-disable-next-line no-use-before-define private static _instance: ImageEditorData; private static get imageData() { return (ImageEditorData._instance ?? new ImageEditorData()).imageData; } // prettier-ignore @observable imageData: { rootDoc: Doc | undefined; open: boolean; source: string; addDoc: Opt<(doc: Doc | Doc[], annotationKey?: string) => boolean> } = observable({ rootDoc: undefined, open: false, source: '', addDoc: undefined }); - @action private static set = (open: boolean, rootDoc: Doc | undefined, source: string, addDoc: Opt<(doc: Doc | Doc[], annotationKey?: string) => boolean>) => (this._instance.imageData = { open, rootDoc, source, addDoc }); + @action private static set = (open: boolean, rootDoc: Doc | undefined, source: string, addDoc: Opt<(doc: Doc | Doc[], annotationKey?: string) => boolean>) => { + this._instance.imageData = { open, rootDoc, source, addDoc }; + }; constructor() { makeObservable(this); ImageEditorData._instance = this; } - public static get Open() { return ImageEditorData.imageData.open; } // prettier-ignore - public static get Source() { return ImageEditorData.imageData.source; } // prettier-ignore - public static get RootDoc() { return ImageEditorData.imageData.rootDoc; } // prettier-ignore - public static get AddDoc() { return ImageEditorData.imageData.addDoc; } // prettier-ignore + public static get Open() { return ImageEditorData.imageData.open; } // prettier-ignore public static set Open(open: boolean) { ImageEditorData.set(open, this.imageData.rootDoc, this.imageData.source, this.imageData.addDoc); } // prettier-ignore + public static get Source() { return ImageEditorData.imageData.source; } // prettier-ignore public static set Source(source: string) { ImageEditorData.set(this.imageData.open, this.imageData.rootDoc, source, this.imageData.addDoc); } // prettier-ignore + public static get RootDoc() { return ImageEditorData.imageData.rootDoc; } // prettier-ignore public static set RootDoc(rootDoc: Opt<Doc>) { ImageEditorData.set(this.imageData.open, rootDoc, this.imageData.source, this.imageData.addDoc); } // prettier-ignore + public static get AddDoc() { return ImageEditorData.imageData.addDoc; } // prettier-ignore public static set AddDoc(addDoc: Opt<(doc: Doc | Doc[], annotationKey?: string) => boolean>) { ImageEditorData.set(this.imageData.open, this.imageData.rootDoc, this.imageData.source, addDoc); } // prettier-ignore } @observer @@ -66,7 +71,13 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() impl private _getAnchor: (savedAnnotations: Opt<ObservableMap<number, HTMLDivElement[]>>, addAsAnnotation: boolean) => Opt<Doc> = () => undefined; private _overlayIconRef = React.createRef<HTMLDivElement>(); private _marqueeref = React.createRef<MarqueeAnnotator>(); + private _mainCont: React.RefObject<HTMLDivElement> = React.createRef(); + private _annotationLayer: React.RefObject<HTMLDivElement> = React.createRef(); + @observable _savedAnnotations = new ObservableMap<number, HTMLDivElement[]>(); @observable _curSuffix = ''; + @observable _error = ''; + @observable _isHovering = false; // flag to switch between primary and alternate images on hover + _ffref = React.createRef<CollectionFreeFormView>(); constructor(props: FieldViewProps) { super(props); @@ -93,7 +104,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() impl if (anchor) { if (!addAsAnnotation) anchor.backgroundColor = 'transparent'; addAsAnnotation && this.addDocument(anchor); - PresBox.pinDocView(anchor, { pinDocLayout: pinProps?.pinDocLayout, pinData: { ...(pinProps?.pinData ?? {}), pannable: visibleAnchor ? false : true } }, this.Document); + PinDocView(anchor, { pinDocLayout: pinProps?.pinDocLayout, pinData: { ...(pinProps?.pinData ?? {}), pannable: !visibleAnchor } }, this.Document); return anchor; } return this.Document; @@ -106,10 +117,12 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() impl scrSize: (this.ScreenToLocalBoxXf().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'), + ({ 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 } ); - const layoutDoc = this.layoutDoc; + const { layoutDoc } = this; this._disposers.path = reaction( () => ({ nativeSize: this.nativeSize, width: NumCast(this.layoutDoc._width) }), ({ nativeSize, width }) => { @@ -121,10 +134,10 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() impl ); this._disposers.scroll = reaction( () => this.layoutDoc.layout_scrollTop, - s_top => { + sTop => { this._forcedScroll = true; - !this._ignoreScroll && this._mainCont.current && (this._mainCont.current.scrollTop = NumCast(s_top)); - this._mainCont.current?.scrollTo({ top: NumCast(s_top) }); + !this._ignoreScroll && this._mainCont.current && (this._mainCont.current.scrollTop = NumCast(sTop)); + this._mainCont.current?.scrollTo({ top: NumCast(sTop) }); this._forcedScroll = false; }, { fireImmediately: true } @@ -138,7 +151,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() impl @undoBatch drop = (e: Event, de: DragManager.DropEvent) => { if (de.complete.docDragData) { - let added: boolean | undefined = undefined; + let added: boolean | undefined; const targetIsBullseye = (ele: HTMLElement): boolean => { if (!ele) return false; if (ele === this._overlayIconRef.current) return true; @@ -168,7 +181,9 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() impl }; @undoBatch - resolution = () => (this.layoutDoc._showFullRes = !this.layoutDoc._showFullRes); + resolution = () => { + this.layoutDoc._showFullRes = !this.layoutDoc._showFullRes; + }; @undoBatch setNativeSize = action(() => { @@ -189,7 +204,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() impl const nh = NumCast(this.dataDoc[this.fieldKey + '_nativeHeight']); const w = this.layoutDoc._width; const h = this.layoutDoc._height; - this.dataDoc[this.fieldKey + '-rotation'] = (NumCast(this.dataDoc[this.fieldKey + '-rotation']) + 90) % 360; + this.dataDoc[this.fieldKey + '_rotation'] = (NumCast(this.dataDoc[this.fieldKey + '_rotation']) + 90) % 360; this.dataDoc[this.fieldKey + '_nativeWidth'] = nh; this.dataDoc[this.fieldKey + '_nativeHeight'] = nw; this.layoutDoc._width = h; @@ -197,7 +212,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() impl }); crop = (region: Doc | undefined, addCrop?: boolean) => { - if (!region) return; + if (!region) return undefined; const cropping = Doc.MakeCopy(region, true); const regionData = region[DocData]; regionData.lockedPosition = true; @@ -223,8 +238,8 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() impl croppingProto.type = DocumentType.IMG; croppingProto.layout = ImageBox.LayoutString('data'); croppingProto.data = ObjectField.MakeCopy(this.dataDoc[this.fieldKey] as ObjectField); - croppingProto['data_nativeWidth'] = anchw; - croppingProto['data_nativeHeight'] = anchh; + croppingProto.data_nativeWidth = anchw; + croppingProto.data_nativeHeight = anchh; croppingProto.freeform_scale = viewScale; croppingProto.freeform_scale_min = viewScale; croppingProto.freeform_panX = anchx / viewScale; @@ -239,19 +254,19 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() impl cropping.y = NumCast(this.Document.y); this._props.addDocTab(cropping, OpenWhere.inParent); } - DocumentManager.Instance.AddViewRenderedCb(cropping, dv => setTimeout(() => (dv.ComponentView as ImageBox).setNativeSize(), 200)); + DocumentView.addViewRenderedCb(cropping, dv => setTimeout(() => (dv.ComponentView as ImageBox).setNativeSize(), 200)); this._props.bringToFront?.(cropping); return cropping; }; - specificContextMenu = (e: React.MouseEvent): void => { + specificContextMenu = (): void => { const field = Cast(this.dataDoc[this.fieldKey], ImageField); if (field) { const funcs: ContextMenuProps[] = []; funcs.push({ description: 'Rotate Clockwise 90', event: this.rotate, icon: 'redo-alt' }); funcs.push({ description: `Show ${this.layoutDoc._showFullRes ? 'Dynamic Res' : 'Full Res'}`, event: this.resolution, icon: 'expand' }); funcs.push({ description: 'Set Native Pixel Size', event: this.setNativeSize, icon: 'expand-arrows-alt' }); - funcs.push({ description: 'Copy path', event: () => Utils.CopyText(this.choosePath(field.url)), icon: 'copy' }); + funcs.push({ description: 'Copy path', event: () => ClientUtils.CopyText(this.choosePath(field.url)), icon: 'copy' }); funcs.push({ description: 'Open Image Editor', event: action(() => { @@ -266,23 +281,23 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() impl } }; - choosePath(url: URL) { + choosePath = (url: URL) => { if (!url?.href) return ''; const lower = url.href.toLowerCase(); if (url.protocol === 'data') return url.href; - if (url.href.indexOf(window.location.origin) === -1 && url.href.indexOf('dashblobstore') === -1) return Utils.CorsProxy(url.href); + if (url.href.indexOf(window.location.origin) === -1 && url.href.indexOf('dashblobstore') === -1) return ClientUtils.CorsProxy(url.href); if (!/\.(png|jpg|jpeg|gif|webp)$/.test(lower) || lower.endsWith('/assets/unknown-file-icon-hi.png')) return `/assets/unknown-file-icon-hi.png`; const ext = extname(url.href); return url.href.replace(ext, (this._error ? '_o' : this._curSuffix) + ext); - } + }; 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(); const nativeWidth = NumCast(this.dataDoc[this.fieldKey + '_nativeWidth'], NumCast(this.layoutDoc[this.fieldKey + '_nativeWidth'], 500)); const nativeHeight = NumCast(this.dataDoc[this.fieldKey + '_nativeHeight'], NumCast(this.layoutDoc[this.fieldKey + '_nativeHeight'], 500)); - const nativeOrientation = NumCast(this.dataDoc[this.fieldKey + '-nativeOrientation'], 1); + const nativeOrientation = NumCast(this.dataDoc[this.fieldKey + '_nativeOrientation'], 1); return { nativeWidth, nativeHeight, nativeOrientation }; } @computed get overlayImageIcon() { @@ -307,7 +322,11 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() impl <div className="imageBox-alternateDropTarget" 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))} + onPointerDown={e => + setupMoveUpEvents(e.target, e, returnFalse, emptyFunction, () => { + this.layoutDoc[`_${this.fieldKey}_usePath`] = usePath === undefined ? 'alternate' : usePath === 'alternate' ? 'alternate:hover' : undefined; + }) + } style={{ display: (this._props.isContentActive() !== false && DragManager.DocDragData?.canEmbed) || this.dataDoc[this.fieldKey + '_alternates'] ? 'block' : 'none', width: 'min(10%, 25px)', @@ -323,8 +342,8 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() impl @computed get paths() { const field = Cast(this.dataDoc[this.fieldKey], ImageField, null); // retrieve the primary image URL that is being rendered from the data doc - const alts = this.dataDoc[this.fieldKey + '_alternates'] as any as List<Doc>; // retrieve alternate documents that may be rendered as alternate images - const defaultUrl = new URL(Utils.prepend('/assets/unknown-file-icon-hi.png')); + const alts = DocListCast(this.dataDoc[this.fieldKey + '_alternates']); // retrieve alternate documents that may be rendered as alternate images + const defaultUrl = new URL(ClientUtils.prepend('/assets/unknown-file-icon-hi.png')); const altpaths = alts ?.map(doc => (doc instanceof Doc ? ImageCast(doc[Doc.LayoutFieldKey(doc)])?.url ?? defaultUrl : defaultUrl)) @@ -334,9 +353,6 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() impl return paths.length ? paths : [defaultUrl.href]; } - @observable _error = ''; - - @observable _isHovering = false; // flag to switch between primary and alternate images on hover @computed get content() { TraceMobx(); @@ -344,8 +360,8 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() impl 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(); - const { nativeWidth, nativeHeight, nativeOrientation } = this.nativeSize; - const rotation = NumCast(this.dataDoc[this.fieldKey + '-rotation']); + const { nativeWidth, nativeHeight /* , nativeOrientation */ } = this.nativeSize; + const rotation = NumCast(this.dataDoc[this.fieldKey + '_rotation']); const aspect = rotation % 180 ? nativeHeight / nativeWidth : 1; let transformOrigin = 'center center'; let transform = `translate(0%, 0%) rotate(${rotation}deg) scale(${aspect})`; @@ -361,12 +377,32 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() impl const usePath = this.layoutDoc[`_${this.fieldKey}_usePath`]; return ( - <div className="imageBox-cont" onPointerEnter={action(() => (this._isHovering = true))} onPointerLeave={action(() => (this._isHovering = false))} key={this.layoutDoc[Id]} ref={this.createDropTarget} onPointerDown={this.marqueeDown}> + <div + className="imageBox-cont" + onPointerEnter={action(() => { + this._isHovering = true; + })} + onPointerLeave={action(() => { + this._isHovering = false; + })} + key={this.layoutDoc[Id]} + ref={this.createDropTarget} + onPointerDown={this.marqueeDown}> <div className="imageBox-fader" style={{ opacity: backAlpha }}> - <img key="paths" src={srcpath} style={{ transform, transformOrigin }} onError={action(e => (this._error = e.toString()))} draggable={false} width={nativeWidth} /> + <img + alt="" + key="paths" + src={srcpath} + style={{ transform, transformOrigin }} + onError={action(e => { + this._error = e.toString(); + })} + draggable={false} + width={nativeWidth} + /> {fadepath === srcpath ? null : ( <div className={`imageBox-fadeBlocker${(this._isHovering && usePath === 'alternate:hover') || usePath === 'alternate' ? '-hover' : ''}`} style={{ transition: StrCast(this.layoutDoc.viewTransition, 'opacity 1000ms') }}> - <img className="imageBox-fadeaway" key="fadeaway" src={fadepath} style={{ transform, transformOrigin }} draggable={false} width={nativeWidth} /> + <img alt="" className="imageBox-fadeaway" key="fadeaway" src={fadepath} style={{ transform, transformOrigin }} draggable={false} width={nativeWidth} /> </div> )} </div> @@ -375,23 +411,27 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() impl ); } - private _mainCont: React.RefObject<HTMLDivElement> = React.createRef(); - private _annotationLayer: React.RefObject<HTMLDivElement> = React.createRef(); - @observable _savedAnnotations = new ObservableMap<number, HTMLDivElement[]>(); @computed get annotationLayer() { TraceMobx(); return <div className="imageBox-annotationLayer" style={{ height: this._props.PanelHeight() }} ref={this._annotationLayer} />; } screenToLocalTransform = () => this.ScreenToLocalBoxXf().translate(0, NumCast(this.layoutDoc._layout_scrollTop) * this.ScreenToLocalBoxXf().Scale); marqueeDown = (e: React.PointerEvent) => { - if (!this.dataDoc[this.fieldKey]) return this.chooseImage(); - if (!e.altKey && e.button === 0 && NumCast(this.layoutDoc._freeform_scale, 1) <= NumCast(this.dataDoc.freeform_scaleMin, 1) && this._props.isContentActive() && ![InkTool.Highlighter, InkTool.Pen, InkTool.Write].includes(Doc.ActiveTool)) { + if (!this.dataDoc[this.fieldKey]) { + this.chooseImage(); + } else if ( + !e.altKey && + e.button === 0 && + NumCast(this.layoutDoc._freeform_scale, 1) <= NumCast(this.dataDoc.freeform_scaleMin, 1) && + this._props.isContentActive() && + ![InkTool.Highlighter, InkTool.Pen, InkTool.Write].includes(Doc.ActiveTool) + ) { setupMoveUpEvents( this, e, - action(e => { + action(moveEv => { MarqueeAnnotator.clearAnnotations(this._savedAnnotations); - this._marqueeref.current?.onInitiateSelection([e.clientX, e.clientY]); + this._marqueeref.current?.onInitiateSelection([moveEv.clientX, moveEv.clientY]); return true; }), returnFalse, @@ -408,7 +448,6 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() impl }; focus = (anchor: Doc, options: FocusViewOptions) => (anchor.type === DocumentType.CONFIG ? undefined : this._ffref.current?.focus(anchor, options)); - _ffref = React.createRef<CollectionFreeFormView>(); savedAnnotations = () => this._savedAnnotations; render() { TraceMobx(); @@ -419,7 +458,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() impl className="imageBox" onContextMenu={this.specificContextMenu} ref={this._mainCont} - onScroll={action(e => { + onScroll={action(() => { if (!this._forcedScroll) { if (this.layoutDoc._layout_scrollTop || this._mainCont.current?.scrollTop) { this._ignoreScroll = true; @@ -437,6 +476,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() impl }}> <CollectionFreeFormView ref={this._ffref} + // eslint-disable-next-line react/jsx-props-no-spreading {...this._props} setContentViewBox={emptyFunction} NativeWidth={returnZero} @@ -444,8 +484,8 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() impl renderDepth={this._props.renderDepth + 1} fieldKey={this.annotationKey} styleProvider={this._props.styleProvider} - isAnnotationOverlay={true} - annotationLayerHostsContent={true} + isAnnotationOverlay + annotationLayerHostsContent PanelWidth={this._props.PanelWidth} PanelHeight={this._props.PanelHeight} ScreenToLocalTransform={this.screenToLocalTransform} @@ -476,7 +516,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() impl selectionText={returnEmptyString} annotationLayer={this._annotationLayer.current} marqueeContainer={this._mainCont.current} - highlightDragSrcColor={''} + highlightDragSrcColor="" anchorMenuCrop={this.crop} /> )} @@ -489,7 +529,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() impl input.type = 'file'; input.multiple = true; input.accept = 'image/*'; - input.onchange = async _e => { + input.onchange = async () => { const file = input.files?.[0]; if (file) { const disposer = OverlayView.ShowSpinner(); @@ -508,3 +548,8 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() impl input.click(); }; } + +Docs.Prototypes.TemplateMap.set(DocumentType.IMG, { + layout: { view: ImageBox, dataField: 'data' }, + options: { acl: '', freeform: '', systemIcon: 'BsFileEarmarkImageFill' }, +}); diff --git a/src/client/views/nodes/KeyValueBox.tsx b/src/client/views/nodes/KeyValueBox.tsx index 31a2367fc..66e210c03 100644 --- a/src/client/views/nodes/KeyValueBox.tsx +++ b/src/client/views/nodes/KeyValueBox.tsx @@ -1,26 +1,28 @@ +/* eslint-disable jsx-a11y/control-has-associated-label */ import { action, computed, makeObservable, observable } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; -import { returnAlways, returnTrue } from '../../../Utils'; -import { Doc, Field, FieldResult } from '../../../fields/Doc'; +import { returnAlways, returnTrue } from '../../../ClientUtils'; +import { Doc, Field, FieldResult, FieldType } from '../../../fields/Doc'; import { List } from '../../../fields/List'; import { RichTextField } from '../../../fields/RichTextField'; import { ComputedField, ScriptField } from '../../../fields/ScriptField'; import { DocCast } from '../../../fields/Types'; import { ImageField } from '../../../fields/URLField'; +import { DocumentType } from '../../documents/DocumentTypes'; import { Docs } from '../../documents/Documents'; import { SetupDrag } from '../../util/DragManager'; import { CompiledScript } from '../../util/Scripting'; -import { undoBatch } from '../../util/UndoManager'; +import { undoable } from '../../util/UndoManager'; import { ContextMenu } from '../ContextMenu'; import { ContextMenuProps } from '../ContextMenuItem'; -import { ObservableReactComponent } from '../ObservableReactComponent'; +import { ViewBoxBaseComponent } from '../DocComponent'; import { DocumentIconContainer } from './DocumentIcon'; -import { OpenWhere } from './DocumentView'; import { FieldView, FieldViewProps } from './FieldView'; import { ImageBox } from './ImageBox'; import './KeyValueBox.scss'; import { KeyValuePair } from './KeyValuePair'; +import { OpenWhere } from './OpenWhere'; import { FormattedTextBox } from './formattedText/FormattedTextBox'; export type KVPScript = { @@ -29,7 +31,7 @@ export type KVPScript = { onDelegate: boolean; }; @observer -export class KeyValueBox extends ObservableReactComponent<FieldViewProps> { +export class KeyValueBox extends ViewBoxBaseComponent<FieldViewProps>() { public static LayoutString() { return FieldView.LayoutString(KeyValueBox, 'data'); } @@ -46,16 +48,16 @@ export class KeyValueBox extends ObservableReactComponent<FieldViewProps> { componentDidMount() { this._props.setContentViewBox?.(this); } - isKeyValueBox = returnTrue; - able = returnAlways; - layout_fitWidth = returnTrue; - onClickScriptDisable = returnAlways; + // ViewBoxInterface overrides + override isUnstyledView = returnTrue; // used by style provider via ViewBoxInterface - ignore opacity, anim effects, titles + override dontRegisterView = returnTrue; // don't want to follow links to this view + override onClickScriptDisable = returnAlways; @observable private rows: KeyValuePair[] = []; @observable _splitPercentage = 50; get fieldDocToLayout() { - return DocCast(this._props.Document); + return DocCast(this.Document); } @action @@ -79,7 +81,8 @@ export class KeyValueBox extends ObservableReactComponent<FieldViewProps> { * @param value * @returns */ - public static CompileKVPScript(rawvalue: string): KVPScript | undefined { + public static CompileKVPScript = (rawvalueIn: string): KVPScript | undefined => { + let rawvalue = rawvalueIn; const onDelegate = rawvalue.startsWith('='); rawvalue = onDelegate ? rawvalue.substring(1) : rawvalue; const type: 'computed' | 'script' | false = rawvalue.startsWith(':=') ? 'computed' : rawvalue.startsWith('$=') ? 'script' : false; @@ -87,28 +90,28 @@ export class KeyValueBox extends ObservableReactComponent<FieldViewProps> { rawvalue = rawvalue.replace(/.*\(\((.*)\)\)/, 'dashCallChat(_setCacheResult_, this, `$1`)'); const value = ["'", '"', '`'].includes(rawvalue.length ? rawvalue[0] : '') || !isNaN(rawvalue as any) ? rawvalue : '`' + rawvalue + '`'; - var script = ScriptField.CompileScript(rawvalue, {}, true, undefined, DocumentIconContainer.getTransformer()); + let script = ScriptField.CompileScript(rawvalue, {}, true, undefined, DocumentIconContainer.getTransformer()); if (!script.compiled) { script = ScriptField.CompileScript(value, {}, true, undefined, DocumentIconContainer.getTransformer()); } return !script.compiled ? undefined : { script, type, onDelegate }; - } + }; - public static ApplyKVPScript(doc: Doc, key: string, kvpScript: KVPScript, forceOnDelegate?: boolean, setResult?: (value: FieldResult) => void) { + public static ApplyKVPScript = (doc: Doc, key: string, kvpScript: KVPScript, forceOnDelegate?: boolean, setResult?: (value: FieldResult) => void) => { const { script, type, onDelegate } = kvpScript; - //const target = onDelegate ? Doc.Layout(doc.layout) : Doc.GetProto(doc); // bcz: TODO need to be able to set fields on layout templates + // const target = onDelegate ? Doc.Layout(doc.layout) : Doc.GetProto(doc); // bcz: TODO need to be able to set fields on layout templates const target = forceOnDelegate || onDelegate || key.startsWith('_') ? doc : DocCast(doc.proto, doc); - let field: Field | undefined; + let field: FieldType | undefined; switch (type) { case 'computed': field = new ComputedField(script); break; // prettier-ignore case 'script': field = new ScriptField(script); break; // prettier-ignore default: { const _setCacheResult_ = (value: FieldResult) => { - field = value as Field; + field = value as FieldType; if (setResult) setResult?.(value); else target[key] = field; }; - const res = script.run({ this: Doc.Layout(doc), self: doc, _setCacheResult_ }, console.log); + const res = script.run({ this: Doc.Layout(doc), _setCacheResult_ }, console.log); if (!res.success) { if (key) target[key] = script.originalScript; return false; @@ -122,14 +125,13 @@ export class KeyValueBox extends ObservableReactComponent<FieldViewProps> { return true; } return false; - } + }; - @undoBatch - public static SetField(doc: Doc, key: string, value: string, forceOnDelegate?: boolean, setResult?: (value: FieldResult) => void) { - const script = this.CompileKVPScript(value); + public static SetField = undoable((doc: Doc, key: string, value: string, forceOnDelegate?: boolean, setResult?: (value: FieldResult) => void) => { + const script = KeyValueBox.CompileKVPScript(value); if (!script) return false; - return this.ApplyKVPScript(doc, key, script, forceOnDelegate, setResult); - } + return KeyValueBox.ApplyKVPScript(doc, key, script, forceOnDelegate, setResult); + }, 'Set Doc Field'); onPointerDown = (e: React.PointerEvent): void => { if (e.buttons === 1 && this._props.isSelected()) { @@ -153,20 +155,20 @@ export class KeyValueBox extends ObservableReactComponent<FieldViewProps> { const ids: { [key: string]: string } = {}; const protos = Doc.GetAllPrototypes(doc); - for (const proto of protos) { + protos.forEach(proto => { Object.keys(proto).forEach(key => { if (!(key in ids) && realDoc[key] !== ComputedField.undefined) { ids[key] = key; } }); - } + }); const rows: JSX.Element[] = []; let i = 0; const self = this; const keys = Object.keys(ids).slice(); - //for (const key of [...keys.filter(id => id !== 'layout' && !id.includes('_')).sort(), ...keys.filter(id => id === 'layout' || id.includes('_')).sort()]) { - for (const key of keys.sort((a: string, b: string) => { + // for (const key of [...keys.filter(id => id !== 'layout' && !id.includes('_')).sort(), ...keys.filter(id => id === 'layout' || id.includes('_')).sort()]) { + const sortedKeys = keys.sort((a: string, b: string) => { const a_ = a.split('_')[0]; const b_ = b.split('_')[0]; if (a_ < b_) return -1; @@ -174,7 +176,8 @@ export class KeyValueBox extends ObservableReactComponent<FieldViewProps> { if (a === a_) return -1; if (b === b_) return 1; return a === b ? 0 : a < b ? -1 : 1; - })) { + }); + sortedKeys.forEach(key => { rows.push( <KeyValuePair doc={realDoc} @@ -195,7 +198,7 @@ export class KeyValueBox extends ObservableReactComponent<FieldViewProps> { keyName={key} /> ); - } + }); return rows; } @computed get newKeyValue() { @@ -229,7 +232,7 @@ export class KeyValueBox extends ObservableReactComponent<FieldViewProps> { this._splitPercentage = Math.max(0, 100 - Math.round(((e.clientX - nativeWidth.left) / nativeWidth.width) * 100)); }; @action - onDividerUp = (e: PointerEvent): void => { + onDividerUp = (): void => { document.removeEventListener('pointermove', this.onDividerMove); document.removeEventListener('pointerup', this.onDividerUp); }; @@ -243,35 +246,36 @@ export class KeyValueBox extends ObservableReactComponent<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 }); - for (const row of rows) { - const field = this.createFieldView(DocCast(this._props.Document), row); + const parent = Docs.Create.StackingDocument([], { _layout_autoHeight: true, _width: 300, title: `field views for ${DocCast(this.Document).title}`, _chromeHidden: true }); + rows.forEach(row => { + const field = this.createFieldView(DocCast(this.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.Document), rows.lastElement()) : undefined; }; createFieldView = (templateDoc: Doc, row: KeyValuePair) => { const metaKey = row._props.keyName; - const fieldTemplate = Doc.IsDelegateField(templateDoc, metaKey) ? Doc.MakeDelegate(templateDoc) : Doc.MakeEmbedding(templateDoc); - fieldTemplate.title = metaKey; - fieldTemplate.layout_fitWidth = true; - fieldTemplate._xMargin = 10; - fieldTemplate._yMargin = 10; - fieldTemplate._width = 100; - fieldTemplate._height = 40; - fieldTemplate.layout = this.inferType(templateDoc[metaKey], metaKey); - return fieldTemplate; + const fieldTempDoc = Doc.IsDelegateField(templateDoc, metaKey) ? Doc.MakeDelegate(templateDoc) : Doc.MakeEmbedding(templateDoc); + fieldTempDoc.title = metaKey; + fieldTempDoc.layout_fitWidth = true; + fieldTempDoc._xMargin = 10; + fieldTempDoc._yMargin = 10; + fieldTempDoc._width = 100; + fieldTempDoc._height = 40; + fieldTempDoc.layout = this.inferType(templateDoc[metaKey], metaKey); + return fieldTempDoc; }; inferType = (data: FieldResult, metaKey: string) => { const options = { _width: 300, _height: 300, title: metaKey }; if (data instanceof RichTextField || typeof data === 'string' || typeof data === 'number') { return FormattedTextBox.LayoutString(metaKey); - } else if (data instanceof List) { + } + if (data instanceof List) { if (data.length === 0) { return Docs.Create.StackingDocument([], options); } @@ -280,28 +284,25 @@ export class KeyValueBox extends ObservableReactComponent<FieldViewProps> { return Docs.Create.StackingDocument([], options); } switch (first.data.constructor) { - case RichTextField: - return Docs.Create.TreeDocument([], options); - case ImageField: - return Docs.Create.MasonryDocument([], options); - default: - console.log(`Template for ${first.data.constructor} not supported!`); - return undefined; - } + case RichTextField: return Docs.Create.TreeDocument([], options); + case ImageField: return Docs.Create.MasonryDocument([], options); + default: console.log(`Template for ${first.data.constructor} not supported!`); + return undefined; + } // prettier-ignore } else if (data instanceof ImageField) { return ImageBox.LayoutString(metaKey); } return new Doc(); }; - specificContextMenu = (e: React.MouseEvent): void => { + specificContextMenu = (): void => { const cm = ContextMenu.Instance; const open = cm.findByDescription('Change Perspective...'); const openItems: ContextMenuProps[] = open && 'subitems' in open ? open.subitems : []; openItems.push({ description: 'Default Perspective', event: () => { - this._props.addDocTab(this._props.Document, OpenWhere.close); + this._props.addDocTab(this.Document, OpenWhere.close); this._props.addDocTab(this.fieldDocToLayout, OpenWhere.addRight); }, icon: 'image', @@ -337,4 +338,12 @@ export class KeyValueBox extends ObservableReactComponent<FieldViewProps> { </div> ); } + public static Init() { + Doc.SetField = KeyValueBox.SetField; + } } + +Docs.Prototypes.TemplateMap.set(DocumentType.KVP, { + layout: { view: KeyValueBox, dataField: 'data' }, + options: { acl: '', _layout_fitWidth: true, _height: 150 }, +}); diff --git a/src/client/views/nodes/KeyValuePair.tsx b/src/client/views/nodes/KeyValuePair.tsx index f9e8ce4f3..397cc15ed 100644 --- a/src/client/views/nodes/KeyValuePair.tsx +++ b/src/client/views/nodes/KeyValuePair.tsx @@ -1,8 +1,10 @@ +/* eslint-disable jsx-a11y/control-has-associated-label */ import { Tooltip } from '@mui/material'; import { action, makeObservable, observable } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; -import { emptyFunction, returnEmptyDoclist, returnEmptyFilter, returnFalse, returnZero } from '../../../Utils'; +import { returnEmptyDoclist, returnEmptyFilter, returnFalse, returnZero } from '../../../ClientUtils'; +import { emptyFunction } from '../../../Utils'; import { Doc, Field } from '../../../fields/Doc'; import { DocCast } from '../../../fields/Types'; import { DocumentOptions, FInfo } from '../../documents/Documents'; @@ -12,10 +14,10 @@ import { ContextMenu } from '../ContextMenu'; import { EditableView } from '../EditableView'; import { ObservableReactComponent } from '../ObservableReactComponent'; import { DefaultStyleProvider } from '../StyleProvider'; -import { OpenWhere, returnEmptyDocViewList } from './DocumentView'; -import { KeyValueBox } from './KeyValueBox'; +import { returnEmptyDocViewList } from './DocumentView'; import './KeyValueBox.scss'; import './KeyValuePair.scss'; +import { OpenWhere } from './OpenWhere'; // Represents one row in a key value plane @@ -62,7 +64,7 @@ export class KeyValuePair extends ObservableReactComponent<KeyValuePairProps> { render() { // let fieldKey = Object.keys(props.Document).indexOf(props.fieldKey) !== -1 ? props.fieldKey : "(" + props.fieldKey + ")"; let protoCount = 0; - let doc = this._props.doc; + let { doc } = this._props; while (doc) { if (Object.keys(doc).includes(this._props.keyName)) { break; @@ -76,10 +78,18 @@ export class KeyValuePair extends ObservableReactComponent<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))}> + <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 + type="button" style={hover} className="keyValuePair-td-key-delete" onClick={undoBatch(() => { @@ -91,7 +101,7 @@ export class KeyValuePair extends ObservableReactComponent<KeyValuePairProps> { </button> <input className="keyValuePair-td-key-check" type="checkbox" style={hover} onChange={this.handleCheck} ref={this.checkbox} /> <Tooltip title={Object.entries(new DocumentOptions()).find((pair: [string, FInfo]) => pair[0].replace(/^_/, '') === this._props.keyName)?.[1].description ?? ''}> - <div className="keyValuePair-keyField" style={{ marginLeft: 20 * (this._props.keyName.match(/_/g)?.length || 0), color: keyStyle }}> + <div className="keyValuePair-keyField" style={{ marginLeft: 20 * (this._props.keyName.replace(/__/g, '').match(/_/g)?.length || 0), color: keyStyle }}> {'('.repeat(parenCount)} {this._props.keyName} {')'.repeat(parenCount)} @@ -125,7 +135,7 @@ export class KeyValuePair extends ObservableReactComponent<KeyValuePairProps> { pinToPres: returnZero, }} GetValue={() => Field.toKeyValueString(this._props.doc, this._props.keyName)} - SetValue={(value: string) => KeyValueBox.SetField(this._props.doc, this._props.keyName, value)} + SetValue={(value: string) => Doc.SetField(this._props.doc, this._props.keyName, value)} /> </div> </td> diff --git a/src/client/views/nodes/LabelBox.tsx b/src/client/views/nodes/LabelBox.tsx index 74e78c671..f80ff5f94 100644 --- a/src/client/views/nodes/LabelBox.tsx +++ b/src/client/views/nodes/LabelBox.tsx @@ -1,21 +1,22 @@ import { action, computed, makeObservable, observable } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; -import { Doc, DocListCast, Field } from '../../../fields/Doc'; +import { Doc, DocListCast, Field, FieldType } from '../../../fields/Doc'; import { List } from '../../../fields/List'; import { listSpec } from '../../../fields/Schema'; import { BoolCast, Cast, NumCast, StrCast } from '../../../fields/Types'; +import { DocumentType } from '../../documents/DocumentTypes'; +import { Docs } from '../../documents/Documents'; import { DragManager } from '../../util/DragManager'; import { undoBatch } from '../../util/UndoManager'; import { ContextMenu } from '../ContextMenu'; import { ContextMenuProps } from '../ContextMenuItem'; import { ViewBoxBaseComponent } from '../DocComponent'; -import { StyleProp } from '../StyleProvider'; +import { PinDocView, PinProps } from '../PinFuncs'; +import { StyleProp } from '../StyleProp'; import { FieldView, FieldViewProps } from './FieldView'; import BigText from './LabelBigText'; import './LabelBox.scss'; -import { PinProps, PresBox } from './trails'; -import { Docs } from '../../documents/Documents'; @observer export class LabelBox extends ViewBoxBaseComponent<FieldViewProps>() { @@ -23,7 +24,7 @@ export class LabelBox extends ViewBoxBaseComponent<FieldViewProps>() { return FieldView.LayoutString(LabelBox, fieldKey); } public static LayoutStringWithTitle(fieldStr: string, label?: string) { - return !label ? LabelBox.LayoutString(fieldStr) : `<LabelBox fieldKey={'${fieldStr}'} label={'${label}'} {...props} />`; //e.g., "<ImageBox {...props} fieldKey={"data} />" + return !label ? LabelBox.LayoutString(fieldStr) : `<LabelBox fieldKey={'${fieldStr}'} label={'${label}'} {...props} />`; // e.g., "<ImageBox {...props} fieldKey={"data} />" } private dropDisposer?: DragManager.DragDropDisposer; private _timeout: any; @@ -41,7 +42,7 @@ export class LabelBox extends ViewBoxBaseComponent<FieldViewProps>() { } @computed get Title() { - return Field.toString(this.dataDoc[this.fieldKey] as Field) || StrCast(this.Document.title); + return Field.toString(this.dataDoc[this.fieldKey] as FieldType) || StrCast(this.Document.title); } protected createDropTarget = (ele: HTMLDivElement) => { @@ -54,14 +55,16 @@ export class LabelBox extends ViewBoxBaseComponent<FieldViewProps>() { get paramsDoc() { return Doc.AreProtosEqual(this.layoutDoc, this.dataDoc) ? this.dataDoc : this.layoutDoc; } - specificContextMenu = (e: React.MouseEvent): void => { + specificContextMenu = (): void => { const funcs: ContextMenuProps[] = []; !Doc.noviceMode && funcs.push({ description: 'Clear Script Params', event: () => { const params = Cast(this.paramsDoc['onClick-paramFieldKeys'], listSpec('string'), []); - params?.map(p => (this.paramsDoc[p] = undefined)); + params?.forEach(p => { + this.paramsDoc[p] = undefined; + }); }, icon: 'trash', }); @@ -71,7 +74,7 @@ export class LabelBox extends ViewBoxBaseComponent<FieldViewProps>() { @undoBatch drop = (e: Event, de: DragManager.DropEvent) => { - const docDragData = de.complete.docDragData; + const { docDragData } = de.complete; const params = Cast(this.paramsDoc['onClick-paramFieldKeys'], listSpec('string'), []); const missingParams = params?.filter(p => !this.paramsDoc[p]); if (docDragData && missingParams?.includes((e.target as any).textContent)) { @@ -94,7 +97,7 @@ export class LabelBox extends ViewBoxBaseComponent<FieldViewProps>() { if (anchor) { if (!addAsAnnotation) anchor.backgroundColor = 'transparent'; // addAsAnnotation && this.addDocument(anchor); - PresBox.pinDocView(anchor, { pinDocLayout: pinProps?.pinDocLayout, pinData: { ...(pinProps?.pinData ?? {}) } }, this.Document); + PinDocView(anchor, { pinDocLayout: pinProps?.pinDocLayout, pinData: { ...(pinProps?.pinData ?? {}) } }, this.Document); return anchor; } return anchor; @@ -131,7 +134,10 @@ export class LabelBox extends ViewBoxBaseComponent<FieldViewProps>() { }; this._timeout = undefined; if (!r) return params; - if (!r.offsetHeight || !r.offsetWidth) return (this._timeout = setTimeout(() => this.fitTextToBox(r))); + if (!r.offsetHeight || !r.offsetWidth) { + this._timeout = setTimeout(() => this.fitTextToBox(r)); + return this._timeout; + } const parent = r.parentNode; const parentStyle = parent.style; parentStyle.display = ''; @@ -154,8 +160,13 @@ export class LabelBox extends ViewBoxBaseComponent<FieldViewProps>() { return ( <div className="labelBox-outerDiv" - onMouseLeave={action(() => (this._mouseOver = false))} - onMouseOver={action(() => (this._mouseOver = true))} + onMouseLeave={action(() => { + this._mouseOver = false; + })} + // eslint-disable-next-line jsx-a11y/mouse-events-have-key-events + onMouseOver={action(() => { + this._mouseOver = true; + })} ref={this.createDropTarget} onContextMenu={this.specificContextMenu} style={{ boxShadow: this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.BoxShadow) }}> @@ -193,3 +204,12 @@ export class LabelBox extends ViewBoxBaseComponent<FieldViewProps>() { ); } } + +Docs.Prototypes.TemplateMap.set(DocumentType.LABEL, { + layout: { view: LabelBox, dataField: 'title' }, + options: { acl: '', _singleLine: true, _layout_nativeDimEditable: true, _layout_reflowHorizontal: true, _layout_reflowVertical: true }, +}); +Docs.Prototypes.TemplateMap.set(DocumentType.BUTTON, { + layout: { view: LabelBox, dataField: 'title' }, + options: { acl: '', _layout_nativeDimEditable: true, _layout_reflowHorizontal: true, _layout_reflowVertical: true }, +}); diff --git a/src/client/views/nodes/LinkAnchorBox.scss b/src/client/views/nodes/LinkAnchorBox.scss deleted file mode 100644 index caff369df..000000000 --- a/src/client/views/nodes/LinkAnchorBox.scss +++ /dev/null @@ -1,34 +0,0 @@ -.linkAnchorBox-cont, -.linkAnchorBox-cont-small { - cursor: default; - position: absolute; - width: 15; - height: 15; - border-radius: 20px; - user-select: none; - pointer-events: all; - - .linkAnchorBox-linkCloser { - position: absolute; - width: 18; - height: 18; - background: rgb(219, 21, 21); - top: -1px; - left: -1px; - border-radius: 5px; - display: flex; - justify-content: center; - align-items: center; - padding-left: 2px; - padding-top: 1px; - } - .linkAnchorBox-button { - position: relative; - display: inline-block; - } -} - -.linkAnchorBox-cont-small { - width: 5px; - height: 5px; -}
\ No newline at end of file diff --git a/src/client/views/nodes/LinkAnchorBox.tsx b/src/client/views/nodes/LinkAnchorBox.tsx deleted file mode 100644 index ff1e62885..000000000 --- a/src/client/views/nodes/LinkAnchorBox.tsx +++ /dev/null @@ -1,115 +0,0 @@ -import { action, computed, makeObservable } from 'mobx'; -import { observer } from 'mobx-react'; -import * as React from 'react'; -import { Utils, emptyFunction, setupMoveUpEvents } from '../../../Utils'; -import { Doc } from '../../../fields/Doc'; -import { NumCast, StrCast } from '../../../fields/Types'; -import { TraceMobx } from '../../../fields/util'; -import { DragManager, dropActionType } from '../../util/DragManager'; -import { LinkFollower } from '../../util/LinkFollower'; -import { SelectionManager } from '../../util/SelectionManager'; -import { ViewBoxBaseComponent } from '../DocComponent'; -import { StyleProp } from '../StyleProvider'; -import { FieldView, FieldViewProps } from './FieldView'; -import './LinkAnchorBox.scss'; -import { LinkInfo } from './LinkDocPreview'; -const { MEDIUM_GRAY } = require('../global/globalCssVariables.module.scss'); // prettier-ignore -@observer -export class LinkAnchorBox extends ViewBoxBaseComponent<FieldViewProps>() { - public static LayoutString(fieldKey: string) { - return FieldView.LayoutString(LinkAnchorBox, fieldKey); - } - _doubleTap = false; - _lastTap: number = 0; - _ref = React.createRef<HTMLDivElement>(); - _isOpen = false; - _timeout: NodeJS.Timeout | undefined; - - constructor(props: FieldViewProps) { - super(props); - makeObservable(this); - } - - componentDidMount() { - this._props.setContentViewBox?.(this); - } - - @computed get linkSource() { - return this.DocumentView?.().containerViewPath?.().lastElement().Document; // this._props.styleProvider?.(this.dataDoc, this._props, StyleProp.LinkSource); - } - - onPointerDown = (e: React.PointerEvent) => { - const linkSource = this.linkSource; - linkSource && - setupMoveUpEvents(this, e, this.onPointerMove, emptyFunction, (e, doubleTap) => { - if (doubleTap) LinkFollower.FollowLink(this.Document, linkSource, false); - else this._props.select(false); - }); - }; - onPointerMove = action((e: PointerEvent, down: number[], delta: number[]) => { - const cdiv = this._ref?.current?.parentElement; - if (!this._isOpen && cdiv) { - const bounds = cdiv.getBoundingClientRect(); - const pt = Utils.getNearestPointInPerimeter(bounds.left, bounds.top, bounds.width, bounds.height, e.clientX, e.clientY); - const separation = Math.sqrt((pt[0] - e.clientX) * (pt[0] - e.clientX) + (pt[1] - e.clientY) * (pt[1] - e.clientY)); - if (separation > 100) { - const dragData = new DragManager.DocumentDragData([this.Document]); - dragData.dropAction = dropActionType.embed; - dragData.dropPropertiesToRemove = ['link_anchor_1_x', 'link_anchor_1_y', 'link_anchor_2_x', 'link_anchor_2_y', 'onClick']; - DragManager.StartDocumentDrag([this._ref.current!], dragData, pt[0], pt[1]); - return true; - } else { - this.layoutDoc[this.fieldKey + '_x'] = ((pt[0] - bounds.left) / bounds.width) * 100; - this.layoutDoc[this.fieldKey + '_y'] = ((pt[1] - bounds.top) / bounds.height) * 100; - this.layoutDoc.link_autoMoveAnchors = false; - } - } - return false; - }); - - specificContextMenu = (e: React.MouseEvent): void => {}; - - render() { - TraceMobx(); - const small = this._props.PanelWidth() <= 1; // this happens when rendered in a treeView - const x = NumCast(this.layoutDoc[this.fieldKey + '_x'], 100); - const y = NumCast(this.layoutDoc[this.fieldKey + '_y'], 100); - const background = this._props.styleProvider?.(this.dataDoc, this._props, StyleProp.BackgroundColor + ':anchor'); - const anchor = this.fieldKey === 'link_anchor_1' ? 'link_anchor_2' : 'link_anchor_1'; - const anchorScale = !this.dataDoc[this.fieldKey + '_useSmallAnchor'] && (x === 0 || x === 100 || y === 0 || y === 100) ? 1 : 0.25; - const targetTitle = StrCast((this.dataDoc[anchor] as Doc)?.title); - const selView = SelectionManager.Views.lastElement()?._props.LayoutTemplateString?.includes('link_anchor_1') - ? 'link_anchor_1' - : SelectionManager.Views.lastElement()?._props.LayoutTemplateString?.includes('link_anchor_2') - ? 'link_anchor_2' - : ''; - return ( - <div - ref={this._ref} - title={targetTitle} - className={`linkAnchorBox-cont${small ? '-small' : ''}`} - onPointerEnter={e => - LinkInfo.SetLinkInfo({ - DocumentView: this.DocumentView, - styleProvider: this._props.styleProvider, - linkSrc: this.linkSource, - linkDoc: this.Document, - showHeader: true, - location: [e.clientX, e.clientY + 20], - noPreview: false, - }) - } - onPointerDown={this.onPointerDown} - onContextMenu={this.specificContextMenu} - style={{ - border: selView && this.dataDoc[selView] === this.dataDoc[this.fieldKey] ? `solid ${MEDIUM_GRAY} 2px` : undefined, - background, - left: `calc(${x}% - ${small ? 2.5 : 7.5}px)`, - top: `calc(${y}% - ${small ? 2.5 : 7.5}px)`, - transform: `scale(${anchorScale})`, - cursor: 'grab', - }} - /> - ); - } -} diff --git a/src/client/views/nodes/LinkBox.tsx b/src/client/views/nodes/LinkBox.tsx index 3a2509c3d..6caa38a7f 100644 --- a/src/client/views/nodes/LinkBox.tsx +++ b/src/client/views/nodes/LinkBox.tsx @@ -1,19 +1,23 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ import { action, computed, IReactionDisposer, makeObservable, observable, reaction } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; import Xarrow from 'react-xarrows'; +import { DashColor, lightOrDark, returnFalse } from '../../../ClientUtils'; import { FieldResult } from '../../../fields/Doc'; import { DocCss, DocData } from '../../../fields/DocSymbols'; import { Id } from '../../../fields/FieldSymbols'; +import { List } from '../../../fields/List'; import { DocCast, NumCast, StrCast } from '../../../fields/Types'; import { TraceMobx } from '../../../fields/util'; -import { DashColor, emptyFunction, lightOrDark, returnFalse } from '../../../Utils'; -import { DocumentManager } from '../../util/DocumentManager'; +import { emptyFunction } from '../../../Utils'; +import { Docs } from '../../documents/Documents'; +import { DocumentType } from '../../documents/DocumentTypes'; import { SnappingManager } from '../../util/SnappingManager'; import { ViewBoxBaseComponent } from '../DocComponent'; import { EditableView } from '../EditableView'; import { LightboxView } from '../LightboxView'; -import { StyleProp } from '../StyleProvider'; +import { StyleProp } from '../StyleProp'; import { ComparisonBox } from './ComparisonBox'; import { DocumentView } from './DocumentView'; import { FieldView, FieldViewProps } from './FieldView'; @@ -38,7 +42,7 @@ export class LinkBox extends ViewBoxBaseComponent<FieldViewProps>() { anchor = (which: number) => { const anch = DocCast(this.dataDoc['link_anchor_' + which]); const anchor = anch?.layout_unrendered ? DocCast(anch.annotationOn) : anch; - return DocumentManager.Instance.getDocumentView(anchor, this.DocumentView?.().containerViewPath?.().lastElement()); + return DocumentView.getDocumentView(anchor, this.DocumentView?.().containerViewPath?.().lastElement()); }; _hackToSeeIfDeleted: any; componentWillUnmount() { @@ -49,15 +53,19 @@ export class LinkBox extends ViewBoxBaseComponent<FieldViewProps>() { this._props.setContentViewBox?.(this); this._disposers.deleting = reaction( () => !this.anchor1 && !this.anchor2 && this.DocumentView?.() && (!LightboxView.LightboxDoc || LightboxView.Contains(this.DocumentView!())), - empty => empty && ((this._hackToSeeIfDeleted = setTimeout(() => - (!this.anchor1 && !this.anchor2) && this._props.removeDocument?.(this.Document) - )), 1000) // prettier-ignore + empty => { + if (empty) { + this._hackToSeeIfDeleted = setTimeout(() => { + !this.anchor1 && !this.anchor2 && this._props.removeDocument?.(this.Document); + }, 1000); + } + } ); this._disposers.dragging = reaction( () => SnappingManager.IsDragging, () => setTimeout( action(() => {// need to wait for drag manager to set 'hidden' flag on dragged DOM elements - const a = this.anchor1, - b = this.anchor2; + const a = this.anchor1; + const b = this.anchor2; let a1 = a && document.getElementById(a.ViewGuid); let a2 = b && document.getElementById(b.ViewGuid); // test whether the anchors themselves are hidden,... @@ -66,7 +74,7 @@ export class LinkBox extends ViewBoxBaseComponent<FieldViewProps>() { // .. or whether any of their DOM parents are hidden for (; a1 && !a1.hidden; a1 = a1.parentElement); for (; a2 && !a2.hidden; a2 = a2.parentElement); - this._hide = a1 || a2 ? true : false; + this._hide = !!(a1 || a2); } })) // prettier-ignore ); @@ -91,24 +99,25 @@ export class LinkBox extends ViewBoxBaseComponent<FieldViewProps>() { a.Document[DocCss]; b.Document[DocCss]; + // eslint-disable-next-line @typescript-eslint/no-unused-vars const axf = a.screenToViewTransform(); // these force re-render when a or b moves (so do NOT remove) const bxf = b.screenToViewTransform(); const scale = docView?.screenToViewTransform().Scale ?? 1; const at = a.getBounds?.transition; // these force re-render when a or b change size and at the end of an animated transition const bt = b.getBounds?.transition; // inquring getBounds() also causes text anchors to update whether or not they reflow (any size change triggers an invalidation) - var foundParent = false; + let foundParent = false; const getAnchor = (field: FieldResult): Element[] => { const docField = DocCast(field); const doc = docField?.layout_unrendered ? DocCast(docField.annotationOn, docField) : docField; const ele = document.getElementById(DocumentView.UniquifyId(LightboxView.Contains(this.DocumentView?.()), doc[Id])); if (ele?.className === 'linkBox-label') foundParent = true; if (ele?.getBoundingClientRect().width) return [ele]; - const eles = Array.from(document.getElementsByClassName(doc[Id])).filter(ele => ele?.getBoundingClientRect().width); + const eles = Array.from(document.getElementsByClassName(doc[Id])).filter(el => el?.getBoundingClientRect().width); const annoOn = DocCast(doc.annotationOn); if (eles.length || !annoOn) return eles; const pareles = getAnchor(annoOn); - foundParent = pareles.length ? true : false; + foundParent = !!pareles.length; return pareles; }; // if there's an element in the DOM with a classname containing a link anchor's id (eg a hypertext <a>), @@ -121,26 +130,38 @@ export class LinkBox extends ViewBoxBaseComponent<FieldViewProps>() { const aid = targetAhyperlinks?.find(alink => container?.contains(alink))?.id ?? targetAhyperlinks?.lastElement()?.id; const bid = targetBhyperlinks?.find(blink => container?.contains(blink))?.id ?? targetBhyperlinks?.lastElement()?.id; if (!aid || !bid) { - setTimeout(action(() => (this._forceAnimate = this._forceAnimate + 0.01))); + setTimeout( + action(() => { + this._forceAnimate += 0.01; + }) + ); return null; } if (foundParent) { setTimeout( - action(() => (this._forceAnimate = this._forceAnimate + 0.01)), + action(() => { + this._forceAnimate += 0.01; + }), 1 ); } - if (at || bt) setTimeout(action(() => (this._forceAnimate = this._forceAnimate + 0.01))); // this forces an update during a transition animation + if (at || bt) + setTimeout( + action(() => { + this._forceAnimate += 0.01; + }) + ); // this forces an update during a transition animation const highlight = this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.Highlighting); const highlightColor = highlight?.highlightIndex ? highlight?.highlightColor : undefined; const color = this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.Color); const fontFamily = this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.FontFamily); const fontSize = this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.FontSize); const fontColor = (c => (c !== 'transparent' ? c : undefined))(StrCast(this.layoutDoc.link_fontColor)); - const { stroke_markerScale, stroke_width, stroke_startMarker, stroke_endMarker, stroke_dash } = this.Document; + // eslint-disable-next-line camelcase + const { stroke_markerScale: strokeMarkerScale, stroke_width: strokeRawWidth, stroke_startMarker: strokeStartMarker, stroke_endMarker: strokeEndMarker, stroke_dash: strokeDash } = this.Document; - const strokeWidth = NumCast(stroke_width, 4); + const strokeWidth = NumCast(strokeRawWidth, 4); const linkDesc = StrCast(this.dataDoc.link_description) || ' '; const labelText = linkDesc.substring(0, 50) + (linkDesc.length > 50 ? '...' : ''); return ( @@ -151,12 +172,12 @@ export class LinkBox extends ViewBoxBaseComponent<FieldViewProps>() { start={aid} end={bid} // strokeWidth={strokeWidth + Math.max(2, strokeWidth * 0.1)} - showHead={stroke_startMarker ? true : false} - showTail={stroke_endMarker ? true : false} - headSize={NumCast(stroke_markerScale, 3)} - tailSize={NumCast(stroke_markerScale, 3)} - tailShape={stroke_endMarker === 'dot' ? 'circle' : 'arrow1'} - headShape={stroke_startMarker === 'dot' ? 'circle' : 'arrow1'} + showHead={!!strokeStartMarker} + showTail={!!strokeEndMarker} + headSize={NumCast(strokeMarkerScale, 3)} + tailSize={NumCast(strokeMarkerScale, 3)} + tailShape={strokeEndMarker === 'dot' ? 'circle' : 'arrow1'} + headShape={strokeStartMarker === 'dot' ? 'circle' : 'arrow1'} color={highlightColor} /> )} @@ -165,23 +186,23 @@ export class LinkBox extends ViewBoxBaseComponent<FieldViewProps>() { start={aid} end={bid} // strokeWidth={strokeWidth} - dashness={Number(stroke_dash) ? true : false} - showHead={stroke_startMarker ? true : false} - showTail={stroke_endMarker ? true : false} - headSize={NumCast(stroke_markerScale, 3)} - tailSize={NumCast(stroke_markerScale, 3)} - tailShape={stroke_endMarker === 'dot' ? 'circle' : 'arrow1'} - headShape={stroke_startMarker === 'dot' ? 'circle' : 'arrow1'} + dashness={!!Number(strokeDash)} + showHead={!!strokeStartMarker} + showTail={!!strokeEndMarker} + headSize={NumCast(strokeMarkerScale, 3)} + tailSize={NumCast(strokeMarkerScale, 3)} + tailShape={strokeEndMarker === 'dot' ? 'circle' : 'arrow1'} + headShape={strokeStartMarker === 'dot' ? 'circle' : 'arrow1'} color={color} labels={ <div id={this.DocumentView?.().DocUniqueId} - className={'linkBox-label'} + className="linkBox-label" style={{ borderRadius: '8px', pointerEvents: this._props.isDocumentActive?.() ? 'all' : undefined, fontSize, - fontFamily /*, fontStyle: 'italic'*/, + fontFamily /* , fontStyle: 'italic' */, color: fontColor || lightOrDark(DashColor(color).fade(0.5).toString()), paddingLeft: 4, paddingRight: 4, @@ -222,16 +243,19 @@ export class LinkBox extends ViewBoxBaseComponent<FieldViewProps>() { } setTimeout( - action(() => (this._forceAnimate = this._forceAnimate + 1)), + action(() => { + this._forceAnimate += 1; + }), 2 ); return ( <div className={`linkBox-container${this._props.isContentActive() ? '-interactive' : ''}`} style={{ background: this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.BackgroundColor) }}> <ComparisonBox + // eslint-disable-next-line react/jsx-props-no-spreading {...this.props} // fieldKey="link_anchor" setHeight={emptyFunction} - dontRegisterView={true} + dontRegisterView renderDepth={this._props.renderDepth + 1} addDocument={returnFalse} removeDocument={returnFalse} @@ -241,3 +265,18 @@ export class LinkBox extends ViewBoxBaseComponent<FieldViewProps>() { ); } } + +Docs.Prototypes.TemplateMap.set(DocumentType.LINK, { + layout: { view: LinkBox, dataField: 'link' }, + options: { + acl: '', + childDontRegisterViews: true, + layout_hideLinkAnchors: true, + _height: 1, + _width: 1, + link: '', + link_description: '', + color: 'lightBlue', // lightblue is default color for linking dot and link documents text comment area + _dropPropertiesToRemove: new List(['onClick']), + }, +}); diff --git a/src/client/views/nodes/LinkDescriptionPopup.tsx b/src/client/views/nodes/LinkDescriptionPopup.tsx index 2a96ce458..ff95f8547 100644 --- a/src/client/views/nodes/LinkDescriptionPopup.tsx +++ b/src/client/views/nodes/LinkDescriptionPopup.tsx @@ -2,15 +2,17 @@ import { action, makeObservable, observable, reaction } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; import { DocData } from '../../../fields/DocSymbols'; +import { StrCast } from '../../../fields/Types'; import { LinkManager } from '../../util/LinkManager'; import './LinkDescriptionPopup.scss'; import { TaskCompletionBox } from './TaskCompletedBox'; -import { StrCast } from '../../../fields/Types'; @observer export class LinkDescriptionPopup extends React.Component<{}> { + // eslint-disable-next-line no-use-before-define public static Instance: LinkDescriptionPopup; @observable public display: boolean = false; + // eslint-disable-next-line react/no-unused-class-component-methods @observable public showDescriptions: string = 'ON'; @observable public popupX: number = 700; @observable public popupY: number = 350; @@ -23,6 +25,20 @@ export class LinkDescriptionPopup extends React.Component<{}> { LinkDescriptionPopup.Instance = this; } + componentDidMount() { + document.addEventListener('pointerdown', this.onClick, true); + reaction( + () => this.display, + display => { + display && (this.description = StrCast(LinkManager.Instance.currentLink?.link_description)); + } + ); + } + + componentWillUnmount() { + document.removeEventListener('pointerdown', this.onClick, true); + } + @action descriptionChanged = (e: React.ChangeEvent<HTMLInputElement>) => { this.description = e.currentTarget.value; @@ -39,25 +55,13 @@ export class LinkDescriptionPopup extends React.Component<{}> { @action onClick = (e: PointerEvent) => { - if (this.popupRef && !!!this.popupRef.current?.contains(e.target as any)) { + if (this.popupRef && !this.popupRef.current?.contains(e.target as any)) { this.display = false; this.description = ''; TaskCompletionBox.taskCompleted = false; } }; - componentDidMount() { - document.addEventListener('pointerdown', this.onClick, true); - reaction( - () => this.display, - display => display && (this.description = StrCast(LinkManager.Instance.currentLink?.link_description)) - ); - } - - componentWillUnmount() { - document.removeEventListener('pointerdown', this.onClick, true); - } - render() { return !this.display ? null : ( <div @@ -78,11 +82,11 @@ export class LinkDescriptionPopup extends React.Component<{}> { onChange={e => this.descriptionChanged(e)} /> <div className="linkDescriptionPopup-btn"> - <div className="linkDescriptionPopup-btn-dismiss" onPointerDown={e => this.onDismiss(false)}> + <div className="linkDescriptionPopup-btn-dismiss" onPointerDown={() => this.onDismiss(false)}> {' '} Dismiss{' '} </div> - <div className="linkDescriptionPopup-btn-add" onPointerDown={e => this.onDismiss(true)}> + <div className="linkDescriptionPopup-btn-add" onPointerDown={() => this.onDismiss(true)}> {' '} Add{' '} </div> diff --git a/src/client/views/nodes/LinkDocPreview.tsx b/src/client/views/nodes/LinkDocPreview.tsx index c9c8f9260..508008569 100644 --- a/src/client/views/nodes/LinkDocPreview.tsx +++ b/src/client/views/nodes/LinkDocPreview.tsx @@ -4,53 +4,59 @@ import { action, computed, makeObservable, observable, runInAction } from 'mobx' import { observer } from 'mobx-react'; import * as React from 'react'; import wiki from 'wikijs'; -import { emptyFunction, returnEmptyDoclist, returnEmptyFilter, returnEmptyString, returnFalse, returnNone, setupMoveUpEvents } from '../../../Utils'; +import { returnEmptyDoclist, returnEmptyFilter, returnEmptyString, returnFalse, returnNone, setupMoveUpEvents } from '../../../ClientUtils'; +import { emptyFunction } from '../../../Utils'; import { Doc, Opt } from '../../../fields/Doc'; import { Cast, DocCast, NumCast, PromiseValue, StrCast } from '../../../fields/Types'; import { DocServer } from '../../DocServer'; import { DocumentType } from '../../documents/DocumentTypes'; import { Docs } from '../../documents/Documents'; -import { DocumentManager } from '../../util/DocumentManager'; import { DragManager } from '../../util/DragManager'; -import { LinkFollower } from '../../util/LinkFollower'; import { LinkManager } from '../../util/LinkManager'; import { SearchUtil } from '../../util/SearchUtil'; -import { SettingsManager } from '../../util/SettingsManager'; +import { SnappingManager } from '../../util/SnappingManager'; import { Transform } from '../../util/Transform'; import { ObservableReactComponent } from '../ObservableReactComponent'; -import { DocumentView, OpenWhere } from './DocumentView'; +import { DocumentView } from './DocumentView'; import { StyleProviderFuncType } from './FieldView'; import './LinkDocPreview.scss'; +import { OpenWhere } from './OpenWhere'; +interface LinkDocPreviewProps { + linkDoc?: Doc; + linkSrc?: Doc; + DocumentView?: () => DocumentView; + styleProvider?: StyleProviderFuncType; + location: number[]; + hrefs?: string[]; + showHeader?: boolean; + noPreview?: boolean; +} export class LinkInfo { + // eslint-disable-next-line no-use-before-define private static _instance: Opt<LinkInfo>; constructor() { LinkInfo._instance = this; makeObservable(this); } + // eslint-disable-next-line no-use-before-define @observable public LinkInfo: Opt<LinkDocPreviewProps> = undefined; public static get Instance() { return LinkInfo._instance ?? new LinkInfo(); } public static Clear() { - runInAction(() => LinkInfo.Instance && (LinkInfo.Instance.LinkInfo = undefined)); + runInAction(() => { + LinkInfo.Instance && (LinkInfo.Instance.LinkInfo = undefined); + }); } public static SetLinkInfo(info?: LinkDocPreviewProps) { - runInAction(() => LinkInfo.Instance && (LinkInfo.Instance.LinkInfo = info)); + runInAction(() => { + LinkInfo.Instance && (LinkInfo.Instance.LinkInfo = info); + }); } } -interface LinkDocPreviewProps { - linkDoc?: Doc; - linkSrc?: Doc; - DocumentView?: () => DocumentView; - styleProvider?: StyleProviderFuncType; - location: number[]; - hrefs?: string[]; - showHeader?: boolean; - noPreview?: boolean; -} @observer export class LinkDocPreview extends ObservableReactComponent<LinkDocPreviewProps> { _infoRef = React.createRef<HTMLDivElement>(); @@ -68,13 +74,13 @@ export class LinkDocPreview extends ObservableReactComponent<LinkDocPreviewProps @action init() { - var linkTarget = this._props.linkDoc; + let linkTarget = this._props.linkDoc; this._linkSrc = this._props.linkSrc; this._linkDoc = this._props.linkDoc; - const link_anchor_1 = DocCast(this._linkDoc?.link_anchor_1); - const link_anchor_2 = DocCast(this._linkDoc?.link_anchor_2); - if (link_anchor_1 && link_anchor_2) { - linkTarget = Doc.AreProtosEqual(link_anchor_1, this._linkSrc) || Doc.AreProtosEqual(link_anchor_1?.annotationOn as Doc, this._linkSrc) ? link_anchor_2 : link_anchor_1; + const linkAnchor1 = DocCast(this._linkDoc?.link_anchor_1); + const linkAnchor2 = DocCast(this._linkDoc?.link_anchor_2); + if (linkAnchor1 && linkAnchor2) { + linkTarget = Doc.AreProtosEqual(linkAnchor1, this._linkSrc) || Doc.AreProtosEqual(linkAnchor1?.annotationOn as Doc, this._linkSrc) ? linkAnchor2 : linkAnchor1; } if (linkTarget?.annotationOn && linkTarget?.type !== DocumentType.RTF) { linkTarget = DocCast(linkTarget.annotationOn); // want to show annotation embedContainer document if annotation is not text @@ -110,7 +116,13 @@ export class LinkDocPreview extends ObservableReactComponent<LinkDocPreviewProps if (href.startsWith('https://en.wikipedia.org/wiki/')) { wiki() .page(href.replace('https://en.wikipedia.org/wiki/', '')) - .then(page => page.summary().then(action(summary => (this._toolTipText = summary.substring(0, 500))))); + .then(page => + page.summary().then( + action(summary => { + this._toolTipText = summary.substring(0, 500); + }) + ) + ); } else { this._toolTipText = 'url => ' + href; } @@ -120,19 +132,19 @@ export class LinkDocPreview extends ObservableReactComponent<LinkDocPreviewProps const anchorDoc = anchorDocId ? PromiseValue(DocCast(DocServer.GetCachedRefField(anchorDocId) ?? DocServer.GetRefField(anchorDocId))) : undefined; anchorDoc?.then?.( action(anchor => { - if (anchor instanceof Doc && LinkManager.Links(anchor).length) { - this._linkDoc = this._linkDoc ?? LinkManager.Links(anchor)[0]; + if (anchor instanceof Doc && Doc.Links(anchor).length) { + this._linkDoc = this._linkDoc ?? Doc.Links(anchor)[0]; const automaticLink = this._linkDoc.link_relationship === LinkManager.AutoKeywords; if (automaticLink) { // automatic links specify the target in the link info, not the source const linkTarget = anchor; - this._linkSrc = LinkManager.getOppositeAnchor(this._linkDoc, linkTarget); + this._linkSrc = Doc.getOppositeAnchor(this._linkDoc, linkTarget); this._markerTargetDoc = this._targetDoc = linkTarget; } else { this._linkSrc = anchor; - const linkTarget = LinkManager.getOppositeAnchor(this._linkDoc, this._linkSrc); + const linkTarget = Doc.getOppositeAnchor(this._linkDoc, this._linkSrc); this._markerTargetDoc = linkTarget; - this._targetDoc = /*linkTarget?.type === DocumentType.MARKER &&*/ linkTarget?.annotationOn ? Cast(linkTarget.annotationOn, Doc, null) ?? linkTarget : linkTarget; + this._targetDoc = /* linkTarget?.type === DocumentType.MARKER && */ linkTarget?.annotationOn ? Cast(linkTarget.annotationOn, Doc, null) ?? linkTarget : linkTarget; } if (LinkInfo.Instance?.LinkInfo?.noPreview || this._linkSrc?.followLinkToggle || this._markerTargetDoc?.type === DocumentType.PRES) this.followLink(); } @@ -155,8 +167,8 @@ export class LinkDocPreview extends ObservableReactComponent<LinkDocPreviewProps LinkManager.Instance.currentLink = this._linkDoc; LinkManager.Instance.currentLinkAnchor = this._linkSrc; this._props.DocumentView?.().select(false); - if ((SettingsManager.Instance.propertiesWidth ?? 0) < 100) { - SettingsManager.Instance.propertiesWidth = 250; + if ((SnappingManager.PropertiesWidth ?? 0) < 100) { + SnappingManager.SetPropertiesWidth(250); } }) ); @@ -182,18 +194,18 @@ export class LinkDocPreview extends ObservableReactComponent<LinkDocPreviewProps followLink = () => { LinkInfo.Clear(); if (this._linkDoc && this._linkSrc) { - LinkFollower.FollowLink(this._linkDoc, this._linkSrc, false); + DocumentView.FollowLink(this._linkDoc, this._linkSrc, false); } else if (this._props.hrefs?.length) { const webDoc = Array.from(SearchUtil.SearchCollection(Doc.MyFilesystem, this._props.hrefs[0], false).keys()) .filter(doc => doc.type === DocumentType.WEB) .lastElement() ?? Docs.Create.WebDocument(this._props.hrefs[0], { title: this._props.hrefs[0], _nativeWidth: 850, _width: 200, _height: 400, data_useCors: true }); - DocumentManager.Instance.showDocument(webDoc, { + DocumentView.showDocument(webDoc, { openLocation: OpenWhere.lightbox, willPan: true, zoomTime: 500, }); - //this._props.docProps?.addDocTab(webDoc, OpenWhere.lightbox); + // this._props.docProps?.addDocTab(webDoc, OpenWhere.lightbox); } }; @@ -215,7 +227,7 @@ export class LinkDocPreview extends ObservableReactComponent<LinkDocPreviewProps }; @computed get previewHeader() { return !this._linkDoc || !this._markerTargetDoc || !this._targetDoc || !this._linkSrc ? null : ( - <div className="linkDocPreview-info" style={{ background: SettingsManager.userBackgroundColor }}> + <div className="linkDocPreview-info" style={{ background: SnappingManager.userBackgroundColor }}> <div className="linkDocPreview-buttonBar" style={{ float: 'left' }}> <Tooltip title={<div className="dash-tooltip">Edit Link</div>} placement="top"> <div className="linkDocPreview-button" onPointerDown={this.editLink}> @@ -248,9 +260,9 @@ export class LinkDocPreview extends ObservableReactComponent<LinkDocPreviewProps setupMoveUpEvents( this, e, - (e, down, delta) => { - if (Math.abs(e.clientX - down[0]) + Math.abs(e.clientY - down[1]) > 100) { - DragManager.StartDocumentDrag([this._infoRef.current!], new DragManager.DocumentDragData([this._targetDoc!]), e.pageX, e.pageY); + (moveEv, down) => { + if (Math.abs(moveEv.clientX - down[0]) + Math.abs(moveEv.clientY - down[1]) > 100) { + DragManager.StartDocumentDrag([this._infoRef.current!], new DragManager.DocumentDragData([this._targetDoc!]), moveEv.pageX, moveEv.pageY); LinkInfo.Clear(); return true; } @@ -268,7 +280,7 @@ export class LinkDocPreview extends ObservableReactComponent<LinkDocPreviewProps ) : ( <DocumentView ref={r => { - const targetanchor = this._linkDoc && this._linkSrc && LinkManager.getOppositeAnchor(this._linkDoc, this._linkSrc); + const targetanchor = this._linkDoc && this._linkSrc && Doc.getOppositeAnchor(this._linkDoc, this._linkSrc); targetanchor && this._targetDoc !== targetanchor && r?._props.focus?.(targetanchor, {}); }} Document={this._targetDoc!} @@ -283,18 +295,18 @@ export class LinkDocPreview extends ObservableReactComponent<LinkDocPreviewProps removeDocument={returnFalse} addDocTab={returnFalse} pinToPres={returnFalse} - dontRegisterView={true} + dontRegisterView childFilters={returnEmptyFilter} childFiltersByRanges={returnEmptyFilter} searchFilterDocs={returnEmptyDoclist} renderDepth={0} - suppressSetHeight={true} + suppressSetHeight PanelWidth={this.width} PanelHeight={this.height} pointerEvents={returnNone} focus={emptyFunction} whenChildContentsActiveChanged={returnFalse} - ignoreAutoHeight={true} // need to ignore layout_autoHeight otherwise layout_autoHeight text boxes will expand beyond the preview panel size. + ignoreAutoHeight // need to ignore layout_autoHeight otherwise layout_autoHeight text boxes will expand beyond the preview panel size. NativeWidth={Doc.NativeWidth(this._targetDoc) ? () => Doc.NativeWidth(this._targetDoc) : undefined} NativeHeight={Doc.NativeHeight(this._targetDoc) ? () => Doc.NativeHeight(this._targetDoc) : undefined} /> @@ -311,7 +323,7 @@ export class LinkDocPreview extends ObservableReactComponent<LinkDocPreviewProps className="linkDocPreview" ref={this._linkDocRef} onPointerDown={this.followLinkPointerDown} - style={{ borderColor: SettingsManager.userColor, left: this._props.location[0], top: this._props.location[1], width: this.width() + borders, height: this.height() + borders + (this._props.showHeader ? 37 : 0) }}> + style={{ borderColor: SnappingManager.userColor, left: this._props.location[0], top: this._props.location[1], width: this.width() + borders, height: this.height() + borders + (this._props.showHeader ? 37 : 0) }}> {this.docPreview} </div> ); diff --git a/src/client/views/nodes/LoadingBox.tsx b/src/client/views/nodes/LoadingBox.tsx index adccc9db6..5f343bdfe 100644 --- a/src/client/views/nodes/LoadingBox.tsx +++ b/src/client/views/nodes/LoadingBox.tsx @@ -6,7 +6,8 @@ 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 { DocumentType } from '../../documents/DocumentTypes'; +import { Docs } from '../../documents/Documents'; import { ViewBoxAnnotatableComponent } from '../DocComponent'; import { FieldView, FieldViewProps } from './FieldView'; import './LoadingBox.scss'; @@ -37,30 +38,18 @@ export class LoadingBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { public static LayoutString(fieldKey: string) { return FieldView.LayoutString(LoadingBox, fieldKey); } - // removes from currently loading display - public static removeCurrentlyLoading(doc: Doc) { - if (DocumentManager.Instance.CurrentlyLoading) { - const index = DocumentManager.Instance.CurrentlyLoading.indexOf(doc); - runInAction(() => index !== -1 && DocumentManager.Instance.CurrentlyLoading.splice(index, 1)); - } - } - - // adds doc to currently loading display - public static addCurrentlyLoading(doc: Doc) { - if (DocumentManager.Instance.CurrentlyLoading.indexOf(doc) === -1) { - runInAction(() => DocumentManager.Instance.CurrentlyLoading.push(doc)); - } - } _timer: any; @observable progress = ''; componentDidMount() { - if (!DocumentManager.Instance.CurrentlyLoading?.includes(this.Document)) { + if (!Doc.CurrentlyLoading?.includes(this.Document)) { this.Document.loadingError = 'Upload interrupted, please try again'; } else { const updateFunc = async () => { const result = await Networking.QueryYoutubeProgress(StrCast(this.Document[Id])); // We use the guid of the overwriteDoc to track file uploads. - runInAction(() => (this.progress = result.progress)); + runInAction(() => { + this.progress = result.progress; + }); !this.Document.loadingError && this._timer && (this._timer = setTimeout(updateFunc, 1000)); }; this._timer = setTimeout(updateFunc, 1000); @@ -87,3 +76,7 @@ export class LoadingBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { ); } } +Docs.Prototypes.TemplateMap.set(DocumentType.LOADING, { + layout: { view: LoadingBox, dataField: '' }, + options: { acl: '', _layout_fitWidth: true, _layout_nativeDimEditable: true }, +}); diff --git a/src/client/views/nodes/MapBox/AnimationSpeedIcons.tsx b/src/client/views/nodes/MapBox/AnimationSpeedIcons.tsx index d54a175b2..f4ece627f 100644 --- a/src/client/views/nodes/MapBox/AnimationSpeedIcons.tsx +++ b/src/client/views/nodes/MapBox/AnimationSpeedIcons.tsx @@ -1,35 +1,44 @@ -import * as React from "react"; +import * as React from 'react'; export const slowSpeedIcon: JSX.Element = ( <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 435.62"> <defs> - <style type="text/css"> - {` + <style type="text/css"> + {` .fil0 { fill: black; fill-rule: nonzero; } .fil1 { fill: #FE0000; fill-rule: nonzero; } `} - </style> + </style> </defs> - <path className="fil0" d="M174.84 343.06c-7.31,-13.12 -13.03,-27.28 -16.89,-42.18 -3.76,-14.56 -5.77,-29.71 -5.77,-45.17 0,-11.94 1.19,-23.66 3.43,-35.03 2.29,-11.57 5.74,-22.83 10.2,-33.63 13.7,-33.14 37.01,-61.29 66.42,-80.96 25.38,-16.96 55.28,-27.66 87.45,-29.87l0 -30.17c0,-0.46 0.02,-0.92 0.06,-1.37l-33.7 0c-5.53,0 -10.05,-4.52 -10.05,-10.04l0 -24.59c0,-5.53 4.52,-10.05 10.05,-10.05l101.27 0c5.53,0 10.05,4.52 10.05,10.05l0 24.59c0,5.52 -4.52,10.04 -10.05,10.04l-33.69 0c0.03,0.45 0.05,0.91 0.05,1.37l0 31.03 -0.1 0c41.1,4.89 77.94,23.63 105.73,51.42 32.56,32.55 52.7,77.54 52.7,127.21 0,49.67 -20.14,94.66 -52.7,127.21 -32.55,32.55 -77.54,52.7 -127.21,52.7 -33.16,0 -64.29,-9.04 -91.05,-24.78 -27.66,-16.27 -50.59,-39.73 -66.2,-67.78zm148.42 -36.62l-80.33 0 0 -25.71 28.6 0 0 -42.57 -28.6 1.93 0 -25.71 36.95 -8.35 25.38 0 0 74.7 18 0 0 25.71zm44.34 -100.41l11.08 26.83 1.61 0 11.09 -26.83 34.86 0 -22.33 48.52 22.33 51.89 -35.67 0 -12.05 -28.92 -1.44 0 -11.89 28.92 -34.06 0 21.85 -50.93 -21.85 -49.48 36.47 0zm126.08 -74.6c6.98,-16.66 6.15,-34.13 -3.84,-45.82 -12,-14.03 -33.67,-15.64 -53.8,-5.77 21.32,14.62 40.68,31.63 57.64,51.59zm-323.17 0c-6.98,-16.66 -6.16,-34.13 3.84,-45.82 11.99,-14.03 33.67,-15.64 53.79,-5.77 -21.32,14.62 -40.68,31.63 -57.63,51.59zm15.31 162.23c3.23,12.5 8.04,24.39 14.18,35.42 13.13,23.58 32.39,43.29 55.6,56.94 22.37,13.16 48.52,20.71 76.49,20.71 41.71,0 79.47,-16.9 106.8,-44.23 27.32,-27.32 44.23,-65.08 44.23,-106.79 0,-41.71 -16.91,-79.47 -44.23,-106.8 -27.33,-27.32 -65.09,-44.23 -106.8,-44.23 -31.07,0 -59.91,9.34 -83.84,25.33 -24.74,16.54 -44.33,40.19 -55.82,67.98 -3.68,8.91 -6.56,18.35 -8.5,28.22 -1.87,9.49 -2.86,19.36 -2.86,29.5 0,13.24 1.65,25.96 4.75,37.95z"/> - <path className="fil1" d="M55.23 188.52c-7.98,0 -14.45,-6.47 -14.45,-14.44 0,-7.98 6.47,-14.45 14.45,-14.45l63.94 0c7.98,0 14.45,6.47 14.45,14.45 0,7.97 -6.47,14.44 -14.45,14.44l-63.94 0zm0.72 167.68c-7.97,0 -14.44,-6.47 -14.44,-14.45 0,-7.97 6.47,-14.45 14.44,-14.45l64.58 0c7.97,0 14.45,6.48 14.45,14.45 0,7.98 -6.48,14.45 -14.45,14.45l-64.58 0zm-41.5 -84.94c-7.98,0 -14.45,-6.47 -14.45,-14.45 0,-7.97 6.47,-14.44 14.45,-14.44l89.12 0c7.98,0 14.45,6.47 14.45,14.44 0,7.98 -6.47,14.45 -14.45,14.45l-89.12 0z"/> + <path + className="fil0" + d="M174.84 343.06c-7.31,-13.12 -13.03,-27.28 -16.89,-42.18 -3.76,-14.56 -5.77,-29.71 -5.77,-45.17 0,-11.94 1.19,-23.66 3.43,-35.03 2.29,-11.57 5.74,-22.83 10.2,-33.63 13.7,-33.14 37.01,-61.29 66.42,-80.96 25.38,-16.96 55.28,-27.66 87.45,-29.87l0 -30.17c0,-0.46 0.02,-0.92 0.06,-1.37l-33.7 0c-5.53,0 -10.05,-4.52 -10.05,-10.04l0 -24.59c0,-5.53 4.52,-10.05 10.05,-10.05l101.27 0c5.53,0 10.05,4.52 10.05,10.05l0 24.59c0,5.52 -4.52,10.04 -10.05,10.04l-33.69 0c0.03,0.45 0.05,0.91 0.05,1.37l0 31.03 -0.1 0c41.1,4.89 77.94,23.63 105.73,51.42 32.56,32.55 52.7,77.54 52.7,127.21 0,49.67 -20.14,94.66 -52.7,127.21 -32.55,32.55 -77.54,52.7 -127.21,52.7 -33.16,0 -64.29,-9.04 -91.05,-24.78 -27.66,-16.27 -50.59,-39.73 -66.2,-67.78zm148.42 -36.62l-80.33 0 0 -25.71 28.6 0 0 -42.57 -28.6 1.93 0 -25.71 36.95 -8.35 25.38 0 0 74.7 18 0 0 25.71zm44.34 -100.41l11.08 26.83 1.61 0 11.09 -26.83 34.86 0 -22.33 48.52 22.33 51.89 -35.67 0 -12.05 -28.92 -1.44 0 -11.89 28.92 -34.06 0 21.85 -50.93 -21.85 -49.48 36.47 0zm126.08 -74.6c6.98,-16.66 6.15,-34.13 -3.84,-45.82 -12,-14.03 -33.67,-15.64 -53.8,-5.77 21.32,14.62 40.68,31.63 57.64,51.59zm-323.17 0c-6.98,-16.66 -6.16,-34.13 3.84,-45.82 11.99,-14.03 33.67,-15.64 53.79,-5.77 -21.32,14.62 -40.68,31.63 -57.63,51.59zm15.31 162.23c3.23,12.5 8.04,24.39 14.18,35.42 13.13,23.58 32.39,43.29 55.6,56.94 22.37,13.16 48.52,20.71 76.49,20.71 41.71,0 79.47,-16.9 106.8,-44.23 27.32,-27.32 44.23,-65.08 44.23,-106.79 0,-41.71 -16.91,-79.47 -44.23,-106.8 -27.33,-27.32 -65.09,-44.23 -106.8,-44.23 -31.07,0 -59.91,9.34 -83.84,25.33 -24.74,16.54 -44.33,40.19 -55.82,67.98 -3.68,8.91 -6.56,18.35 -8.5,28.22 -1.87,9.49 -2.86,19.36 -2.86,29.5 0,13.24 1.65,25.96 4.75,37.95z" + /> + <path + className="fil1" + d="M55.23 188.52c-7.98,0 -14.45,-6.47 -14.45,-14.44 0,-7.98 6.47,-14.45 14.45,-14.45l63.94 0c7.98,0 14.45,6.47 14.45,14.45 0,7.97 -6.47,14.44 -14.45,14.44l-63.94 0zm0.72 167.68c-7.97,0 -14.44,-6.47 -14.44,-14.45 0,-7.97 6.47,-14.45 14.44,-14.45l64.58 0c7.97,0 14.45,6.48 14.45,14.45 0,7.98 -6.48,14.45 -14.45,14.45l-64.58 0zm-41.5 -84.94c-7.98,0 -14.45,-6.47 -14.45,-14.45 0,-7.97 6.47,-14.44 14.45,-14.44l89.12 0c7.98,0 14.45,6.47 14.45,14.44 0,7.98 -6.47,14.45 -14.45,14.45l-89.12 0z" + /> </svg> ); export const mediumSpeedIcon: JSX.Element = ( <svg xmlns="http://www.w3.org/2000/svg" id="Layer_1" data-name="Layer 1" viewBox="0 0 122.88 104.55"> - <defs><style>{`.cls-1{fill:#fe0000;}`}</style></defs> - <path d="M42,82.34a42.82,42.82,0,0,1-4.05-10.13A43.2,43.2,0,0,1,76.72,18.29V11.05c0-.11,0-.22,0-.33H68.65a2.41,2.41,0,0,1-2.41-2.41V2.41A2.41,2.41,0,0,1,68.65,0H93a2.42,2.42,0,0,1,2.42,2.41v5.9A2.42,2.42,0,0,1,93,10.72H84.87c0,.11,0,.22,0,.33V18.5h0A43.17,43.17,0,1,1,42,82.34ZM88.22,49.45l2.66,6.44h.39l2.66-6.44h8.37L96.94,61.09l5.36,12.45H93.74L90.85,66.6H90.5l-2.85,6.94H79.47l5.25-12.22L79.47,49.45ZM58.65,56.08l-1-5.75a33.58,33.58,0,0,1,9.68-1.46c1.28,0,2.35,0,3.22.11a11.77,11.77,0,0,1,2.67.58,5.41,5.41,0,0,1,2.2,1.28c1.24,1.23,1.85,3.12,1.85,5.66s-.72,4.42-2.16,5.63S70.64,64.73,66,66.3v1.08H76.89v6.16H57.11V68.72a10.73,10.73,0,0,1,.81-4.12,8.4,8.4,0,0,1,2.43-2.7,12.13,12.13,0,0,1,2.79-1.7l3.32-1.52c1-.47,1.88-.87,2.52-1.17V55.42a28.59,28.59,0,0,0-3.2-.19,30.66,30.66,0,0,0-7.13.85Zm59.83-24.54c1.68-4,1.48-8.19-.92-11-2.88-3.37-8.08-3.76-12.91-1.39a69.74,69.74,0,0,1,13.83,12.38Zm-77.56,0c-1.67-4-1.48-8.19.92-11,2.88-3.37,8.08-3.76,12.91-1.39A70,70,0,0,0,40.92,31.54ZM44.6,70.48A36,36,0,0,0,48,79a35.91,35.91,0,1,0-3.4-8.5Z"/> - <path className="cls-1" d="M13.25,45.25a3.47,3.47,0,0,1,0-6.94H28.6a3.47,3.47,0,0,1,0,6.94Z"/> - <path className="cls-1" d="M3.47,65.1a3.47,3.47,0,1,1,0-6.93H24.86a3.47,3.47,0,0,1,0,6.93Z"/> - <path className="cls-1" d="M13.43,85.49a3.47,3.47,0,1,1,0-6.94h15.5a3.47,3.47,0,0,1,0,6.94Z"/> + <defs> + <style>{`.cls-1{fill:#fe0000;}`}</style> + </defs> + <path d="M42,82.34a42.82,42.82,0,0,1-4.05-10.13A43.2,43.2,0,0,1,76.72,18.29V11.05c0-.11,0-.22,0-.33H68.65a2.41,2.41,0,0,1-2.41-2.41V2.41A2.41,2.41,0,0,1,68.65,0H93a2.42,2.42,0,0,1,2.42,2.41v5.9A2.42,2.42,0,0,1,93,10.72H84.87c0,.11,0,.22,0,.33V18.5h0A43.17,43.17,0,1,1,42,82.34ZM88.22,49.45l2.66,6.44h.39l2.66-6.44h8.37L96.94,61.09l5.36,12.45H93.74L90.85,66.6H90.5l-2.85,6.94H79.47l5.25-12.22L79.47,49.45ZM58.65,56.08l-1-5.75a33.58,33.58,0,0,1,9.68-1.46c1.28,0,2.35,0,3.22.11a11.77,11.77,0,0,1,2.67.58,5.41,5.41,0,0,1,2.2,1.28c1.24,1.23,1.85,3.12,1.85,5.66s-.72,4.42-2.16,5.63S70.64,64.73,66,66.3v1.08H76.89v6.16H57.11V68.72a10.73,10.73,0,0,1,.81-4.12,8.4,8.4,0,0,1,2.43-2.7,12.13,12.13,0,0,1,2.79-1.7l3.32-1.52c1-.47,1.88-.87,2.52-1.17V55.42a28.59,28.59,0,0,0-3.2-.19,30.66,30.66,0,0,0-7.13.85Zm59.83-24.54c1.68-4,1.48-8.19-.92-11-2.88-3.37-8.08-3.76-12.91-1.39a69.74,69.74,0,0,1,13.83,12.38Zm-77.56,0c-1.67-4-1.48-8.19.92-11,2.88-3.37,8.08-3.76,12.91-1.39A70,70,0,0,0,40.92,31.54ZM44.6,70.48A36,36,0,0,0,48,79a35.91,35.91,0,1,0-3.4-8.5Z" /> + <path className="cls-1" d="M13.25,45.25a3.47,3.47,0,0,1,0-6.94H28.6a3.47,3.47,0,0,1,0,6.94Z" /> + <path className="cls-1" d="M3.47,65.1a3.47,3.47,0,1,1,0-6.93H24.86a3.47,3.47,0,0,1,0,6.93Z" /> + <path className="cls-1" d="M13.43,85.49a3.47,3.47,0,1,1,0-6.94h15.5a3.47,3.47,0,0,1,0,6.94Z" /> </svg> ); export const fastSpeedIcon: JSX.Element = ( <svg xmlns="http://www.w3.org/2000/svg" id="Layer_1" data-name="Layer 1" viewBox="0 0 122.88 104.55"> - <defs><style>{`.cls-1{fill:#fe0000;`}</style></defs> - <path d="M42,82.34a42.82,42.82,0,0,1-4.05-10.13A43.2,43.2,0,0,1,76.72,18.29V11.05c0-.11,0-.22,0-.33H68.65a2.41,2.41,0,0,1-2.41-2.41V2.41A2.41,2.41,0,0,1,68.65,0H93a2.42,2.42,0,0,1,2.42,2.41v5.9A2.42,2.42,0,0,1,93,10.72H84.87c0,.11,0,.22,0,.33V18.5h0A43.17,43.17,0,1,1,42,82.34ZM88.22,49.61l2.66,6.44h.39l2.66-6.44h8.37L96.94,61.26l5.36,12.45H93.74l-2.9-6.94H90.5l-2.86,6.94H79.47l5.24-12.22L79.47,49.61Zm-19,8.48v-2.5a24.92,24.92,0,0,0-3.74-.2A33.25,33.25,0,0,0,59,56.2l-1-5.7A30.47,30.47,0,0,1,67.13,49a22.86,22.86,0,0,1,5.48.47,6.91,6.91,0,0,1,2.5,1.11,5.62,5.62,0,0,1,1.78,4.55,5.84,5.84,0,0,1-3.2,5.56v.19a5.73,5.73,0,0,1,3.81,5.74,8.67,8.67,0,0,1-.63,3.49,6,6,0,0,1-1.6,2.24,7.15,7.15,0,0,1-2.55,1.25,25.64,25.64,0,0,1-6.61.66,37.78,37.78,0,0,1-8.54-1l1.08-6.37a27.22,27.22,0,0,0,6.21.89,35.79,35.79,0,0,0,4.35-.23V65.11l-6.63-.65V58.87l6.63-.78Zm49.27-26.55c1.68-4,1.48-8.19-.92-11-2.88-3.37-8.08-3.76-12.91-1.39a69.74,69.74,0,0,1,13.83,12.38Zm-77.56,0c-1.67-4-1.48-8.19.92-11,2.88-3.37,8.08-3.76,12.91-1.39A70,70,0,0,0,40.92,31.54ZM44.6,70.48A36,36,0,0,0,48,79a35.91,35.91,0,1,0-3.4-8.5Z"/> - <path className="cls-1" d="M13.25,45.25a3.47,3.47,0,0,1,0-6.94H28.6a3.47,3.47,0,0,1,0,6.94Zm.18,40.24a3.47,3.47,0,1,1,0-6.94h15.5a3.47,3.47,0,0,1,0,6.94ZM3.47,65.1a3.47,3.47,0,1,1,0-6.93H24.86a3.47,3.47,0,0,1,0,6.93Z"/> + <defs> + <style>{`.cls-1{fill:#fe0000;`}</style> + </defs> + <path d="M42,82.34a42.82,42.82,0,0,1-4.05-10.13A43.2,43.2,0,0,1,76.72,18.29V11.05c0-.11,0-.22,0-.33H68.65a2.41,2.41,0,0,1-2.41-2.41V2.41A2.41,2.41,0,0,1,68.65,0H93a2.42,2.42,0,0,1,2.42,2.41v5.9A2.42,2.42,0,0,1,93,10.72H84.87c0,.11,0,.22,0,.33V18.5h0A43.17,43.17,0,1,1,42,82.34ZM88.22,49.61l2.66,6.44h.39l2.66-6.44h8.37L96.94,61.26l5.36,12.45H93.74l-2.9-6.94H90.5l-2.86,6.94H79.47l5.24-12.22L79.47,49.61Zm-19,8.48v-2.5a24.92,24.92,0,0,0-3.74-.2A33.25,33.25,0,0,0,59,56.2l-1-5.7A30.47,30.47,0,0,1,67.13,49a22.86,22.86,0,0,1,5.48.47,6.91,6.91,0,0,1,2.5,1.11,5.62,5.62,0,0,1,1.78,4.55,5.84,5.84,0,0,1-3.2,5.56v.19a5.73,5.73,0,0,1,3.81,5.74,8.67,8.67,0,0,1-.63,3.49,6,6,0,0,1-1.6,2.24,7.15,7.15,0,0,1-2.55,1.25,25.64,25.64,0,0,1-6.61.66,37.78,37.78,0,0,1-8.54-1l1.08-6.37a27.22,27.22,0,0,0,6.21.89,35.79,35.79,0,0,0,4.35-.23V65.11l-6.63-.65V58.87l6.63-.78Zm49.27-26.55c1.68-4,1.48-8.19-.92-11-2.88-3.37-8.08-3.76-12.91-1.39a69.74,69.74,0,0,1,13.83,12.38Zm-77.56,0c-1.67-4-1.48-8.19.92-11,2.88-3.37,8.08-3.76,12.91-1.39A70,70,0,0,0,40.92,31.54ZM44.6,70.48A36,36,0,0,0,48,79a35.91,35.91,0,1,0-3.4-8.5Z" /> + <path className="cls-1" d="M13.25,45.25a3.47,3.47,0,0,1,0-6.94H28.6a3.47,3.47,0,0,1,0,6.94Zm.18,40.24a3.47,3.47,0,1,1,0-6.94h15.5a3.47,3.47,0,0,1,0,6.94ZM3.47,65.1a3.47,3.47,0,1,1,0-6.93H24.86a3.47,3.47,0,0,1,0,6.93Z" /> </svg> ); - diff --git a/src/client/views/nodes/MapBox/AnimationUtility.ts b/src/client/views/nodes/MapBox/AnimationUtility.ts index 35153f439..f4bae66bb 100644 --- a/src/client/views/nodes/MapBox/AnimationUtility.ts +++ b/src/client/views/nodes/MapBox/AnimationUtility.ts @@ -87,25 +87,24 @@ export class AnimationUtility { @computed get currentPitch(): number { if (!this.isStreetViewAnimation) return 50; if (!this.terrainDisplayed) return 80; - else { - // const groundElevation = 0; - const heightAboveGround = this.currentAnimationAltitude; - const horizontalDistance = 500; - - let pitch; - if (heightAboveGround >= 0) { - pitch = 90 - Math.atan(heightAboveGround / horizontalDistance) * (180 / Math.PI); - } else { - pitch = 80; - } - console.log(Math.max(50, Math.min(pitch, 85))); + // const groundElevation = 0; + const heightAboveGround = this.currentAnimationAltitude; + const horizontalDistance = 500; - if (this.previousPitch) { - return this.lerp(Math.max(50, Math.min(pitch, 85)), this.previousPitch, 0.02); - } - return Math.max(50, Math.min(pitch, 85)); + let pitch; + if (heightAboveGround >= 0) { + pitch = 90 - Math.atan(heightAboveGround / horizontalDistance) * (180 / Math.PI); + } else { + pitch = 80; + } + + console.log(Math.max(50, Math.min(pitch, 85))); + + if (this.previousPitch) { + return this.lerp(Math.max(50, Math.min(pitch, 85)), this.previousPitch, 0.02); } + return Math.max(50, Math.min(pitch, 85)); } @computed get flyInEndPitch() { @@ -214,8 +213,9 @@ export class AnimationUtility { currentAnimationPhase: number; updateAnimationPhase: (newAnimationPhase: number) => void; updateFrameId: (newFrameId: number) => void; - }) => { - return new Promise<void>(async resolve => { + }) => + // eslint-disable-next-line no-async-promise-executor + new Promise<void>(async resolve => { let startTime: number | null = null; const frame = async (currentTime: number) => { @@ -257,7 +257,7 @@ export class AnimationUtility { updateAnimationPhase(animationPhase); // compute corrected camera ground position, so that he leading edge of the path is in view - var correctedPosition = this.computeCameraPosition( + const correctedPosition = this.computeCameraPosition( this.isStreetViewAnimation, this.currentPitch, bearing, @@ -277,7 +277,7 @@ export class AnimationUtility { map.setFreeCameraOptions(camera); this.previousAltitude = this.currentAnimationAltitude; - this.previousPitch = this.previousPitch; + // this.previousPitch = this.previousPitch; // repeat! const innerFrameId = await window.requestAnimationFrame(frame); @@ -287,15 +287,15 @@ export class AnimationUtility { const outerFrameId = await window.requestAnimationFrame(frame); updateFrameId(outerFrameId); }); - }; - public flyInAndRotate = async ({ map, updateFrameId }: { map: MapRef; updateFrameId: (newFrameId: number) => void }) => { - return new Promise<{ bearing: number; altitude: number }>(async resolve => { + public flyInAndRotate = async ({ map, updateFrameId }: { map: MapRef; updateFrameId: (newFrameId: number) => void }) => + // eslint-disable-next-line no-async-promise-executor + new Promise<{ bearing: number; altitude: number }>(async resolve => { let start: number | null; - var currentAltitude; - var currentBearing; - var currentPitch; + let currentAltitude; + let currentBearing; + let currentPitch; // the animation frame will run as many times as necessary until the duration has been reached const frame = async (time: number) => { @@ -319,7 +319,7 @@ export class AnimationUtility { currentPitch = this.FLY_IN_START_PITCH + (this.flyInEndPitch - this.FLY_IN_START_PITCH) * d3.easeCubicOut(animationPhase); // compute corrected camera ground position, so the start of the path is always in view - var correctedPosition = this.computeCameraPosition(false, currentPitch, currentBearing, this.FIRST_LNG_LAT, currentAltitude); + const correctedPosition = this.computeCameraPosition(false, currentPitch, currentBearing, this.FIRST_LNG_LAT, currentAltitude); // set the pitch and bearing of the camera const camera = map.getFreeCameraOptions(); @@ -349,13 +349,10 @@ export class AnimationUtility { const outerFrameId = await window.requestAnimationFrame(frame); updateFrameId(outerFrameId); }); - }; previousCameraPosition: { lng: number; lat: number } | null = null; - lerp = (start: number, end: number, amt: number) => { - return (1 - amt) * start + amt * end; - }; + lerp = (start: number, end: number, amt: number) => (1 - amt) * start + amt * end; computeCameraPosition = (isStreetViewAnimation: boolean, pitch: number, bearing: number, targetPosition: { lng: number; lat: number }, altitude: number, smooth = false) => { const bearingInRadian = (bearing * Math.PI) / 180; diff --git a/src/client/views/nodes/MapBox/DirectionsAnchorMenu.tsx b/src/client/views/nodes/MapBox/DirectionsAnchorMenu.tsx index 7e99795b5..b8fd8ac6a 100644 --- a/src/client/views/nodes/MapBox/DirectionsAnchorMenu.tsx +++ b/src/client/views/nodes/MapBox/DirectionsAnchorMenu.tsx @@ -4,15 +4,17 @@ import { IconButton } from 'browndash-components'; import { IReactionDisposer, ObservableMap, reaction } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; -import { returnFalse, unimplementedFunction } from '../../../../Utils'; +import { returnFalse } from '../../../../ClientUtils'; +import { unimplementedFunction } from '../../../../Utils'; import { Doc, Opt } from '../../../../fields/Doc'; import { NumCast, StrCast } from '../../../../fields/Types'; -import { SelectionManager } from '../../../util/SelectionManager'; import { SettingsManager } from '../../../util/SettingsManager'; import { AntimodeMenu, AntimodeMenuProps } from '../../AntimodeMenu'; +import { DocumentView } from '../DocumentView'; @observer export class DirectionsAnchorMenu extends AntimodeMenu<AntimodeMenuProps> { + // eslint-disable-next-line no-use-before-define static Instance: DirectionsAnchorMenu; private _disposer: IReactionDisposer | undefined; @@ -23,8 +25,8 @@ export class DirectionsAnchorMenu extends AntimodeMenu<AntimodeMenuProps> { public OnClick: (e: PointerEvent) => void = unimplementedFunction; // public OnAudio: (e: PointerEvent) => void = unimplementedFunction; public StartDrag: (e: PointerEvent, ele: HTMLElement) => void = unimplementedFunction; - public Highlight: (color: string, isTargetToggler: boolean, savedAnnotations?: ObservableMap<number, HTMLDivElement[]>, addAsAnnotation?: boolean) => Opt<Doc> = (color: string, isTargetToggler: boolean) => undefined; - public GetAnchor: (savedAnnotations: Opt<ObservableMap<number, HTMLDivElement[]>>, addAsAnnotation: boolean) => Opt<Doc> = (savedAnnotations: Opt<ObservableMap<number, HTMLDivElement[]>>, addAsAnnotation: boolean) => undefined; + public Highlight: (color: string, isTargetToggler: boolean, savedAnnotations?: ObservableMap<number, HTMLDivElement[]>, addAsAnnotation?: boolean) => Opt<Doc> = () => undefined; + public GetAnchor: (savedAnnotations: Opt<ObservableMap<number, HTMLDivElement[]>>, addAsAnnotation: boolean) => Opt<Doc> = () => undefined; public Delete: () => void = unimplementedFunction; // public MakeTargetToggle: () => void = unimplementedFunction; // public ShowTargetTrail: () => void = unimplementedFunction; @@ -54,8 +56,8 @@ export class DirectionsAnchorMenu extends AntimodeMenu<AntimodeMenuProps> { componentDidMount() { this._disposer = reaction( - () => SelectionManager.Views.slice(), - sel => DirectionsAnchorMenu.Instance.fadeOut(true) + () => DocumentView.Selected().slice(), + () => DirectionsAnchorMenu.Instance.fadeOut(true) ); } // audioDown = (e: React.PointerEvent) => { @@ -103,8 +105,8 @@ export class DirectionsAnchorMenu extends AntimodeMenu<AntimodeMenuProps> { color={SettingsManager.userColor} /> - <IconButton tooltip="Animate route" onPointerDown={this.Delete} /**TODO: fix */ icon={<FontAwesomeIcon icon={faRoute as IconLookup} />} color={SettingsManager.userColor} /> - <IconButton tooltip="Add to calendar" onPointerDown={this.Delete} /**TODO: fix */ icon={<FontAwesomeIcon icon={faCalendarDays as IconLookup} />} color={SettingsManager.userColor} /> + <IconButton tooltip="Animate route" onPointerDown={this.Delete} /* *TODO: fix */ icon={<FontAwesomeIcon icon={faRoute as IconLookup} />} color={SettingsManager.userColor} /> + <IconButton tooltip="Add to calendar" onPointerDown={this.Delete} /* *TODO: fix */ icon={<FontAwesomeIcon icon={faCalendarDays as IconLookup} />} color={SettingsManager.userColor} /> </div> ); diff --git a/src/client/views/nodes/MapBox/GeocoderControl.tsx b/src/client/views/nodes/MapBox/GeocoderControl.tsx index e4ba51316..e118c57d9 100644 --- a/src/client/views/nodes/MapBox/GeocoderControl.tsx +++ b/src/client/views/nodes/MapBox/GeocoderControl.tsx @@ -3,8 +3,6 @@ // import { ControlPosition, MarkerProps, useControl } from "react-map-gl"; // import '@mapbox/mapbox-gl-geocoder/dist/mapbox-gl-geocoder.css' - - // export type GeocoderControlProps = Omit<GeocoderOptions, 'accessToken' | 'mapboxgl' | 'marker'> & { // mapboxAccessToken: string; // marker?: Omit<MarkerProps, 'longitude' | 'latitude'>; @@ -31,7 +29,6 @@ // ctrl.on('results', props.onResults); // ctrl.on('result', evt => { // props.onResult(evt); - // // const {result} = evt; // // const location = // // result && @@ -49,8 +46,6 @@ // position: props.position // } // ); - - // // @ts-ignore (TS2339) private member // if (geocoder._map) { // if (geocoder.getProximity() !== props.proximity && props.proximity !== undefined) { @@ -104,4 +99,4 @@ // onLoading: noop, // onResults: noop, // onError: noop -// };
\ No newline at end of file +// }; diff --git a/src/client/views/nodes/MapBox/MapAnchorMenu.tsx b/src/client/views/nodes/MapBox/MapAnchorMenu.tsx index 08bea5d9d..103a35434 100644 --- a/src/client/views/nodes/MapBox/MapAnchorMenu.tsx +++ b/src/client/views/nodes/MapBox/MapAnchorMenu.tsx @@ -1,3 +1,4 @@ +/* eslint-disable react/button-has-type */ import { IconLookup, faAdd, faArrowDown, faArrowLeft, faArrowsRotate, faBicycle, faCalendarDays, faCar, faDiamondTurnRight, faEdit, faPersonWalking, faRoute } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { Autocomplete, Checkbox, FormControlLabel, TextField } from '@mui/material'; @@ -7,13 +8,14 @@ import { IReactionDisposer, ObservableMap, action, makeObservable, observable, r import { observer } from 'mobx-react'; import * as React from 'react'; import { CirclePicker, ColorResult } from 'react-color'; -import { returnFalse, setupMoveUpEvents, unimplementedFunction } from '../../../../Utils'; +import { returnFalse, setupMoveUpEvents } from '../../../../ClientUtils'; +import { unimplementedFunction } from '../../../../Utils'; import { Doc, Opt } from '../../../../fields/Doc'; import { NumCast, StrCast } from '../../../../fields/Types'; import { CalendarManager } from '../../../util/CalendarManager'; -import { SelectionManager } from '../../../util/SelectionManager'; import { SettingsManager } from '../../../util/SettingsManager'; import { AntimodeMenu, AntimodeMenuProps } from '../../AntimodeMenu'; +import { DocumentView } from '../DocumentView'; import './MapAnchorMenu.scss'; import { MapboxApiUtility, TransportationType } from './MapboxApiUtility'; import { MarkerIcons } from './MarkerIcons'; @@ -23,6 +25,7 @@ type MapAnchorMenuType = 'standard' | 'routeCreation' | 'calendar' | 'customize' @observer export class MapAnchorMenu extends AntimodeMenu<AntimodeMenuProps> { + // eslint-disable-next-line no-use-before-define static Instance: MapAnchorMenu; private _disposer: IReactionDisposer | undefined; @@ -35,8 +38,8 @@ export class MapAnchorMenu extends AntimodeMenu<AntimodeMenuProps> { public OnClick: (e: PointerEvent) => void = unimplementedFunction; // public OnAudio: (e: PointerEvent) => void = unimplementedFunction; public StartDrag: (e: PointerEvent, ele: HTMLElement) => void = unimplementedFunction; - public Highlight: (color: string, isTargetToggler: boolean, savedAnnotations?: ObservableMap<number, HTMLDivElement[]>, addAsAnnotation?: boolean) => Opt<Doc> = (color: string, isTargetToggler: boolean) => undefined; - public GetAnchor: (savedAnnotations: Opt<ObservableMap<number, HTMLDivElement[]>>, addAsAnnotation: boolean) => Opt<Doc> = (savedAnnotations: Opt<ObservableMap<number, HTMLDivElement[]>>, addAsAnnotation: boolean) => undefined; + public Highlight: (color: string, isTargetToggler: boolean, savedAnnotations?: ObservableMap<number, HTMLDivElement[]>, addAsAnnotation?: boolean) => Opt<Doc> = () => undefined; + public GetAnchor: (savedAnnotations: Opt<ObservableMap<number, HTMLDivElement[]>>, addAsAnnotation: boolean) => Opt<Doc> = () => undefined; public Delete: () => void = unimplementedFunction; // public MakeTargetToggle: () => void = unimplementedFunction; // public ShowTargetTrail: () => void = unimplementedFunction; @@ -123,8 +126,8 @@ export class MapAnchorMenu extends AntimodeMenu<AntimodeMenuProps> { componentDidMount() { this._disposer = reaction( - () => SelectionManager.Views.slice(), - sel => MapAnchorMenu.Instance.fadeOut(true) + () => DocumentView.Selected().slice(), + () => MapAnchorMenu.Instance.fadeOut(true) ); } // audioDown = (e: React.PointerEvent) => { @@ -147,12 +150,12 @@ export class MapAnchorMenu extends AntimodeMenu<AntimodeMenuProps> { setupMoveUpEvents( this, e, - (e: PointerEvent) => { - this.StartDrag(e, this._commentRef.current!); + moveEv => { + this.StartDrag(moveEv, this._commentRef.current!); return true; }, returnFalse, - e => this.OnClick(e) + clickEv => this.OnClick(clickEv) ); }; @@ -274,7 +277,7 @@ export class MapAnchorMenu extends AntimodeMenu<AntimodeMenuProps> { HandleAddRouteClick = () => { if (this.currentRouteInfoMap && this.selectedTransportationType && this.selectedDestinationFeature) { - const coordinates = this.currentRouteInfoMap[this.selectedTransportationType].coordinates; + const { coordinates } = this.currentRouteInfoMap[this.selectedTransportationType]; console.log(coordinates); console.log(this.selectedDestinationFeature); this.AddNewRouteToMap(coordinates, this.title ?? '', this.selectedDestinationFeature, this.createPinForDestination); @@ -293,34 +296,30 @@ export class MapAnchorMenu extends AntimodeMenu<AntimodeMenuProps> { getDirectionsButton: JSX.Element = (<IconButton tooltip="Get directions" onPointerDown={this.DirectionsClick} icon={<FontAwesomeIcon icon={faDiamondTurnRight as IconLookup} />} color={SettingsManager.userColor} />); - getAddToCalendarButton = (docType: string): JSX.Element => { - return ( - <IconButton - tooltip="Add to calendar" - onPointerDown={() => { - CalendarManager.Instance.open(undefined, docType === 'pin' ? this.pinDoc : this.routeDoc); - }} - icon={<FontAwesomeIcon icon={faCalendarDays as IconLookup} />} - color={SettingsManager.userColor} - /> - ); - }; + getAddToCalendarButton = (docType: string): JSX.Element => ( + <IconButton + tooltip="Add to calendar" + onPointerDown={() => { + CalendarManager.Instance.open(undefined, docType === 'pin' ? this.pinDoc : this.routeDoc); + }} + icon={<FontAwesomeIcon icon={faCalendarDays as IconLookup} />} + color={SettingsManager.userColor} + /> + ); addToCalendarButton: JSX.Element = ( <IconButton tooltip="Add to calendar" onPointerDown={() => CalendarManager.Instance.open(undefined, this.pinDoc)} icon={<FontAwesomeIcon icon={faCalendarDays as IconLookup} />} color={SettingsManager.userColor} /> ); - getLinkNoteToDocButton = (docType: string): JSX.Element => { - return ( - <div ref={this._commentRef}> - <IconButton - tooltip={`Link Note to ${docType === 'pin' ? 'Pin' : 'Route'}`} // - onPointerDown={this.notePointerDown} - icon={<FontAwesomeIcon icon="sticky-note" />} - color={SettingsManager.userColor} - /> - </div> - ); - }; + getLinkNoteToDocButton = (docType: string): JSX.Element => ( + <div ref={this._commentRef}> + <IconButton + tooltip={`Link Note to ${docType === 'pin' ? 'Pin' : 'Route'}`} // + onPointerDown={this.notePointerDown} + icon={<FontAwesomeIcon icon="sticky-note" />} + color={SettingsManager.userColor} + /> + </div> + ); linkNoteToPinOrRoutenButton: JSX.Element = ( <div ref={this._commentRef}> @@ -362,16 +361,14 @@ export class MapAnchorMenu extends AntimodeMenu<AntimodeMenuProps> { /> ); - getDeleteButton = (type: string) => { - return ( - <IconButton - tooltip={`Delete ${type === 'pin' ? 'Pin' : 'Route'}`} // - onPointerDown={this.Delete} - icon={<FontAwesomeIcon icon="trash-alt" />} - color={SettingsManager.userColor} - /> - ); - }; + getDeleteButton = (type: string) => ( + <IconButton + tooltip={`Delete ${type === 'pin' ? 'Pin' : 'Route'}`} // + onPointerDown={this.Delete} + icon={<FontAwesomeIcon icon="trash-alt" />} + color={SettingsManager.userColor} + /> + ); animateRouteButton: JSX.Element = (<IconButton tooltip="Animate route" onPointerDown={() => this.OpenAnimationPanel(this.routeDoc)} icon={<FontAwesomeIcon icon={faRoute as IconLookup} />} color={SettingsManager.userColor} />); @@ -452,18 +449,17 @@ export class MapAnchorMenu extends AntimodeMenu<AntimodeMenuProps> { }} options={this.destinationFeatures.filter(feature => feature.place_name).map(feature => feature)} getOptionLabel={(feature: any) => feature.place_name} + // eslint-disable-next-line react/jsx-props-no-spreading renderInput={(params: any) => <TextField {...params} placeholder="Enter a destination" />} /> - {this.selectedDestinationFeature && ( - <> - {!this.allMapPinDocs.some(pinDoc => pinDoc.title === this.selectedDestinationFeature.place_name) && ( - <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', gap: '5px' }}> - <FormControlLabel label="Create pin for destination?" control={<Checkbox color="success" checked={this.createPinForDestination} onChange={this.toggleCreatePinForDestinationCheckbox} />} /> - </div> - )} - </> - )} - <button id="get-routes-button" disabled={this.selectedDestinationFeature ? false : true} onClick={() => this.getRoutes(this.selectedDestinationFeature)}> + {!this.selectedDestinationFeature + ? null + : !this.allMapPinDocs.some(pinDoc => pinDoc.title === this.selectedDestinationFeature.place_name) && ( + <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', gap: '5px' }}> + <FormControlLabel label="Create pin for destination?" control={<Checkbox color="success" checked={this.createPinForDestination} onChange={this.toggleCreatePinForDestinationCheckbox} />} /> + </div> + )} + <button id="get-routes-button" disabled={!this.selectedDestinationFeature} onClick={() => this.getRoutes(this.selectedDestinationFeature)}> Get routes </button> @@ -516,7 +512,7 @@ export class MapAnchorMenu extends AntimodeMenu<AntimodeMenuProps> { </div> ))} </div> - <div style={{ width: '100%', height: '3px', color: 'white' }}></div> + <div style={{ width: '100%', height: '3px', color: 'white' }} /> </div> )} {this.menuType === 'route' && this.routeDoc && <div>{StrCast(this.routeDoc.title)}</div>} diff --git a/src/client/views/nodes/MapBox/MapBox.tsx b/src/client/views/nodes/MapBox/MapBox.tsx index b73898f59..ac8010f11 100644 --- a/src/client/views/nodes/MapBox/MapBox.tsx +++ b/src/client/views/nodes/MapBox/MapBox.tsx @@ -1,3 +1,5 @@ +/* eslint-disable jsx-a11y/no-static-element-interactions */ +/* eslint-disable jsx-a11y/click-events-have-key-events */ import { IconLookup, faCircleXmark, faGear, faPause, faPlay, faRotate } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { Checkbox, FormControlLabel, TextField } from '@mui/material'; @@ -5,31 +7,30 @@ import * as turf from '@turf/turf'; import { IconButton, Size, Type } from 'browndash-components'; import * as d3 from 'd3'; import { Feature, FeatureCollection, GeoJsonProperties, Geometry, LineString, Position } from 'geojson'; -import mapboxgl, { LngLat, LngLatBoundsLike, MapLayerMouseEvent } from 'mapbox-gl'; +import mapboxgl, { LngLatBoundsLike, MapLayerMouseEvent } from 'mapbox-gl'; import { IReactionDisposer, ObservableMap, action, autorun, computed, makeObservable, observable, runInAction } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; import { CirclePicker, ColorResult } from 'react-color'; import { Layer, MapProvider, MapRef, Map as MapboxMap, Marker, Source, ViewState, ViewStateChangeEvent } from 'react-map-gl'; import { MarkerEvent } from 'react-map-gl/dist/esm/types'; -import { Utils, emptyFunction, setupMoveUpEvents } from '../../../../Utils'; +import { ClientUtils, setupMoveUpEvents } from '../../../../ClientUtils'; +import { emptyFunction } from '../../../../Utils'; import { Doc, DocListCast, Field, LinkedTo, Opt } from '../../../../fields/Doc'; -import { DocCast, NumCast, StrCast } from '../../../../fields/Types'; +import { DocCast, NumCast, StrCast, toList } from '../../../../fields/Types'; +import { DocUtils } from '../../../documents/DocUtils'; import { DocumentType } from '../../../documents/DocumentTypes'; -import { DocUtils, Docs } from '../../../documents/Documents'; -import { DocumentManager } from '../../../util/DocumentManager'; +import { Docs } from '../../../documents/Documents'; import { DragManager } from '../../../util/DragManager'; -import { LinkManager } from '../../../util/LinkManager'; -import { SnappingManager } from '../../../util/SnappingManager'; import { UndoManager, undoable } from '../../../util/UndoManager'; import { ViewBoxAnnotatableComponent, ViewBoxInterface } from '../../DocComponent'; +import { PinDocView, PinProps } from '../../PinFuncs'; import { SidebarAnnos } from '../../SidebarAnnos'; import { MarqueeOptionsMenu } from '../../collections/collectionFreeForm'; import { Colors } from '../../global/globalEnums'; import { DocumentView } from '../DocumentView'; -import { FieldView, FieldViewProps, FocusViewOptions } from '../FieldView'; -import { FormattedTextBox } from '../formattedText/FormattedTextBox'; -import { PinProps, PresBox } from '../trails'; +import { FieldView, FieldViewProps } from '../FieldView'; +import { FocusViewOptions } from '../FocusViewOptions'; import { fastSpeedIcon, mediumSpeedIcon, slowSpeedIcon } from './AnimationSpeedIcons'; import { AnimationSpeed, AnimationStatus, AnimationUtility } from './AnimationUtility'; import { MapAnchorMenu } from './MapAnchorMenu'; @@ -53,9 +54,6 @@ import { MarkerIcons } from './MarkerIcons'; */ const MAPBOX_ACCESS_TOKEN = 'pk.eyJ1IjoiemF1bHRhdmFuZ2FyIiwiYSI6ImNscHgwNDd1MDA3MXIydm92ODdianp6cGYifQ.WFAqbhwxtMHOWSPtu0l2uQ'; -const MAPBOX_FORWARD_GEOCODE_BASE_URL = 'https://api.mapbox.com/geocoding/v5/mapbox.places/'; - -const MAPBOX_REVERSE_GEOCODE_BASE_URL = 'https://api.mapbox.com/geocoding/v5/mapbox.places/'; type PopupInfo = { longitude: number; @@ -111,13 +109,13 @@ export class MapBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem }; // this list contains pushpins and configs - @computed get allAnnotations() { return DocListCast(this.dataDoc[this.annotationKey]); } //prettier-ignore - @computed get allSidebarDocs() { return DocListCast(this.dataDoc[this.SidebarKey]); } //prettier-ignore - @computed get allPushpins() { return this.allAnnotations.filter(anno => anno.type === DocumentType.PUSHPIN); } //prettier-ignore - @computed get allRoutes() { return this.allAnnotations.filter(anno => anno.type === DocumentType.MAPROUTE); } //prettier-ignore - @computed get SidebarShown() { return this.layoutDoc._layout_showSidebar ? true : false; } //prettier-ignore - @computed get sidebarWidthPercent() { return StrCast(this.layoutDoc._layout_sidebarWidthPercent, '0%'); } //prettier-ignore - @computed get SidebarKey() { return this.fieldKey + '_sidebar'; } //prettier-ignore + @computed get allAnnotations() { return DocListCast(this.dataDoc[this.annotationKey]); } // prettier-ignore + @computed get allSidebarDocs() { return DocListCast(this.dataDoc[this.SidebarKey]); } // prettier-ignore + @computed get allPushpins() { return this.allAnnotations.filter(anno => anno.type === DocumentType.PUSHPIN); } // prettier-ignore + @computed get allRoutes() { return this.allAnnotations.filter(anno => anno.type === DocumentType.MAPROUTE); } // prettier-ignore + @computed get SidebarShown() { return !!this.layoutDoc._layout_showSidebar; } // prettier-ignore + @computed get sidebarWidthPercent() { return StrCast(this.layoutDoc._layout_sidebarWidthPercent, '0%'); } // prettier-ignore + @computed get SidebarKey() { return this.fieldKey + '_sidebar'; } // prettier-ignore @computed get sidebarColor() { return StrCast(this.layoutDoc.sidebar_color, StrCast(this.layoutDoc[this._props.fieldKey + '_backgroundColor'], '#e4e4e4')); } @@ -235,14 +233,13 @@ export class MapBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem /** * Called when dragging documents into map sidebar or directly into infowindow; to create a map marker, ref to MapMarkerDocument in Documents.ts - * @param doc + * @param docs * @param sidebarKey * @returns */ - sidebarAddDocument = (doc: Doc | Doc[], sidebarKey?: string) => { + sidebarAddDocument = (docs: Doc | Doc[], sidebarKey?: string) => { if (!this.layoutDoc._layout_showSidebar) this.toggleSidebar(); - const docs = doc instanceof Doc ? [doc] : doc; - docs.forEach(doc => { + toList(docs).forEach(doc => { let existingPin = this.allPushpins.find(pin => pin.latitude === doc.latitude && pin.longitude === doc.longitude) ?? this._selectedPinOrRoute; if (doc.latitude !== undefined && doc.longitude !== undefined && !existingPin) { existingPin = this.createPushpin(NumCast(doc.latitude), NumCast(doc.longitude), StrCast(doc.map)); @@ -250,7 +247,7 @@ export class MapBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem if (existingPin) { setTimeout(() => { // we use a timeout in case this is called from the sidebar which may have just added a link that hasn't made its way into th elink manager yet - if (!LinkManager.Instance.getAllRelatedLinks(doc).some(link => DocCast(link.link_anchor_1)?.mapPin === existingPin || DocCast(link.link_anchor_2)?.mapPin === existingPin)) { + if (!Doc.Links(doc).some(link => DocCast(link.link_anchor_1)?.mapPin === existingPin || DocCast(link.link_anchor_2)?.mapPin === existingPin)) { const anchor = this.getAnchor(true, undefined, existingPin); anchor && DocUtils.MakeLink(anchor, doc, { link_relationship: 'link to map location' }); doc.latitude = existingPin?.latitude; @@ -258,14 +255,17 @@ export class MapBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem } }); } - }); //add to annotation list + }); // add to annotation list - return this.addDocument(doc, sidebarKey); // add to sidebar list + return this.addDocument(docs, sidebarKey); // add to sidebar list }; removeMapDocument = (doc: Doc | Doc[], annotationKey?: string) => { - const docs = doc instanceof Doc ? [doc] : doc; - this.allAnnotations.filter(anno => docs.includes(DocCast(anno.mapPin))).forEach(anno => (anno.mapPin = undefined)); + this.allAnnotations + .filter(anno => toList(doc).includes(DocCast(anno.mapPin))) + .forEach(anno => { + anno.mapPin = undefined; + }); return this.removeDocument(doc, annotationKey, undefined); }; @@ -285,7 +285,7 @@ export class MapBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem setupMoveUpEvents( this, e, - (e, down, delta) => + (moveEv, down, delta) => runInAction(() => { const localDelta = this._props .ScreenToLocalTransform() @@ -325,7 +325,7 @@ export class MapBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem backgroundColor: this.SidebarShown ? Colors.MEDIUM_BLUE : Colors.BLACK, }} onPointerDown={this.sidebarBtnDown}> - <FontAwesomeIcon style={{ color: Colors.WHITE }} icon={'comment-alt'} size="sm" /> + <FontAwesomeIcon style={{ color: Colors.WHITE }} icon="comment-alt" size="sm" /> </div> ); } @@ -354,16 +354,16 @@ export class MapBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem const targetCreator = (annotationOn: Doc | undefined) => { const target = DocUtils.GetNewTextDoc('Note linked to ' + this.Document.title, 0, 0, 100, 100, annotationOn, 'yellow'); - FormattedTextBox.SetSelectOnLoad(target); + Doc.SetSelectOnLoad(target); return target; }; const docView = this.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.Document; - e.annoDragData.linkSourceDoc.followLinkZoom = false; + dragComplete: dragEv => { + if (!dragEv.aborted && dragEv.annoDragData && dragEv.annoDragData.linkSourceDoc && dragEv.annoDragData.dropDocument && dragEv.linkDocument) { + dragEv.annoDragData.linkSourceDoc.followLinkToggle = dragEv.annoDragData.dropDocument.annotationOn === this.Document; + dragEv.annoDragData.linkSourceDoc.followLinkZoom = false; } }, }); @@ -389,7 +389,7 @@ export class MapBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem sidebarDown = (e: React.PointerEvent) => { setupMoveUpEvents(this, e, this.sidebarMove, emptyFunction, () => setTimeout(this.toggleSidebar), true); }; - sidebarMove = (e: PointerEvent, down: number[], delta: number[]) => { + sidebarMove = (e: PointerEvent) => { const bounds = this._ref.current!.getBoundingClientRect(); this.layoutDoc._layout_sidebarWidthPercent = '' + 100 * Math.max(0, 1 - (e.clientX - bounds.left) / bounds.width) + '%'; this.layoutDoc._layout_showSidebar = this.layoutDoc._layout_sidebarWidthPercent !== '0%'; @@ -401,8 +401,8 @@ export class MapBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem panelWidth = () => this._props.PanelWidth() / (this._props.NativeDimScaling?.() || 1) - this.sidebarWidth(); panelHeight = () => this._props.PanelHeight() / (this._props.NativeDimScaling?.() || 1); scrollXf = () => this.ScreenToLocalBoxXf().translate(0, NumCast(this.layoutDoc._layout_scrollTop)); - transparentFilter = () => [...this._props.childFilters(), Utils.TransparentBackgroundFilter]; - opaqueFilter = () => [...this._props.childFilters(), Utils.OpaqueBackgroundFilter]; + transparentFilter = () => [...this._props.childFilters(), ClientUtils.TransparentBackgroundFilter]; + opaqueFilter = () => [...this._props.childFilters(), ClientUtils.OpaqueBackgroundFilter]; infoWidth = () => this._props.PanelWidth() / 5; infoHeight = () => this._props.PanelHeight() / 5; anchorMenuClick = () => this._sidebarRef.current?.anchorMenuClick; @@ -435,7 +435,9 @@ export class MapBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem this.toggleSidebar(); options.didMove = true; } - return new Promise<Opt<DocumentView>>(res => DocumentManager.Instance.AddViewRenderedCb(doc, dv => res(dv))); + return new Promise<Opt<DocumentView>>(res => { + DocumentView.addViewRenderedCb(doc, dv => res(dv)); + }); }; /* @@ -458,7 +460,7 @@ export class MapBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem if (anchor) { if (!addAsAnnotation) anchor.backgroundColor = 'transparent'; addAsAnnotation && this.addDocument(anchor); - PresBox.pinDocView(anchor, { pinDocLayout: pinProps?.pinDocLayout, pinData: { ...(pinProps?.pinData ?? {}), map: true } }, this.Document); + PinDocView(anchor, { pinDocLayout: pinProps?.pinDocLayout, pinData: { ...(pinProps?.pinData ?? {}), map: true } }, this.Document); return anchor; } return this.Document; @@ -526,6 +528,7 @@ export class MapBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem document.removeEventListener('pointerdown', this.tryHideMapAnchorMenu); }; + // eslint-disable-next-line @typescript-eslint/no-unused-vars recolorPin = (pin: Doc, color?: string) => { // this._bingMap.current.entities.remove(this.map_docToPinMap.get(pin)); // this.map_docToPinMap.delete(pin); @@ -580,6 +583,7 @@ export class MapBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem MapAnchorMenu.Instance.fadeOut(true); return mapRoute; } + return undefined; // TODO: Display error that can't create route to same location }, 'createmaproute'); @@ -652,9 +656,8 @@ export class MapBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem console.error(features); if (features && features.length > 0 && features[0].properties && features[0].geometry) { - const geometry = features[0].geometry as LineString; - const routeTitle: string = features[0].properties['routeTitle']; - const routeDoc: Doc | undefined = this.allRoutes.find(routeDoc => routeDoc.title === routeTitle); + const { routeTitle } = features[0].properties; + const routeDoc: Doc | undefined = this.allRoutes.find(rtDoc => rtDoc.title === routeTitle); this.deselectPinOrRoute(); // TODO: Also deselect route if selected if (routeDoc) { this._selectedPinOrRoute = routeDoc; @@ -698,7 +701,7 @@ export class MapBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem */ handleMapDblClick = async (e: MapLayerMouseEvent) => { e.preventDefault(); - const lngLat: LngLat = e.lngLat; + const { lngLat } = e; const longitude: number = lngLat.lng; const latitude: number = lngLat.lat; @@ -834,6 +837,7 @@ export class MapBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem if (!this._isAnimating) { return this.mapboxMapViewState; } + return undefined; } @action @@ -917,69 +921,73 @@ export class MapBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem this.path = path; this._isAnimating = true; - runInAction(() => { - return new Promise<void>(async resolve => { - const targetLngLat = { - lng: this.selectedRouteCoordinates[0][0], - lat: this.selectedRouteCoordinates[0][1], - }; - - const animationUtil = new AnimationUtility(targetLngLat, this.selectedRouteCoordinates, this._isStreetViewAnimation, this._animationSpeed, this._showTerrain, this._mapRef.current); - runInAction(() => this.setAnimationUtility(animationUtil)); + runInAction( + () => + // eslint-disable-next-line no-async-promise-executor + new Promise<void>(async resolve => { + const targetLngLat = { + lng: this.selectedRouteCoordinates[0][0], + lat: this.selectedRouteCoordinates[0][1], + }; + + const animationUtil = new AnimationUtility(targetLngLat, this.selectedRouteCoordinates, this._isStreetViewAnimation, this._animationSpeed, this._showTerrain, this._mapRef.current); + runInAction(() => this.setAnimationUtility(animationUtil)); + + const updateFrameId = (newFrameId: number) => this.setFrameId(newFrameId); + const updateAnimationPhase = (newAnimationPhase: number) => this.setAnimationPhase(newAnimationPhase); + + if (status !== AnimationStatus.RESUME) { + const result = await animationUtil.flyInAndRotate({ + map: this._mapRef.current!, + // targetLngLat, + // duration 3000 + // startAltitude: 3000000, + // endAltitude: this.isStreetViewAnimation ? 80 : 12000, + // startBearing: 0, + // endBearing: -20, + // startPitch: 40, + // endPitch: this.isStreetViewAnimation ? 80 : 50, + updateFrameId, + }); + + console.log('Bearing: ', result.bearing); + console.log('Altitude: ', result.altitude); + } - const updateFrameId = (newFrameId: number) => this.setFrameId(newFrameId); - const updateAnimationPhase = (newAnimationPhase: number) => this.setAnimationPhase(newAnimationPhase); + runInAction(() => { + this._finishedFlyTo = true; + }); - if (status !== AnimationStatus.RESUME) { - const result = await animationUtil.flyInAndRotate({ + // follow the path while slowly rotating the camera, passing in the camera bearing and altitude from the previous animation + await animationUtil.animatePath({ map: this._mapRef.current!, - // targetLngLat, - // duration 3000 - // startAltitude: 3000000, - // endAltitude: this.isStreetViewAnimation ? 80 : 12000, - // startBearing: 0, - // endBearing: -20, - // startPitch: 40, - // endPitch: this.isStreetViewAnimation ? 80 : 50, + // path: this.path, + // startBearing: -20, + // startAltitude: this.isStreetViewAnimation ? 80 : 12000, + // pitch: this.isStreetViewAnimation ? 80: 50, + currentAnimationPhase: this._animationPhase, + updateAnimationPhase, updateFrameId, }); - console.log('Bearing: ', result.bearing); - console.log('Altitude: ', result.altitude); - } - - runInAction(() => (this._finishedFlyTo = true)); - - // follow the path while slowly rotating the camera, passing in the camera bearing and altitude from the previous animation - await animationUtil.animatePath({ - map: this._mapRef.current!, - // path: this.path, - // startBearing: -20, - // startAltitude: this.isStreetViewAnimation ? 80 : 12000, - // pitch: this.isStreetViewAnimation ? 80: 50, - currentAnimationPhase: this._animationPhase, - updateAnimationPhase, - updateFrameId, - }); - - // get the bounds of the linestring, use fitBounds() to animate to a final view - const bbox3d = turf.bbox(this.path); + // get the bounds of the linestring, use fitBounds() to animate to a final view + const bbox3d = turf.bbox(this.path); - const bbox2d: LngLatBoundsLike = [bbox3d[0], bbox3d[1], bbox3d[2], bbox3d[3]]; + const bbox2d: LngLatBoundsLike = [bbox3d[0], bbox3d[1], bbox3d[2], bbox3d[3]]; - this._mapRef.current!.fitBounds(bbox2d, { - duration: 3000, - pitch: 30, - bearing: 0, - padding: 120, - }); + this._mapRef.current!.fitBounds(bbox2d, { + duration: 3000, + pitch: 30, + bearing: 0, + padding: 120, + }); - setTimeout(() => { - this._isStreetViewAnimation = false; - resolve(); - }, 10000); - }); - }); + setTimeout(() => { + this._isStreetViewAnimation = false; + resolve(); + }, 10000); + }) + ); }; @action @@ -1008,53 +1016,49 @@ export class MapBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem } }; - getRouteAnimationOptions = (): JSX.Element => { - return ( - <> + getRouteAnimationOptions = (): JSX.Element => ( + <> + <IconButton + tooltip={this._isAnimating && this._finishedFlyTo ? 'Pause Animation' : 'Play Animation'} + onPointerDown={() => { + if (this._isAnimating && this._finishedFlyTo) { + this.pauseAnimation(); + } else if (this._animationPhase > 0) { + this.playAnimation(AnimationStatus.RESUME); // Resume from the current phase + } else { + this.playAnimation(AnimationStatus.START); // Play from the beginning + } + }} + icon={this._isAnimating && this._finishedFlyTo ? <FontAwesomeIcon icon={faPause as IconLookup} /> : <FontAwesomeIcon icon={faPlay as IconLookup} />} + color="black" + size={Size.MEDIUM} + /> + {this._isAnimating && this._finishedFlyTo && ( <IconButton - tooltip={this._isAnimating && this._finishedFlyTo ? 'Pause Animation' : 'Play Animation'} + tooltip="Restart animation" onPointerDown={() => { - if (this._isAnimating && this._finishedFlyTo) { - this.pauseAnimation(); - } else if (this._animationPhase > 0) { - this.playAnimation(AnimationStatus.RESUME); // Resume from the current phase - } else { - this.playAnimation(AnimationStatus.START); // Play from the beginning - } + this.stopAnimation(false); + this.playAnimation(AnimationStatus.START); }} - icon={this._isAnimating && this._finishedFlyTo ? <FontAwesomeIcon icon={faPause as IconLookup} /> : <FontAwesomeIcon icon={faPlay as IconLookup} />} + icon={<FontAwesomeIcon icon={faRotate as IconLookup} />} color="black" size={Size.MEDIUM} /> - {this._isAnimating && this._finishedFlyTo && ( - <IconButton - tooltip="Restart animation" - onPointerDown={() => { - this.stopAnimation(false); - this.playAnimation(AnimationStatus.START); - }} - icon={<FontAwesomeIcon icon={faRotate as IconLookup} />} - color="black" - size={Size.MEDIUM} - /> - )} - <IconButton style={{ marginRight: '10px' }} tooltip="Stop and close animation" onPointerDown={() => this.stopAnimation(true)} icon={<FontAwesomeIcon icon={faCircleXmark as IconLookup} />} color="black" size={Size.MEDIUM} /> - <> - <div className="animation-suboptions"> - <div>|</div> - <FormControlLabel className="first-person-label" label="1st person animation:" labelPlacement="start" control={<Checkbox color="success" checked={this._isStreetViewAnimation} onChange={this.toggleIsStreetViewAnimation} />} /> - <div id="divider">|</div> - <IconButton tooltip={this.animationSpeedTooltipText} onPointerDown={this.updateAnimationSpeed} icon={this.animationSpeedIcon} size={Size.MEDIUM} /> - <div id="divider">|</div> - <div style={{ display: 'flex', alignItems: 'center' }}> - <div>Select Line Color: </div> - <CirclePicker circleSize={12} circleSpacing={5} width="100%" colors={['#ffff00', '#03a9f4', '#ff0000', '#ff5722', '#000000', '#673ab7']} onChange={(color: any) => this.setAnimationLineColor(color)} /> - </div> - </div> - </> - </> - ); - }; + )} + <IconButton style={{ marginRight: '10px' }} tooltip="Stop and close animation" onPointerDown={() => this.stopAnimation(true)} icon={<FontAwesomeIcon icon={faCircleXmark as IconLookup} />} color="black" size={Size.MEDIUM} /> + <div className="animation-suboptions"> + <div>|</div> + <FormControlLabel className="first-person-label" label="1st person animation:" labelPlacement="start" control={<Checkbox color="success" checked={this._isStreetViewAnimation} onChange={this.toggleIsStreetViewAnimation} />} /> + <div id="divider">|</div> + <IconButton tooltip={this.animationSpeedTooltipText} onPointerDown={this.updateAnimationSpeed} icon={this.animationSpeedIcon} size={Size.MEDIUM} /> + <div id="divider">|</div> + <div style={{ display: 'flex', alignItems: 'center' }}> + <div>Select Line Color: </div> + <CirclePicker circleSize={12} circleSpacing={5} width="100%" colors={['#ffff00', '#03a9f4', '#ff0000', '#ff5722', '#000000', '#673ab7']} onChange={(color: any) => this.setAnimationLineColor(color)} /> + </div> + </div> + </> + ); @action hideRoute = () => { @@ -1066,7 +1070,7 @@ export class MapBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem @action toggleSettings = () => { - if (!this._isAnimating && this._animationPhase == 0) { + if (!this._isAnimating && this._animationPhase === 0) { this._featuresFromGeocodeResults = []; this._settingsOpen = !this._settingsOpen; } @@ -1123,7 +1127,9 @@ export class MapBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem }; @action - onMapZoom = (e: ViewStateChangeEvent) => (this.dataDoc.map_zoom = e.viewState.zoom); + onMapZoom = (e: ViewStateChangeEvent) => { + this.dataDoc.map_zoom = e.viewState.zoom; + }; @action onMapMove = (e: ViewStateChangeEvent) => { @@ -1132,7 +1138,9 @@ export class MapBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem }; @action - toggleShowTerrain = () => (this._showTerrain = !this._showTerrain); + toggleShowTerrain = () => { + this._showTerrain = !this._showTerrain; + }; getMarkerIcon = (pinDoc: Doc): JSX.Element | null => { const markerType = StrCast(pinDoc.markerType); @@ -1146,7 +1154,6 @@ export class MapBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem const scale = this._props.NativeDimScaling?.() || 1; const parscale = scale === 1 ? 1 : this.ScreenToLocalBoxXf().Scale ?? 1; - const renderAnnotations = (childFilters?: () => string[]) => null; return ( <div className="mapBox" ref={this._ref}> <div @@ -1154,15 +1161,12 @@ export class MapBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem onWheel={e => e.stopPropagation()} onPointerDown={e => e.button === 0 && !e.ctrlKey && e.stopPropagation()} style={{ transformOrigin: 'top left', transform: `scale(${scale})`, width: `calc(100% - ${this.sidebarWidthPercent})`, pointerEvents: this.pointerEvents() }}> - <div style={{ mixBlendMode: 'multiply' }}>{renderAnnotations(this.transparentFilter)}</div> - {renderAnnotations(this.opaqueFilter)} - {SnappingManager.IsDragging ? null : renderAnnotations()} {!this._routeToAnimate && ( <div className="mapBox-searchbar" style={{ width: `${100 / scale}%`, zIndex: 1, position: 'relative', background: 'lightGray' }}> <TextField ref={this._textRef} fullWidth placeholder="Enter a location" onKeyDown={this.searchbarKeyDown} onChange={(e: any) => this.handleSearchChange(e.target.value)} /> - <IconButton icon={<FontAwesomeIcon icon={faGear as IconLookup} size="1x" />} type={Type.TERT} onClick={e => this.toggleSettings()} /> + <IconButton icon={<FontAwesomeIcon icon={faGear as IconLookup} size="1x" />} type={Type.TERT} onClick={() => this.toggleSettings()} /> <div style={{ opacity: 0 }}> - <IconButton icon={<FontAwesomeIcon icon={faGear as IconLookup} size="1x" />} type={Type.TERT} onClick={e => this.toggleSettings()} /> + <IconButton icon={<FontAwesomeIcon icon={faGear as IconLookup} size="1x" />} type={Type.TERT} onClick={() => this.toggleSettings()} /> </div> </div> )} @@ -1210,22 +1214,21 @@ export class MapBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem )} {this._featuresFromGeocodeResults.length > 0 && ( <div className="mapbox-geocoding-search-results"> - <> - <h4>Choose a location for your pin: </h4> - {this._featuresFromGeocodeResults - .filter(feature => feature.place_name) - .map((feature, idx) => ( - <div - key={idx} - className="search-result-container" - onClick={() => { - this.handleSearchChange(''); - this.addMarkerForFeature(feature); - }}> - <div className="search-result-place-name">{feature.place_name}</div> - </div> - ))} - </> + <h4>Choose a location for your pin: </h4> + {this._featuresFromGeocodeResults + .filter(feature => feature.place_name) + .map((feature, idx) => ( + <div + // eslint-disable-next-line react/no-array-index-key + key={idx} + className="search-result-container" + onClick={() => { + this.handleSearchChange(''); + this.addMarkerForFeature(feature); + }}> + <div className="search-result-place-name">{feature.place_name}</div> + </div> + ))} </div> )} <MapProvider> @@ -1252,7 +1255,7 @@ export class MapBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem <Source id="temporary-route" type="geojson" data={this._temporaryRouteSource} /> <Source id="map-routes" type="geojson" data={this.allRoutesGeoJson} /> <Layer id="temporary-route-layer" type="line" source="temporary-route" layout={{ 'line-join': 'round', 'line-cap': 'round' }} paint={{ 'line-color': '#36454F', 'line-width': 4, 'line-dasharray': [1, 1] }} /> - {!this._isAnimating && this._animationPhase == 0 && ( + {!this._isAnimating && this._animationPhase === 0 && ( <Layer id="map-routes-layer" type="line" source="map-routes" layout={{ 'line-join': 'round', 'line-cap': 'round' }} paint={{ 'line-color': '#FF0000', 'line-width': 4 }} /> )} {this._routeToAnimate && (this._isAnimating || this._animationPhase > 0) && ( @@ -1316,16 +1319,15 @@ export class MapBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem </> )} - <> - {!this._isAnimating && - this._animationPhase == 0 && - this.allPushpins // .filter(anno => !anno.layout_unrendered) - .map((pushpin, idx) => ( - <Marker key={idx} longitude={NumCast(pushpin.longitude)} latitude={NumCast(pushpin.latitude)} anchor="bottom" onClick={(e: MarkerEvent<mapboxgl.Marker, MouseEvent>) => this.handleMarkerClick(e, pushpin)}> - {this.getMarkerIcon(pushpin)} - </Marker> - ))} - </> + {!this._isAnimating && + this._animationPhase === 0 && + this.allPushpins // .filter(anno => !anno.layout_unrendered) + .map((pushpin, idx) => ( + // eslint-disable-next-line react/no-array-index-key + <Marker key={idx} longitude={NumCast(pushpin.longitude)} latitude={NumCast(pushpin.latitude)} anchor="bottom" onClick={(e: MarkerEvent<mapboxgl.Marker, MouseEvent>) => this.handleMarkerClick(e, pushpin)}> + {this.getMarkerIcon(pushpin)} + </Marker> + ))} {/* {this.mapMarkers.length > 0 && this.mapMarkers.map((marker, idx) => ( <Marker key={idx} longitude={marker.longitude} latitude={marker.latitude}/> @@ -1336,12 +1338,13 @@ export class MapBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem <div className="mapBox-sidebar" style={{ width: `${this.sidebarWidthPercent}`, backgroundColor: `${this.sidebarColor}` }}> <SidebarAnnos ref={this._sidebarRef} + // eslint-disable-next-line react/jsx-props-no-spreading {...this._props} fieldKey={this.fieldKey} Document={this.Document} layoutDoc={this.layoutDoc} dataDoc={this.dataDoc} - usePanelWidth={true} + usePanelWidth showSidebar={this.SidebarShown} nativeWidth={NumCast(this.layoutDoc._nativeWidth)} whenChildContentsActiveChanged={this.whenChildContentsActiveChanged} @@ -1356,3 +1359,8 @@ export class MapBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem ); } } + +Docs.Prototypes.TemplateMap.set(DocumentType.MAP, { + layout: { view: MapBox, dataField: 'data' }, + options: { acl: '', map: '', _height: 600, _width: 800, _layout_reflowHorizontal: true, _layout_reflowVertical: true, _layout_nativeDimEditable: true, systemIcon: 'BsFillPinMapFill' }, +}); diff --git a/src/client/views/nodes/MapBox/MapBox2.tsx b/src/client/views/nodes/MapBox/MapBox2.tsx index 9825824bd..7697fd295 100644 --- a/src/client/views/nodes/MapBox/MapBox2.tsx +++ b/src/client/views/nodes/MapBox/MapBox2.tsx @@ -509,9 +509,9 @@ // // TODO: auto center on select a document in the sidebar // private handleMapCenter = (map: google.maps.Map) => { -// // console.log("print the selected views in selectionManager:") -// // if (SelectionManager.Views.lastElement()) { -// // console.log(SelectionManager.Views.lastElement()); +// // console.log("print the selected views in Document.Selected:") +// // if (DocumentView.Selected().lastElement()) { +// // console.log(DocumentView.Selected().lastElement()); // // } // }; diff --git a/src/client/views/nodes/MapBox/MapBoxInfoWindow.tsx b/src/client/views/nodes/MapBox/MapBoxInfoWindow.tsx index 6ccbbbe1c..c69cd8e89 100644 --- a/src/client/views/nodes/MapBox/MapBoxInfoWindow.tsx +++ b/src/client/views/nodes/MapBox/MapBoxInfoWindow.tsx @@ -32,7 +32,7 @@ // addNoteClick = (e: React.PointerEvent) => { // setupMoveUpEvents(this, e, returnFalse, emptyFunction, e => { // const newDoc = Docs.Create.TextDocument('Note', { _layout_autoHeight: true }); -// FormattedTextBox.SetSelectOnLoad(newDoc); // track the new text box so we can give it a prop that tells it to focus itself when it's displayed +// Doc.SetSelectOnLoad(newDoc); // track the new text box so we can give it a prop that tells it to focus itself when it's displayed // Doc.AddDocToList(this.props.place, 'data', newDoc); // this._stack?.scrollToBottom(); // e.stopPropagation(); diff --git a/src/client/views/nodes/MapBox/MapPushpinBox.tsx b/src/client/views/nodes/MapBox/MapPushpinBox.tsx index 8ebc90157..f3dc44755 100644 --- a/src/client/views/nodes/MapBox/MapPushpinBox.tsx +++ b/src/client/views/nodes/MapBox/MapPushpinBox.tsx @@ -2,6 +2,8 @@ import * as React from 'react'; import { ViewBoxBaseComponent } from '../../DocComponent'; import { FieldView, FieldViewProps } from '../FieldView'; import { MapBoxContainer } from '../MapboxMapBox/MapboxContainer'; +import { Docs } from '../../../documents/Documents'; +import { DocumentType } from '../../../documents/DocumentTypes'; /** * Map Pushpin doc class @@ -28,3 +30,8 @@ export class MapPushpinBox extends ViewBoxBaseComponent<FieldViewProps>() { return <div />; } } + +Docs.Prototypes.TemplateMap.set(DocumentType.PUSHPIN, { + layout: { view: MapPushpinBox, dataField: 'data' }, + options: { acl: '' }, +}); diff --git a/src/client/views/nodes/MapBox/MapboxApiUtility.ts b/src/client/views/nodes/MapBox/MapboxApiUtility.ts index 592330ac2..5c5192372 100644 --- a/src/client/views/nodes/MapBox/MapboxApiUtility.ts +++ b/src/client/views/nodes/MapBox/MapboxApiUtility.ts @@ -1,4 +1,3 @@ - const MAPBOX_FORWARD_GEOCODE_BASE_URL = 'https://api.mapbox.com/geocoding/v5/mapbox.places/'; const MAPBOX_REVERSE_GEOCODE_BASE_URL = 'https://api.mapbox.com/geocoding/v5/mapbox.places/'; const MAPBOX_DIRECTIONS_BASE_URL = 'https://api.mapbox.com/directions/v5/mapbox'; @@ -7,92 +6,79 @@ const MAPBOX_ACCESS_TOKEN = 'pk.eyJ1IjoiemF1bHRhdmFuZ2FyIiwiYSI6ImNscHgwNDd1MDA3 export type TransportationType = 'driving' | 'cycling' | 'walking'; export class MapboxApiUtility { - static forwardGeocodeForFeatures = async (searchText: string) => { try { - const url = MAPBOX_FORWARD_GEOCODE_BASE_URL + encodeURI(searchText) +'.json' +`?access_token=${MAPBOX_ACCESS_TOKEN}`; + const url = MAPBOX_FORWARD_GEOCODE_BASE_URL + encodeURI(searchText) + `.json?access_token=${MAPBOX_ACCESS_TOKEN}`; const response = await fetch(url); const data = await response.json(); return data.features; - } catch (error: any){ - // TODO: handle error in better way + } catch (error: any) { + // TODO: handle error in better way return null; } - } + }; static reverseGeocodeForFeatures = async (longitude: number, latitude: number) => { try { - const url = MAPBOX_REVERSE_GEOCODE_BASE_URL + encodeURI(longitude.toString() + "," + latitude.toString()) + '.json' + - `?access_token=${MAPBOX_ACCESS_TOKEN}`; + const url = MAPBOX_REVERSE_GEOCODE_BASE_URL + encodeURI(longitude.toString() + ',' + latitude.toString()) + `.json?access_token=${MAPBOX_ACCESS_TOKEN}`; const response = await fetch(url); const data = await response.json(); return data.features; - } catch (error: any){ + } catch (error: any) { return null; } - } + }; static getDirections = async (origin: number[], destination: number[]): Promise<Record<TransportationType, any> | undefined> => { try { - const directionsPromises: Promise<any>[] = []; const transportationTypes: TransportationType[] = ['driving', 'cycling', 'walking']; - transportationTypes.forEach((type) => { - directionsPromises.push( - fetch( - `${MAPBOX_DIRECTIONS_BASE_URL}/${type}/${origin[0]},${origin[1]};${destination[0]},${destination[1]}?steps=true&geometries=geojson&access_token=${MAPBOX_ACCESS_TOKEN}` - ).then((response) => response.json()) - ); - }); + transportationTypes.forEach(type => { + directionsPromises.push(fetch(`${MAPBOX_DIRECTIONS_BASE_URL}/${type}/${origin[0]},${origin[1]};${destination[0]},${destination[1]}?steps=true&geometries=geojson&access_token=${MAPBOX_ACCESS_TOKEN}`).then(response => response.json())); + }); const results = await Promise.all(directionsPromises); const routeInfoMap: Record<TransportationType, any> = { - 'driving': {}, - 'cycling': {}, - 'walking': {}, + driving: {}, + cycling: {}, + walking: {}, }; transportationTypes.forEach((type, index) => { const routeData = results[index].routes[0]; if (routeData) { - const geometry = routeData.geometry; - const coordinates = geometry.coordinates; - - routeInfoMap[type] = { - duration: this.secondsToMinutesHours(routeData.duration), - distance: this.metersToMiles(routeData.distance), - coordinates: coordinates, - }; + const { geometry } = routeData; + const { coordinates } = geometry; + + routeInfoMap[type] = { + duration: this.secondsToMinutesHours(routeData.duration), + distance: this.metersToMiles(routeData.distance), + coordinates: coordinates, + }; } - }); + }); return routeInfoMap; - // return current route info, and the temporary route - - } catch (error: any){ + // return current route info, and the temporary route + } catch (error: any) { return undefined; - console.log("Error: ", error); } - } + }; private static secondsToMinutesHours = (seconds: number) => { const hours = Math.floor(seconds / 3600); const minutes = Math.floor((seconds % 3600) / 60).toFixed(2); - if (hours === 0){ - return `${minutes} min` - } else { - return `${hours} hr ${minutes} min` + if (hours === 0) { + return `${minutes} min`; } - } - - private static metersToMiles = (meters: number) => { - return `${parseFloat((meters/1609.34).toFixed(2))} mi`; - } + return `${hours} hr ${minutes} min`; + }; + private static metersToMiles = (meters: number) => `${parseFloat((meters / 1609.34).toFixed(2))} mi`; } // const drivingQuery = await fetch( @@ -136,4 +122,4 @@ export class MapboxApiUtility { // distance: this.metersToMiles(routeData.distance), // coordinates: coordinates // } -// })
\ No newline at end of file +// }) diff --git a/src/client/views/nodes/MapBox/MarkerIcons.tsx b/src/client/views/nodes/MapBox/MarkerIcons.tsx index a580fcaa0..087472112 100644 --- a/src/client/views/nodes/MapBox/MarkerIcons.tsx +++ b/src/client/views/nodes/MapBox/MarkerIcons.tsx @@ -17,8 +17,6 @@ import { faHouse, faLandmark, faLocationDot, - faLocationPin, - faMapPin, faMasksTheater, faMugSaucer, faPersonHiking, @@ -65,6 +63,7 @@ export class MarkerIcons { iconProps.color = color; } + // eslint-disable-next-line react/jsx-props-no-spreading return <FontAwesomeIcon {...iconProps} size={size} />; } diff --git a/src/client/views/nodes/MapboxMapBox/MapboxContainer.tsx b/src/client/views/nodes/MapboxMapBox/MapboxContainer.tsx index 3eb051dbf..bfd40692b 100644 --- a/src/client/views/nodes/MapboxMapBox/MapboxContainer.tsx +++ b/src/client/views/nodes/MapboxMapBox/MapboxContainer.tsx @@ -4,28 +4,28 @@ import { IReactionDisposer, ObservableMap, action, computed, makeObservable, obs import { observer } from 'mobx-react'; import * as React from 'react'; import { MapProvider, Map as MapboxMap } from 'react-map-gl'; -import { Utils, emptyFunction, returnEmptyDoclist, returnEmptyFilter, returnFalse, returnOne, setupMoveUpEvents } from '../../../../Utils'; +import { ClientUtils, returnEmptyDoclist, returnEmptyFilter, returnFalse, returnOne, setupMoveUpEvents } from '../../../../ClientUtils'; +import { emptyFunction } from '../../../../Utils'; import { Doc, DocListCast, Field, LinkedTo, Opt } from '../../../../fields/Doc'; import { DocCss, Highlight } from '../../../../fields/DocSymbols'; -import { DocCast, NumCast, StrCast } from '../../../../fields/Types'; +import { Id } from '../../../../fields/FieldSymbols'; +import { DocCast, NumCast, StrCast, toList } from '../../../../fields/Types'; +import { DocUtils } from '../../../documents/DocUtils'; import { DocumentType } from '../../../documents/DocumentTypes'; -import { DocUtils, Docs } from '../../../documents/Documents'; -import { DocumentManager } from '../../../util/DocumentManager'; +import { Docs } from '../../../documents/Documents'; import { DragManager } from '../../../util/DragManager'; -import { LinkManager } from '../../../util/LinkManager'; -import { SnappingManager } from '../../../util/SnappingManager'; import { Transform } from '../../../util/Transform'; import { UndoManager, undoable } from '../../../util/UndoManager'; import { ViewBoxAnnotatableComponent } from '../../DocComponent'; +import { PinDocView, PinProps } from '../../PinFuncs'; import { SidebarAnnos } from '../../SidebarAnnos'; import { MarqueeOptionsMenu } from '../../collections/collectionFreeForm'; import { Colors } from '../../global/globalEnums'; import { DocumentView } from '../DocumentView'; -import { FocusViewOptions, FieldView, FieldViewProps } from '../FieldView'; +import { FieldView, FieldViewProps } from '../FieldView'; +import { FocusViewOptions } from '../FocusViewOptions'; import { MapAnchorMenu } from '../MapBox/MapAnchorMenu'; -import { FormattedTextBox } from '../formattedText/FormattedTextBox'; -import { PinProps, PresBox } from '../trails'; -import './MapBox.scss'; +import '../MapBox/MapBox.scss'; /** * MapBox architecture: @@ -41,7 +41,6 @@ import './MapBox.scss'; */ const mapboxApiKey = 'pk.eyJ1IjoiemF1bHRhdmFuZ2FyIiwiYSI6ImNsbnc2eHJpbTA1ZTUyam85aGx4Z2FhbGwifQ.2Kqw9mk-9wAAg9kmHmKzcg'; -const bingApiKey = process.env.BING_MAPS; // if you're running local, get a Bing Maps api key here: https://www.bingmapsportal.com/ and then add it to the .env file in the Dash-Web root directory as: _CLIENT_BING_MAPS=<your apikey> /** * Consider integrating later: allows for drawing, circling, making shapes on map @@ -87,7 +86,7 @@ export class MapBoxContainer extends ViewBoxAnnotatableComponent<FieldViewProps> return this.allAnnotations.filter(anno => anno.type === DocumentType.PUSHPIN); } @computed get SidebarShown() { - return this.layoutDoc._layout_showSidebar ? true : false; + return !!this.layoutDoc._layout_showSidebar; } @computed get sidebarWidthPercent() { return StrCast(this.layoutDoc._layout_sidebarWidthPercent, '0%'); @@ -118,9 +117,9 @@ export class MapBoxContainer extends ViewBoxAnnotatableComponent<FieldViewProps> * @param sidebarKey * @returns */ - sidebarAddDocument = (doc: Doc | Doc[], sidebarKey?: string) => { + sidebarAddDocument = (docsIn: Doc | Doc[], sidebarKey?: string) => { if (!this.layoutDoc._layout_showSidebar) this.toggleSidebar(); - const docs = doc instanceof Doc ? [doc] : doc; + const docs = toList(docsIn); docs.forEach(doc => { let existingPin = this.allPushpins.find(pin => pin.latitude === doc.latitude && pin.longitude === doc.longitude) ?? this.selectedPin; if (doc.latitude !== undefined && doc.longitude !== undefined && !existingPin) { @@ -129,7 +128,7 @@ export class MapBoxContainer extends ViewBoxAnnotatableComponent<FieldViewProps> if (existingPin) { setTimeout(() => { // we use a timeout in case this is called from the sidebar which may have just added a link that hasn't made its way into th elink manager yet - if (!LinkManager.Instance.getAllRelatedLinks(doc).some(link => DocCast(link.link_anchor_1)?.mapPin === existingPin || DocCast(link.link_anchor_2)?.mapPin === existingPin)) { + if (!Doc.Links(doc).some(link => DocCast(link.link_anchor_1)?.mapPin === existingPin || DocCast(link.link_anchor_2)?.mapPin === existingPin)) { const anchor = this.getAnchor(true, undefined, existingPin); anchor && DocUtils.MakeLink(anchor, doc, { link_relationship: 'link to map location' }); doc.latitude = existingPin?.latitude; @@ -137,15 +136,19 @@ export class MapBoxContainer extends ViewBoxAnnotatableComponent<FieldViewProps> } }); } - }); //add to annotation list + }); // add to annotation list - return this.addDocument(doc, sidebarKey); // add to sidebar list + return this.addDocument(docs, sidebarKey); // add to sidebar list }; - removeMapDocument = (doc: Doc | Doc[], annotationKey?: string) => { - const docs = doc instanceof Doc ? [doc] : doc; - this.allAnnotations.filter(anno => docs.includes(DocCast(anno.mapPin))).forEach(anno => (anno.mapPin = undefined)); - return this.removeDocument(doc, annotationKey, undefined); + removeMapDocument = (docsIn: Doc | Doc[], annotationKey?: string) => { + const docs = toList(docsIn); + this.allAnnotations + .filter(anno => docs.includes(DocCast(anno.mapPin))) + .forEach(anno => { + anno.mapPin = undefined; + }); + return this.removeDocument(docsIn, annotationKey, undefined); }; /** @@ -164,7 +167,7 @@ export class MapBoxContainer extends ViewBoxAnnotatableComponent<FieldViewProps> setupMoveUpEvents( this, e, - (e, down, delta) => + (moveEv, down, delta) => runInAction(() => { const localDelta = this._props .ScreenToLocalTransform() @@ -204,7 +207,7 @@ export class MapBoxContainer extends ViewBoxAnnotatableComponent<FieldViewProps> backgroundColor: this.SidebarShown ? Colors.MEDIUM_BLUE : Colors.BLACK, }} onPointerDown={this.sidebarBtnDown}> - <FontAwesomeIcon style={{ color: Colors.WHITE }} icon={'comment-alt'} size="sm" /> + <FontAwesomeIcon style={{ color: Colors.WHITE }} icon="comment-alt" size="sm" /> </div> ); } @@ -233,16 +236,16 @@ export class MapBoxContainer extends ViewBoxAnnotatableComponent<FieldViewProps> const targetCreator = (annotationOn: Doc | undefined) => { const target = DocUtils.GetNewTextDoc('Note linked to ' + this.Document.title, 0, 0, 100, 100, annotationOn, 'yellow'); - FormattedTextBox.SetSelectOnLoad(target); + Doc.SetSelectOnLoad(target); return target; }; const docView = this.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.Document; - e.annoDragData.linkSourceDoc.followLinkZoom = false; + dragComplete: dragEv => { + if (!dragEv.aborted && dragEv.annoDragData && dragEv.annoDragData.linkSourceDoc && dragEv.annoDragData.dropDocument && dragEv.linkDocument) { + dragEv.annoDragData.linkSourceDoc.followLinkToggle = dragEv.annoDragData.dropDocument.annotationOn === this.Document; + dragEv.annoDragData.linkSourceDoc.followLinkZoom = false; } }, }); @@ -268,7 +271,7 @@ export class MapBoxContainer extends ViewBoxAnnotatableComponent<FieldViewProps> sidebarDown = (e: React.PointerEvent) => { setupMoveUpEvents(this, e, this.sidebarMove, emptyFunction, () => setTimeout(this.toggleSidebar), true); }; - sidebarMove = (e: PointerEvent, down: number[], delta: number[]) => { + sidebarMove = (e: PointerEvent) => { const bounds = this._ref.current!.getBoundingClientRect(); this.layoutDoc._layout_sidebarWidthPercent = '' + 100 * Math.max(0, 1 - (e.clientX - bounds.left) / bounds.width) + '%'; this.layoutDoc._layout_showSidebar = this.layoutDoc._layout_sidebarWidthPercent !== '0%'; @@ -276,7 +279,9 @@ export class MapBoxContainer extends ViewBoxAnnotatableComponent<FieldViewProps> return false; }; - setPreviewCursor = (func?: (x: number, y: number, drag: boolean, hide: boolean, doc: Opt<Doc>) => void) => (this._setPreviewCursor = func); + 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); @@ -285,8 +290,8 @@ export class MapBoxContainer extends ViewBoxAnnotatableComponent<FieldViewProps> panelWidth = () => this._props.PanelWidth() / (this._props.NativeDimScaling?.() || 1) - this.sidebarWidth(); panelHeight = () => this._props.PanelHeight() / (this._props.NativeDimScaling?.() || 1); scrollXf = () => this.ScreenToLocalBoxXf().translate(0, NumCast(this.layoutDoc._layout_scrollTop)); - transparentFilter = () => [...this._props.childFilters(), Utils.TransparentBackgroundFilter]; - opaqueFilter = () => [...this._props.childFilters(), Utils.OpaqueBackgroundFilter]; + transparentFilter = () => [...this._props.childFilters(), ClientUtils.TransparentBackgroundFilter]; + opaqueFilter = () => [...this._props.childFilters(), ClientUtils.OpaqueBackgroundFilter]; infoWidth = () => this._props.PanelWidth() / 5; infoHeight = () => this._props.PanelHeight() / 5; anchorMenuClick = () => this._sidebarRef.current?.anchorMenuClick; @@ -306,11 +311,11 @@ export class MapBoxContainer extends ViewBoxAnnotatableComponent<FieldViewProps> // center: new this.MicrosoftMaps.Location(loc.latitude, loc.longitude), // }); // - bingGeocode = (map: any, query: string) => { - return new Promise<{ latitude: number; longitude: number }>((res, reject) => { - //If search manager is not defined, load the search module. + bingGeocode = (map: any, query: string) => + new Promise<{ latitude: number; longitude: number }>((res, reject) => { + // If search manager is not defined, load the search module. if (!this._bingSearchManager) { - //Create an instance of the search manager and call the geocodeQuery function again. + // Create an instance of the search manager and call the geocodeQuery function again. this.MicrosoftMaps.loadModule('Microsoft.Maps.Search', () => { this._bingSearchManager = new this.MicrosoftMaps.Search.SearchManager(map.current); res(this.bingGeocode(map, query)); @@ -319,11 +324,10 @@ export class MapBoxContainer extends ViewBoxAnnotatableComponent<FieldViewProps> this._bingSearchManager.geocode({ where: query, callback: action((r: any) => res(r.results[0].location)), - errorCallback: (e: any) => reject(), + errorCallback: () => reject(), }); } }); - }; @observable bingSearchBarContents: any = this.Document.map; // For Bing Maps: The contents of the Bing search bar (string) @@ -368,7 +372,7 @@ export class MapBoxContainer extends ViewBoxAnnotatableComponent<FieldViewProps> this._bingMap.current.entities.remove(this.map_docToPinMap.get(temp)); } const newpin = new this.MicrosoftMaps.Pushpin(new this.MicrosoftMaps.Location(temp.latitude, temp.longitude)); - this.MicrosoftMaps.Events.addHandler(newpin, 'click', (e: any) => this.pushpinClicked(temp as Doc)); + this.MicrosoftMaps.Events.addHandler(newpin, 'click', () => this.pushpinClicked(temp as Doc)); if (!this._unmounting) { this._bingMap.current.entities.push(newpin); } @@ -383,7 +387,9 @@ export class MapBoxContainer extends ViewBoxAnnotatableComponent<FieldViewProps> this.toggleSidebar(); options.didMove = true; } - return new Promise<Opt<DocumentView>>(res => DocumentManager.Instance.AddViewRenderedCb(doc, dv => res(dv))); + return new Promise<Opt<DocumentView>>(res => { + DocumentView.addViewRenderedCb(doc, dv => res(dv)); + }); }; /* * Pushpin onclick @@ -418,7 +424,7 @@ export class MapBoxContainer extends ViewBoxAnnotatableComponent<FieldViewProps> * Map OnClick */ @action - mapOnClick = (e: { location: { latitude: any; longitude: any } }) => { + mapOnClick = (/* e: { location: { latitude: any; longitude: any } } */) => { this._props.select(false); this.deselectPin(); }; @@ -442,22 +448,23 @@ export class MapBoxContainer extends ViewBoxAnnotatableComponent<FieldViewProps> * Updates maptype */ @action - updateMapType = () => (this.dataDoc.map_type = this._bingMap.current.getMapTypeId()); + updateMapType = () => { + this.dataDoc.map_type = this._bingMap.current.getMapTypeId(); + }; /* * For Bing Maps * Called by search button's onClick * Finds the geocode of the searched contents and sets location to that location - **/ + * */ @action - bingSearch = () => { - return this.bingGeocode(this._bingMap, this.bingSearchBarContents).then(location => { + bingSearch = () => + this.bingGeocode(this._bingMap, this.bingSearchBarContents).then(location => { this.dataDoc.latitude = location.latitude; this.dataDoc.longitude = location.longitude; this.dataDoc.map_zoom = this._bingMap.current.getZoom(); this.dataDoc.map = this.bingSearchBarContents; }); - }; /* * Returns doc w/ relevant info @@ -479,7 +486,7 @@ export class MapBoxContainer extends ViewBoxAnnotatableComponent<FieldViewProps> if (anchor) { if (!addAsAnnotation) anchor.backgroundColor = 'transparent'; addAsAnnotation && this.addDocument(anchor); - PresBox.pinDocView(anchor, { pinDocLayout: pinProps?.pinDocLayout, pinData: { ...(pinProps?.pinData ?? {}), map: true } }, this.Document); + PinDocView(anchor, { pinDocLayout: pinProps?.pinDocLayout, pinData: { ...(pinProps?.pinData ?? {}), map: true } }, this.Document); return anchor; } return this.Document; @@ -502,7 +509,7 @@ export class MapBoxContainer extends ViewBoxAnnotatableComponent<FieldViewProps> this._bingMap.current.entities.push(pushPin); - this.MicrosoftMaps.Events.addHandler(pushPin, 'click', (e: any) => this.pushpinClicked(pin)); + this.MicrosoftMaps.Events.addHandler(pushPin, 'click', () => this.pushpinClicked(pin)); // this.MicrosoftMaps.Events.addHandler(pushPin, 'dblclick', (e: any) => this.pushpinDblClicked(pushPin, pin)); this.map_docToPinMap.set(pin, pushPin); }; @@ -591,13 +598,15 @@ export class MapBoxContainer extends ViewBoxAnnotatableComponent<FieldViewProps> }; @action - searchbarOnEdit = (newText: string) => (this.bingSearchBarContents = newText); + searchbarOnEdit = (newText: string) => { + this.bingSearchBarContents = newText; + }; recolorPin = (pin: Doc, color?: string) => { this._bingMap.current.entities.remove(this.map_docToPinMap.get(pin)); this.map_docToPinMap.delete(pin); const newpin = new this.MicrosoftMaps.Pushpin(new this.MicrosoftMaps.Location(pin.latitude, pin.longitude), color ? { color } : {}); - this.MicrosoftMaps.Events.addHandler(newpin, 'click', (e: any) => this.pushpinClicked(pin)); + this.MicrosoftMaps.Events.addHandler(newpin, 'click', () => this.pushpinClicked(pin)); this._bingMap.current.entities.push(newpin); this.map_docToPinMap.set(pin, newpin); }; @@ -620,14 +629,16 @@ export class MapBoxContainer extends ViewBoxAnnotatableComponent<FieldViewProps> this._disposers.mapLocation = reaction( () => this.Document.map, - mapLoc => (this.bingSearchBarContents = mapLoc), + mapLoc => { + this.bingSearchBarContents = mapLoc; + }, { fireImmediately: true } ); this._disposers.highlight = reaction( () => this.allAnnotations.map(doc => doc[Highlight]), () => { const allConfigPins = this.allAnnotations.map(doc => ({ doc, pushpin: DocCast(doc.mapPin) })).filter(pair => pair.pushpin); - allConfigPins.forEach(({ doc, pushpin }) => { + allConfigPins.forEach(({ pushpin }) => { if (!pushpin[Highlight] && this.map_pinHighlighted.get(pushpin)) { this.recolorPin(pushpin); this.map_pinHighlighted.delete(pushpin); @@ -668,23 +679,23 @@ export class MapBoxContainer extends ViewBoxAnnotatableComponent<FieldViewProps> setupMoveUpEvents( e, e, - e => { + moveEv => { if (!dragClone) { dragClone = this._dragRef.current?.cloneNode(true) as HTMLDivElement; dragClone.style.position = 'absolute'; dragClone.style.zIndex = '10000'; DragManager.Root().appendChild(dragClone); } - dragClone.style.transform = `translate(${e.clientX - 15}px, ${e.clientY - 15}px)`; + dragClone.style.transform = `translate(${moveEv.clientX - 15}px, ${moveEv.clientY - 15}px)`; return false; }, - e => { + upEv => { if (!dragClone) return; DragManager.Root().removeChild(dragClone); - let target = document.elementFromPoint(e.x, e.y); + let target = document.elementFromPoint(upEv.x, upEv.y); while (target) { if (target === this._ref.current) { - const cpt = this.ScreenToLocalBoxXf().transformPoint(e.clientX, e.clientY); + const cpt = this.ScreenToLocalBoxXf().transformPoint(upEv.clientX, upEv.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)); @@ -694,7 +705,7 @@ export class MapBoxContainer extends ViewBoxAnnotatableComponent<FieldViewProps> target = target.parentElement; } }, - e => { + () => { const createPin = () => this.createPushpin(this.Document.latitude, this.Document.longitude, this.Document.map); if (this.bingSearchBarContents) { this.bingSearch().then(createPin); @@ -720,12 +731,12 @@ export class MapBoxContainer extends ViewBoxAnnotatableComponent<FieldViewProps> MapBoxContainer._rerenderDelay = 0; } this._rerenderTimeout = undefined; + // eslint-disable-next-line operator-assignment this.Document[DocCss] = this.Document[DocCss] + 1; }), MapBoxContainer._rerenderDelay); return null; } - const renderAnnotations = (childFilters?: () => string[]) => null; return ( <div className="mapBox" ref={this._ref}> <div @@ -735,15 +746,11 @@ export class MapBoxContainer extends ViewBoxAnnotatableComponent<FieldViewProps> 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.IsDragging ? null : renderAnnotations()} - <div className="mapBox-searchbar"> <EditableText // editing setVal={(newText: string | number) => typeof newText === 'string' && this.searchbarOnEdit(newText)} - onEnter={e => this.bingSearch()} + onEnter={() => this.bingSearch()} placeholder={this.bingSearchBarContents || 'enter city/zip/...'} textAlign="center" /> @@ -752,18 +759,19 @@ export class MapBoxContainer extends ViewBoxAnnotatableComponent<FieldViewProps> <svg aria-hidden="true" focusable="false" data-prefix="fas" data-icon="magnifying-glass" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" color="#DFDFDF"> <path fill="currentColor" - d="M416 208c0 45.9-14.9 88.3-40 122.7L502.6 457.4c12.5 12.5 12.5 32.8 0 45.3s-32.8 12.5-45.3 0L330.7 376c-34.4 25.2-76.8 40-122.7 40C93.1 416 0 322.9 0 208S93.1 0 208 0S416 93.1 416 208zM208 352a144 144 0 1 0 0-288 144 144 0 1 0 0 288z"></path> + d="M416 208c0 45.9-14.9 88.3-40 122.7L502.6 457.4c12.5 12.5 12.5 32.8 0 45.3s-32.8 12.5-45.3 0L330.7 376c-34.4 25.2-76.8 40-122.7 40C93.1 416 0 322.9 0 208S93.1 0 208 0S416 93.1 416 208zM208 352a144 144 0 1 0 0-288 144 144 0 1 0 0 288z" + /> </svg> } onClick={this.bingSearch} type={Type.TERT} /> <div style={{ width: 30, height: 30 }} ref={this._dragRef} onPointerDown={this.dragToggle}> - <Button tooltip="drag to place a pushpin" icon={<FontAwesomeIcon size={'lg'} icon={'bullseye'} />} /> + <Button tooltip="drag to place a pushpin" icon={<FontAwesomeIcon size="lg" icon="bullseye" />} /> </div> </div> <MapProvider> - <MapboxMap id="mabox-map" mapStyle={`mapbox://styles/mapbox/streets-v9`} mapboxAccessToken={mapboxApiKey} /> + <MapboxMap id="mabox-map" mapStyle="mapbox://styles/mapbox/streets-v9" mapboxAccessToken={mapboxApiKey} /> </MapProvider> {/* @@ -780,9 +788,10 @@ export class MapBoxContainer extends ViewBoxAnnotatableComponent<FieldViewProps> ? null : this.allAnnotations .filter(anno => !anno.layout_unrendered) - .map((pushpin, i) => ( + .map(pushpin => ( <DocumentView - key={i} + key={pushpin[Id]} + // eslint-disable-next-line react/jsx-props-no-spreading {...this._props} renderDepth={this._props.renderDepth + 1} Document={pushpin} @@ -792,7 +801,6 @@ export class MapBoxContainer extends ViewBoxAnnotatableComponent<FieldViewProps> NativeHeight={returnOne} onKey={undefined} onDoubleClickScript={undefined} - onBrowseClickScript={undefined} childFilters={returnEmptyFilter} childFiltersByRanges={returnEmptyFilter} searchFilterDocs={returnEmptyDoclist} @@ -821,12 +829,13 @@ export class MapBoxContainer extends ViewBoxAnnotatableComponent<FieldViewProps> <div className="mapBox-sidebar" style={{ width: `${this.sidebarWidthPercent}`, backgroundColor: `${this.sidebarColor}` }}> <SidebarAnnos ref={this._sidebarRef} + // eslint-disable-next-line react/jsx-props-no-spreading {...this._props} fieldKey={this.fieldKey} Document={this.Document} layoutDoc={this.layoutDoc} dataDoc={this.dataDoc} - usePanelWidth={true} + usePanelWidth showSidebar={this.SidebarShown} nativeWidth={NumCast(this.layoutDoc._nativeWidth)} whenChildContentsActiveChanged={this.whenChildContentsActiveChanged} diff --git a/src/client/views/nodes/OpenWhere.ts b/src/client/views/nodes/OpenWhere.ts new file mode 100644 index 000000000..e2a5f1f2a --- /dev/null +++ b/src/client/views/nodes/OpenWhere.ts @@ -0,0 +1,25 @@ +export enum OpenWhereMod { + none = '', + left = 'left', + right = 'right', + top = 'top', + bottom = 'bottom', + keyvalue = 'keyValue', +} +export enum OpenWhere { + lightbox = 'lightbox', + add = 'add', + addLeft = 'add:left', + addRight = 'add:right', + addBottom = 'add:bottom', + close = 'close', + toggle = 'toggle', + toggleRight = 'toggle:right', + replace = 'replace', + replaceRight = 'replace:right', + replaceLeft = 'replace:left', + inParent = 'inParent', + inParentFromScreen = 'inParentFromScreen', + overlay = 'overlay', + addRightKeyvalue = 'add:right:keyValue', +} diff --git a/src/client/views/nodes/PDFBox.scss b/src/client/views/nodes/PDFBox.scss index 0f5e25a0c..7bca1230f 100644 --- a/src/client/views/nodes/PDFBox.scss +++ b/src/client/views/nodes/PDFBox.scss @@ -236,7 +236,7 @@ //pointer-events: none; .pdfViewerDash-text { .textLayer { - display: none; + // display: none; // this makes search highlights not show up span { user-select: none; } diff --git a/src/client/views/nodes/PDFBox.tsx b/src/client/views/nodes/PDFBox.tsx index 1274220b6..b8b9f63a9 100644 --- a/src/client/views/nodes/PDFBox.tsx +++ b/src/client/views/nodes/PDFBox.tsx @@ -1,41 +1,45 @@ +/* eslint-disable jsx-a11y/no-static-element-interactions */ +/* eslint-disable jsx-a11y/control-has-associated-label */ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { action, computed, IReactionDisposer, makeObservable, observable, reaction, runInAction } from 'mobx'; import { observer } from 'mobx-react'; import * as Pdfjs from 'pdfjs-dist'; import 'pdfjs-dist/web/pdf_viewer.css'; import * as React from 'react'; +import { ClientUtils, returnFalse, setupMoveUpEvents, UpdateIcon } from '../../../ClientUtils'; import { Doc, DocListCast, Opt } from '../../../fields/Doc'; import { DocData } from '../../../fields/DocSymbols'; import { Id } from '../../../fields/FieldSymbols'; import { InkTool } from '../../../fields/InkField'; import { ComputedField } from '../../../fields/ScriptField'; -import { Cast, FieldValue, ImageCast, NumCast, StrCast } from '../../../fields/Types'; +import { Cast, FieldValue, ImageCast, NumCast, StrCast, toList } from '../../../fields/Types'; import { ImageField, PdfField } from '../../../fields/URLField'; import { TraceMobx } from '../../../fields/util'; -import { emptyFunction, returnFalse, setupMoveUpEvents, Utils } from '../../../Utils'; -import { Docs, DocUtils } from '../../documents/Documents'; +import { emptyFunction } from '../../../Utils'; +import { Docs } from '../../documents/Documents'; import { CollectionViewType, DocumentType } from '../../documents/DocumentTypes'; -import { DocumentManager } from '../../util/DocumentManager'; +import { DocUtils } from '../../documents/DocUtils'; import { KeyCodes } from '../../util/KeyCodes'; -import { SelectionManager } from '../../util/SelectionManager'; import { undoBatch, UndoManager } from '../../util/UndoManager'; import { CollectionFreeFormView } from '../collections/collectionFreeForm'; import { CollectionStackingView } from '../collections/CollectionStackingView'; import { ContextMenu } from '../ContextMenu'; import { ContextMenuProps } from '../ContextMenuItem'; -import { ViewBoxAnnotatableComponent, ViewBoxInterface } from '../DocComponent'; +import { ViewBoxAnnotatableComponent } from '../DocComponent'; import { Colors } from '../global/globalEnums'; -import { CreateImage } from '../nodes/WebBoxRenderer'; import { PDFViewer } from '../pdf/PDFViewer'; +import { PinDocView, PinProps } from '../PinFuncs'; import { SidebarAnnos } from '../SidebarAnnos'; -import { DocumentView, OpenWhere } from './DocumentView'; -import { FocusViewOptions, FieldView, FieldViewProps } from './FieldView'; +import { DocumentView } from './DocumentView'; +import { FieldView, FieldViewProps } from './FieldView'; +import { FocusViewOptions } from './FocusViewOptions'; import { ImageBox } from './ImageBox'; +import { OpenWhere } from './OpenWhere'; import './PDFBox.scss'; -import { PinProps, PresBox } from './trails'; +import { CreateImage } from './WebBoxRenderer'; @observer -export class PDFBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implements ViewBoxInterface { +export class PDFBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { public static LayoutString(fieldKey: string) { return FieldView.LayoutString(PDFBox, fieldKey); } @@ -66,8 +70,16 @@ export class PDFBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem const nh = Doc.NativeHeight(this.Document, this.dataDoc) || 1200; !this.Document._layout_fitWidth && (this.Document._height = NumCast(this.Document._width) * (nh / nw)); if (this.pdfUrl) { - if (PDFBox.pdfcache.get(this.pdfUrl.url.href)) runInAction(() => (this._pdf = PDFBox.pdfcache.get(this.pdfUrl!.url.href))); - else if (PDFBox.pdfpromise.get(this.pdfUrl.url.href)) PDFBox.pdfpromise.get(this.pdfUrl.url.href)?.then(action((pdf: any) => (this._pdf = pdf))); + if (PDFBox.pdfcache.get(this.pdfUrl.url.href)) + runInAction(() => { + this._pdf = PDFBox.pdfcache.get(this.pdfUrl!.url.href); + }); + else if (PDFBox.pdfpromise.get(this.pdfUrl.url.href)) + PDFBox.pdfpromise.get(this.pdfUrl.url.href)?.then( + action((pdf: any) => { + this._pdf = pdf; + }) + ); } } @@ -85,7 +97,7 @@ export class PDFBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem if (oldDiv instanceof HTMLCanvasElement) { const canvas = oldDiv; const img = document.createElement('img'); // create a Image Element - img.src = canvas.toDataURL(); //image sourcez + img.src = canvas.toDataURL(); // image sourcez img.style.width = canvas.style.width; img.style.height = canvas.style.height; const newCan = newDiv as HTMLCanvasElement; @@ -96,8 +108,12 @@ export class PDFBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem }; crop = (region: Doc | undefined, addCrop?: boolean) => { - if (!region) return; + if (!region) return undefined; const cropping = Doc.MakeCopy(region, true); + cropping.layout_unrendered = false; // text selection have this + cropping.text_inlineAnnotations = undefined; // text selections have this -- it causes them not to be rendered. + cropping.backgroundColor = undefined; // text selections have this -- it causes images to be fully transparent + cropping.opacity = undefined; // text selections have this -- it causes images to be fully transparent const regionData = region[DocData]; regionData.lockedPosition = true; regionData.title = 'region:' + this.Document.title; @@ -111,11 +127,11 @@ export class PDFBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem this.replaceCanvases(docViewContent, newDiv); const htmlString = this._pdfViewer?._mainCont.current && new XMLSerializer().serializeToString(newDiv); - const anchx = NumCast(cropping.x); - const anchy = NumCast(cropping.y); + // 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 viewScale = 1; + // const viewScale = 1; cropping.title = 'crop: ' + this.Document.title; cropping.x = NumCast(this.Document.x) + NumCast(this.layoutDoc._width); cropping.y = NumCast(this.Document.y); @@ -128,9 +144,9 @@ export class PDFBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem croppingProto.proto = Cast(this.Document.proto, Doc, null)?.proto; // set proto of cropping's data doc to be IMAGE_PROTO croppingProto.type = DocumentType.IMG; croppingProto.layout = ImageBox.LayoutString('data'); - croppingProto.data = new ImageField(Utils.CorsProxy('http://www.cs.brown.edu/~bcz/noImage.png')); - croppingProto['data_nativeWidth'] = anchw; - croppingProto['data_nativeHeight'] = anchh; + croppingProto.data = new ImageField(ClientUtils.CorsProxy('http://www.cs.brown.edu/~bcz/noImage.png')); + croppingProto.data_nativeWidth = anchw; + croppingProto.data_nativeHeight = anchh; if (addCrop) { DocUtils.MakeLink(region, cropping, { link_relationship: 'cropped image' }); } @@ -146,8 +162,8 @@ export class PDFBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem (NumCast(region.x) * this._props.PanelWidth()) / NumCast(this.dataDoc[this.fieldKey + '_nativeWidth']), 4 ) - .then((data_url: any) => { - Utils.convertDataUri(data_url, region[Id]).then(returnedfilename => + .then((dataUrl: any) => { + ClientUtils.convertDataUri(dataUrl, region[Id]).then(returnedfilename => setTimeout( action(() => { croppingProto.data = new ImageField(returnedfilename); @@ -156,7 +172,7 @@ export class PDFBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem ) ); }) - .catch(function (error: any) { + .catch((error: any) => { console.error('oops, something went wrong!', error); }); @@ -168,7 +184,7 @@ export class PDFBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem const docViewContent = this.DocumentView?.().ContentDiv!; const filename = this.layoutDoc[Id] + '-icon' + new Date().getTime(); this._pdfViewer?._mainCont.current && - CollectionFreeFormView.UpdateIcon( + UpdateIcon( filename, docViewContent, NumCast(this.layoutDoc._width), @@ -182,8 +198,8 @@ export class PDFBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem (iconFile: string, nativeWidth: number, nativeHeight: number) => { setTimeout(() => { this.dataDoc.icon = new ImageField(iconFile); - this.dataDoc['icon_nativeWidth'] = nativeWidth; - this.dataDoc['icon_nativeHeight'] = nativeHeight; + this.dataDoc.icon_nativeWidth = nativeWidth; + this.dataDoc.icon_nativeHeight = nativeHeight; }, 500); } ); @@ -214,12 +230,13 @@ export class PDFBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem ); } - sidebarAddDocTab = (doc: Doc, where: OpenWhere) => { - if (DocListCast(this.Document[this._props.fieldKey + '_sidebar']).includes(doc) && !this.SidebarShown) { + sidebarAddDocTab = (docIn: Doc | Doc[], where: OpenWhere) => { + const docs = toList(docIn); + if (docs.some(doc => 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(docs, where); }; focus = (anchor: Doc, options: FocusViewOptions) => { this._initialScrollTarget = anchor; @@ -231,23 +248,25 @@ export class PDFBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem options.didMove = true; this.toggleSidebar(false); } - return new Promise<Opt<DocumentView>>(res => DocumentManager.Instance.AddViewRenderedCb(doc, dv => res(dv))); + return new Promise<Opt<DocumentView>>(res => { + DocumentView.addViewRenderedCb(doc, dv => res(dv)); + }); }; getAnchor = (addAsAnnotation: boolean, pinProps?: PinProps) => { - let ele: Opt<HTMLDivElement> = undefined; + let ele: Opt<HTMLDivElement>; if (this._pdfViewer?.selectionContent()) { ele = document.createElement('div'); ele.append(this._pdfViewer.selectionContent()!); } const docAnchor = () => Docs.Create.ConfigDocument({ - title: StrCast(this.Document.title + '@' + NumCast(this.layoutDoc._layout_scrollTop)?.toFixed(0)), + title: StrCast(this.Document.title + '@' + (NumCast(this.layoutDoc._layout_scrollTop) ?? 0).toFixed(0)), annotationOn: this.Document, }); const visibleAnchor = this._pdfViewer?._getAnchor?.(this._pdfViewer.savedAnnotations(), true); const anchor = visibleAnchor ?? docAnchor(); - PresBox.pinDocView(anchor, { pinDocLayout: pinProps?.pinDocLayout, pinData: { ...(pinProps?.pinData ?? {}), scrollable: true, pannable: true } }, this.Document); + PinDocView(anchor, { pinDocLayout: pinProps?.pinDocLayout, pinData: { ...(pinProps?.pinData ?? {}), scrollable: true, pannable: true } }, this.Document); anchor.text = ele?.textContent ?? ''; anchor.text_html = ele?.innerHTML; addAsAnnotation && this.addDocument(anchor); @@ -264,7 +283,7 @@ export class PDFBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem !this.Document._layout_fitWidth && (this.Document._height = NumCast(this.Document._width) * (nh / nw)); }; - public search = action((searchString: string, bwd?: boolean, clear: boolean = false) => { + override search = action((searchString: string, bwd?: boolean, clear: boolean = false) => { if (!this._searching && !clear) { this._searching = true; setTimeout(() => { @@ -285,7 +304,9 @@ export class PDFBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem 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); + public gotoPage = (p: number) => { + this.Document._layout_curPage = p; + }; @undoBatch onKeyDown = action((e: KeyboardEvent) => { @@ -297,6 +318,7 @@ export class PDFBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem case 'PageUp': processed = this.backPage(); break; + default: } if (processed) { e.stopImmediatePropagation(); @@ -312,7 +334,9 @@ export class PDFBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem this._initialScrollTarget = undefined; } }; - searchStringChanged = (e: React.ChangeEvent<HTMLInputElement>) => (this._searchString = e.currentTarget.value); + searchStringChanged = (e: React.ChangeEvent<HTMLInputElement>) => { + this._searchString = e.currentTarget.value; + }; // adding external documents; to sidebar key // if (doc.Geolocation) this.addDocument(doc, this.fieldkey+"_annotation") @@ -326,7 +350,7 @@ export class PDFBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem setupMoveUpEvents( this, e, - (e, down, delta) => { + (moveEv, down, delta) => { const localDelta = this._props .ScreenToLocalTransform() .scale(this._props.NativeDimScaling?.() || 1) @@ -341,7 +365,7 @@ export class PDFBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem } return false; }, - (e, movement, isClick) => !isClick && batch.end(), + (clickEv, movement, isClick) => !isClick && batch.end(), () => { onButton && this.toggleSidebar(); batch.end(); @@ -368,11 +392,11 @@ export class PDFBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem settingsPanel() { const pageBtns = ( <> - <button className="pdfBox-backBtn" key="back" title="Page Back" onPointerDown={e => e.stopPropagation()} onClick={this.backPage}> - <FontAwesomeIcon style={{ color: 'white' }} icon={'arrow-left'} size="sm" /> + <button type="button" className="pdfBox-backBtn" key="back" title="Page Back" onPointerDown={e => e.stopPropagation()} onClick={this.backPage}> + <FontAwesomeIcon style={{ color: 'white' }} icon="arrow-left" size="sm" /> </button> - <button className="pdfBox-fwdBtn" key="fwd" title="Page Forward" onPointerDown={e => e.stopPropagation()} onClick={this.forwardPage}> - <FontAwesomeIcon style={{ color: 'white' }} icon={'arrow-right'} size="sm" /> + <button type="button" className="pdfBox-fwdBtn" key="fwd" title="Page Forward" onPointerDown={e => e.stopPropagation()} onClick={this.forwardPage}> + <FontAwesomeIcon style={{ color: 'white' }} icon="arrow-right" size="sm" /> </button> </> ); @@ -385,7 +409,7 @@ export class PDFBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem onPointerDown={e => e.stopPropagation()} 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} /> + <button type="button" className="pdfBox-overlayButton" title={searchTitle} /> <input className="pdfBox-searchBar" placeholder="Search" @@ -396,17 +420,18 @@ export class PDFBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem e.keyCode === KeyCodes.ENTER && this.search(this._searchString, e.shiftKey); }} /> - <button className="pdfBox-search" title="Search" onClick={e => this.search(this._searchString, e.shiftKey)}> + <button type="button" className="pdfBox-search" title="Search" onClick={e => this.search(this._searchString, e.shiftKey)}> <FontAwesomeIcon icon="search" size="sm" /> </button> - <button className="pdfBox-prevIcon" title="Previous Annotation" onClick={this.prevAnnotation}> - <FontAwesomeIcon icon={'arrow-up'} size="lg" /> + <button type="button" className="pdfBox-prevIcon" title="Previous Annotation" onClick={this.prevAnnotation}> + <FontAwesomeIcon icon="arrow-up" size="lg" /> </button> - <button className="pdfBox-nextIcon" title="Next Annotation" onClick={this.nextAnnotation}> - <FontAwesomeIcon icon={'arrow-down'} size="lg" /> + <button type="button" className="pdfBox-nextIcon" title="Next Annotation" onClick={this.nextAnnotation}> + <FontAwesomeIcon icon="arrow-down" size="lg" /> </button> </div> <button + type="button" className="pdfBox-overlayButton" title={searchTitle} onClick={action(() => { @@ -423,9 +448,13 @@ export class PDFBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem <input value={curPage} style={{ width: `${curPage > 99 ? 4 : 3}ch`, pointerEvents: 'all' }} - onChange={e => (this.Document._layout_curPage = Number(e.currentTarget.value))} + onChange={e => { + this.Document._layout_curPage = Number(e.currentTarget.value); + }} onKeyDown={e => e.stopPropagation()} - onClick={action(() => (this._pageControls = !this._pageControls))} + onClick={action(() => { + this._pageControls = !this._pageControls; + })} /> {this._pageControls ? pageBtns : null} </div> @@ -440,18 +469,20 @@ export class PDFBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem 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); - specificContextMenu = (e: React.MouseEvent): void => { + toggleSidebarType = () => { + this.dataDoc[this.SidebarKey + '_type_collection'] = this.dataDoc[this.SidebarKey + '_type_collection'] === CollectionViewType.Freeform ? CollectionViewType.Stacking : CollectionViewType.Freeform; + }; + specificContextMenu = (): void => { const cm = ContextMenu.Instance; const options = cm.findByDescription('Options...'); const optionItems: ContextMenuProps[] = options && 'subitems' in options ? options.subitems : []; !Doc.noviceMode && optionItems.push({ description: 'Toggle Sidebar Type', event: this.toggleSidebarType, icon: 'expand-arrows-alt' }); !Doc.noviceMode && optionItems.push({ description: 'update icon', event: () => this.pdfUrl && this.updateIcon(), icon: 'expand-arrows-alt' }); - //optionItems.push({ description: "Toggle Sidebar ", event: () => this.toggleSidebar(), icon: "expand-arrows-alt" }); + // optionItems.push({ description: "Toggle Sidebar ", event: () => this.toggleSidebar(), icon: "expand-arrows-alt" }); !options && ContextMenu.Instance.addItem({ description: 'Options...', subitems: optionItems, icon: 'asterisk' }); const help = cm.findByDescription('Help...'); const helpItems: ContextMenuProps[] = help && 'subitems' in help ? help.subitems : []; - helpItems.push({ description: 'Copy path', event: () => this.pdfUrl && Utils.CopyText(Utils.prepend('') + this.pdfUrl.url.pathname), icon: 'expand-arrows-alt' }); + helpItems.push({ description: 'Copy path', event: () => this.pdfUrl && ClientUtils.CopyText(ClientUtils.prepend('') + this.pdfUrl.url.pathname), icon: 'expand-arrows-alt' }); !help && ContextMenu.Instance.addItem({ description: 'Help...', noexpand: true, subitems: helpItems, icon: 'asterisk' }); }; @@ -469,7 +500,7 @@ export class PDFBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem anchorMenuClick = () => this._sidebarRef.current?.anchorMenuClick; @observable _showSidebar = false; @computed get SidebarShown() { - return this._showSidebar || this.layoutDoc._show_sidebar ? true : false; + return !!(this._showSidebar || this.layoutDoc._show_sidebar); } @computed get sidebarHandle() { return ( @@ -483,7 +514,7 @@ export class PDFBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem backgroundColor: this.SidebarShown ? Colors.MEDIUM_BLUE : Colors.BLACK, }} onPointerDown={e => this.sidebarBtnDown(e, true)}> - <FontAwesomeIcon style={{ color: Colors.WHITE }} icon={'comment-alt'} size="sm" /> + <FontAwesomeIcon style={{ color: Colors.WHITE }} icon="comment-alt" size="sm" /> </div> ); } @@ -514,6 +545,7 @@ export class PDFBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem return ComponentTag === CollectionStackingView ? ( <SidebarAnnos ref={this._sidebarRef} + // eslint-disable-next-line react/jsx-props-no-spreading {...this._props} Document={this.Document} layoutDoc={this.layoutDoc} @@ -527,8 +559,9 @@ export class PDFBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem removeDocument={this.removeDocument} /> ) : ( - <div onPointerDown={e => setupMoveUpEvents(this, e, returnFalse, emptyFunction, () => SelectionManager.SelectView(this.DocumentView?.()!, false), true)}> + <div onPointerDown={e => setupMoveUpEvents(this, e, returnFalse, emptyFunction, () => DocumentView.SelectView(this.DocumentView?.()!, false), true)}> <ComponentTag + // eslint-disable-next-line react/jsx-props-no-spreading {...this._props} setContentViewBox={emptyFunction} // override setContentView to do nothing NativeWidth={this.sidebarNativeWidthFunc} @@ -539,7 +572,7 @@ export class PDFBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem yPadding={0} viewField={this.SidebarKey} isAnnotationOverlay={false} - originTopLeft={true} + originTopLeft isAnyChildContentActive={this.isAnyChildContentActive} select={emptyFunction} whenChildContentsActiveChanged={this.whenChildContentsActiveChanged} @@ -548,7 +581,7 @@ export class PDFBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem addDocument={this.sidebarAddDocument} ScreenToLocalTransform={this.sidebarScreenToLocal} renderDepth={this._props.renderDepth + 1} - noSidebar={true} + noSidebar fieldKey={this.SidebarKey} /> </div> @@ -582,6 +615,7 @@ export class PDFBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem top: 0, }}> <PDFViewer + // eslint-disable-next-line react/jsx-props-no-spreading {...this._props} pdfBox={this} sidebarAddDoc={this.sidebarAddDocument} @@ -614,12 +648,26 @@ export class PDFBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem const pdfView = !this._pdf ? null : this.renderPdfView; const href = this.pdfUrl?.url.href; if (!pdfView && href) { - if (PDFBox.pdfcache.get(href)) setTimeout(action(() => (this._pdf = PDFBox.pdfcache.get(href)))); + if (PDFBox.pdfcache.get(href)) + setTimeout( + action(() => { + this._pdf = PDFBox.pdfcache.get(href); + }) + ); else { if (!PDFBox.pdfpromise.get(href)) PDFBox.pdfpromise.set(href, Pdfjs.getDocument(href).promise); - PDFBox.pdfpromise.get(href)?.then(action((pdf: any) => PDFBox.pdfcache.set(href, (this._pdf = pdf)))); + PDFBox.pdfpromise.get(href)?.then( + action((pdf: any) => { + PDFBox.pdfcache.set(href, (this._pdf = pdf)); + }) + ); } } return pdfView ?? this.renderTitleBox; } } + +Docs.Prototypes.TemplateMap.set(DocumentType.PDF, { + layout: { view: PDFBox, dataField: 'data' }, + options: { acl: '', _layout_curPage: 1, _layout_fitWidth: true, _layout_nativeDimEditable: true, _layout_reflowVertical: true, systemIcon: 'BsFileEarmarkPdfFill' }, +}); diff --git a/src/client/views/nodes/PhysicsBox/PhysicsSimulationBox.tsx b/src/client/views/nodes/PhysicsBox/PhysicsSimulationBox.tsx index ae674d604..f88eb3bca 100644 --- a/src/client/views/nodes/PhysicsBox/PhysicsSimulationBox.tsx +++ b/src/client/views/nodes/PhysicsBox/PhysicsSimulationBox.tsx @@ -1,3 +1,11 @@ +/* eslint-disable camelcase */ +/* eslint-disable jsx-a11y/control-has-associated-label */ +/* eslint-disable @typescript-eslint/no-unused-vars */ +/* eslint-disable jsx-a11y/no-noninteractive-element-interactions */ +/* eslint-disable jsx-a11y/click-events-have-key-events */ +/* eslint-disable react/no-array-index-key */ +/* eslint-disable react/jsx-props-no-spreading */ +/* eslint-disable no-return-assign */ import ArrowLeftIcon from '@mui/icons-material/ArrowLeft'; import ArrowRightIcon from '@mui/icons-material/ArrowRight'; import PauseIcon from '@mui/icons-material/Pause'; @@ -13,13 +21,15 @@ import { NumListCast } from '../../../../fields/Doc'; import { List } from '../../../../fields/List'; import { BoolCast, NumCast, StrCast } from '../../../../fields/Types'; import { ViewBoxAnnotatableComponent } from '../../DocComponent'; -import { FieldView, FieldViewProps } from './../FieldView'; +import { FieldView, FieldViewProps } from '../FieldView'; import './PhysicsSimulationBox.scss'; import InputField from './PhysicsSimulationInputField'; import questions from './PhysicsSimulationQuestions.json'; import tutorials from './PhysicsSimulationTutorial.json'; import Wall from './PhysicsSimulationWall'; import Weight from './PhysicsSimulationWeight'; +import { Docs } from '../../../documents/Documents'; +import { DocumentType } from '../../../documents/DocumentTypes'; interface IWallProps { length: number; @@ -204,7 +214,7 @@ export class PhysicsSimulationBox extends ViewBoxAnnotatableComponent<FieldViewP componentDidUpdate(prevProps: Readonly<FieldViewProps>) { super.componentDidUpdate(prevProps); - if (this.xMax !== this._props.PanelWidth() * 0.6 || this.yMax != this._props.PanelHeight()) { + if (this.xMax !== this._props.PanelWidth() * 0.6 || this.yMax !== this._props.PanelHeight()) { this.xMax = this._props.PanelWidth() * 0.6; this.yMax = this._props.PanelHeight(); this.setupSimulation(); @@ -219,16 +229,16 @@ export class PhysicsSimulationBox extends ViewBoxAnnotatableComponent<FieldViewP @action setupSimulation = () => { - const simulationType = this.simulationType; + const { simulationType } = this; const mode = this.simulationMode; this.dataDoc.simulation_paused = true; - if (simulationType != 'Circular Motion') { + if (simulationType !== 'Circular Motion') { this.dataDoc.mass1_velocityXstart = 0; this.dataDoc.mass1_velocityYstart = 0; this.dataDoc.mass1_velocityX = 0; this.dataDoc.mass1_velocityY = 0; } - if (mode == 'Freeform') { + if (mode === 'Freeform') { this.dataDoc.simulation_showForceMagnitudes = true; // prettier-ignore switch (simulationType) { @@ -247,9 +257,10 @@ export class PhysicsSimulationBox extends ViewBoxAnnotatableComponent<FieldViewP case 'Circular Motion': this.setupCircular(20); break; case 'Pulley': this.setupPulley(); break; case 'Suspension': this.setupSuspension();break; + default: } this._simReset++; - } else if (mode == 'Review') { + } else if (mode === 'Review') { this.dataDoc.simulation_showComponentForces = false; this.dataDoc.simulation_showForceMagnitudes = true; this.dataDoc.simulation_showAcceleration = false; @@ -265,12 +276,13 @@ export class PhysicsSimulationBox extends ViewBoxAnnotatableComponent<FieldViewP case 'Circular Motion': this.setupCircular(0); break; // TODO - circular motion review problems case 'Pulley': this.setupPulley(); break; // TODO - pulley tutorial review problems case 'Suspension': this.setupSuspension(); break; // TODO - suspension tutorial review problems + default: } - } else if (mode == 'Tutorial') { + } else if (mode === 'Tutorial') { this.dataDoc.simulation_showComponentForces = false; this.dataDoc.tutorial_stepNumber = 0; this.dataDoc.simulation_showAcceleration = false; - if (this.simulationType != 'Circular Motion') { + if (this.simulationType !== 'Circular Motion') { this.dataDoc.mass1_velocityX = 0; this.dataDoc.mass1_velocityY = 0; this.dataDoc.simulation_showVelocity = false; @@ -333,6 +345,7 @@ export class PhysicsSimulationBox extends ViewBoxAnnotatableComponent<FieldViewP this.dataDoc.mass1_forcesStart = JSON.stringify(tutorials.suspension.steps[0].forces); this.dataDoc.simulation_showForceMagnitudes = tutorials.suspension.steps[0].showMagnitude; break; + default: } this._simReset++; } @@ -349,7 +362,7 @@ export class PhysicsSimulationBox extends ViewBoxAnnotatableComponent<FieldViewP magnitude: Math.abs(this.gravity) * Math.cos(Math.atan(height / width)) * this.mass1, directionInDegrees: 180 - 90 - (Math.atan(height / width) * 180) / Math.PI, }; - let frictionForce: IForce = { + const frictionForce: IForce = { description: 'Static Friction Force', magnitude: coefficient * Math.abs(this.gravity) * Math.cos(Math.atan(height / width)) * this.mass1, directionInDegrees: 180 - (Math.atan(height / width) * 180) / Math.PI, @@ -378,7 +391,7 @@ export class PhysicsSimulationBox extends ViewBoxAnnotatableComponent<FieldViewP directionInDegrees: 360 - (Math.atan(height / width) * 180) / Math.PI, }; const gravityForce = this.gravityForce(this.mass1); - if (coefficient != 0) { + if (coefficient !== 0) { this.dataDoc.mass1_forcesStart = JSON.stringify([gravityForce, normalForce, frictionForce]); this.dataDoc.mass1_forcesUpdated = JSON.stringify([gravityForce, normalForce, frictionForce]); this.dataDoc.mass1_componentForces = JSON.stringify([frictionForce, normalForceComponent, gravityParallel, gravityPerpendicular]); @@ -396,12 +409,12 @@ export class PhysicsSimulationBox extends ViewBoxAnnotatableComponent<FieldViewP this.dataDoc.wedge_height = Math.tan(radAng) * this.dataDoc.wedge_width; // update weight position based on updated wedge width/height - let yPos = this.yMax - this.dataDoc.wedge_height - this.mass1Radius * Math.cos(radAng) - this.mass1Radius; - let xPos = this.xMax * 0.25 + this.mass1Radius * Math.sin(radAng) - this.mass1Radius; + const yPos = this.yMax - this.dataDoc.wedge_height - this.mass1Radius * Math.cos(radAng) - this.mass1Radius; + const xPos = this.xMax * 0.25 + this.mass1Radius * Math.sin(radAng) - this.mass1Radius; this.dataDoc.mass1_positionXstart = xPos; this.dataDoc.mass1_positionYstart = yPos; - if (this.simulationMode == 'Freeform') { + if (this.simulationMode === 'Freeform') { this.updateForcesWithFriction(NumCast(this.dataDoc.coefficientOfStaticFriction), this.dataDoc.wedge_width, Math.tan(radAng) * this.dataDoc.wedge_width); } }; @@ -409,7 +422,7 @@ export class PhysicsSimulationBox extends ViewBoxAnnotatableComponent<FieldViewP // In review mode, update forces when coefficient of static friction changed updateReviewForcesBasedOnCoefficient = (coefficient: number) => { let theta = this.wedgeAngle; - let index = this.selectedQuestion.variablesForQuestionSetup.indexOf('theta - max 45'); + const index = this.selectedQuestion.variablesForQuestionSetup.indexOf('theta - max 45'); if (index >= 0) { theta = NumListCast(this.dataDoc.questionVariables)[index]; } @@ -467,26 +480,26 @@ export class PhysicsSimulationBox extends ViewBoxAnnotatableComponent<FieldViewP const description = question.answerSolutionDescriptions[i]; if (!isNaN(NumCast(description))) { solutions.push(NumCast(description)); - } else if (description == 'solve normal force angle from wedge angle') { + } else if (description === 'solve normal force angle from wedge angle') { solutions.push(90 - theta); - } else if (description == 'solve normal force magnitude from wedge angle') { + } else if (description === 'solve normal force magnitude from wedge angle') { solutions.push(Math.abs(this.gravity) * Math.cos((theta / 180) * Math.PI)); - } else if (description == 'solve static force magnitude from wedge angle given equilibrium') { - let normalForceMagnitude = Math.abs(this.gravity) * Math.cos((theta / 180) * Math.PI); - let normalForceAngle = 90 - theta; - let frictionForceAngle = 180 - theta; - let frictionForceMagnitude = (-normalForceMagnitude * Math.sin((normalForceAngle * Math.PI) / 180) + Math.abs(this.gravity)) / Math.sin((frictionForceAngle * Math.PI) / 180); + } else if (description === 'solve static force magnitude from wedge angle given equilibrium') { + const normalForceMagnitude = Math.abs(this.gravity) * Math.cos((theta / 180) * Math.PI); + const normalForceAngle = 90 - theta; + const frictionForceAngle = 180 - theta; + const frictionForceMagnitude = (-normalForceMagnitude * Math.sin((normalForceAngle * Math.PI) / 180) + Math.abs(this.gravity)) / Math.sin((frictionForceAngle * Math.PI) / 180); solutions.push(frictionForceMagnitude); - } else if (description == 'solve static force angle from wedge angle given equilibrium') { + } else if (description === 'solve static force angle from wedge angle given equilibrium') { solutions.push(180 - theta); - } else if (description == 'solve minimum static coefficient from wedge angle given equilibrium') { - let normalForceMagnitude = Math.abs(this.gravity) * Math.cos((theta / 180) * Math.PI); - let normalForceAngle = 90 - theta; - let frictionForceAngle = 180 - theta; - let frictionForceMagnitude = (-normalForceMagnitude * Math.sin((normalForceAngle * Math.PI) / 180) + Math.abs(this.gravity)) / Math.sin((frictionForceAngle * Math.PI) / 180); - let frictionCoefficient = frictionForceMagnitude / normalForceMagnitude; + } else if (description === 'solve minimum static coefficient from wedge angle given equilibrium') { + const normalForceMagnitude = Math.abs(this.gravity) * Math.cos((theta / 180) * Math.PI); + const normalForceAngle = 90 - theta; + const frictionForceAngle = 180 - theta; + const frictionForceMagnitude = (-normalForceMagnitude * Math.sin((normalForceAngle * Math.PI) / 180) + Math.abs(this.gravity)) / Math.sin((frictionForceAngle * Math.PI) / 180); + const frictionCoefficient = frictionForceMagnitude / normalForceMagnitude; solutions.push(frictionCoefficient); - } else if (description == 'solve maximum wedge angle from coefficient of static friction given equilibrium') { + } else if (description === 'solve maximum wedge angle from coefficient of static friction given equilibrium') { solutions.push((Math.atan(muS) * 180) / Math.PI); } } @@ -497,38 +510,38 @@ export class PhysicsSimulationBox extends ViewBoxAnnotatableComponent<FieldViewP // In review mode, check if input answers match correct answers and optionally generate alert checkAnswers = (showAlert: boolean = true) => { let error: boolean = false; - let epsilon: number = 0.01; + const epsilon: number = 0.01; if (this.selectedQuestion) { for (let i = 0; i < this.selectedQuestion.answerParts.length; i++) { - if (this.selectedQuestion.answerParts[i] == 'force of gravity') { + if (this.selectedQuestion.answerParts[i] === 'force of gravity') { if (Math.abs(NumCast(this.dataDoc.review_GravityMagnitude) - this.selectedSolutions[i]) > epsilon) { error = true; } - } else if (this.selectedQuestion.answerParts[i] == 'angle of gravity') { + } else if (this.selectedQuestion.answerParts[i] === 'angle of gravity') { if (Math.abs(NumCast(this.dataDoc.review_GravityAngle) - this.selectedSolutions[i]) > epsilon) { error = true; } - } else if (this.selectedQuestion.answerParts[i] == 'normal force') { + } else if (this.selectedQuestion.answerParts[i] === 'normal force') { if (Math.abs(NumCast(this.dataDoc.review_NormalMagnitude) - this.selectedSolutions[i]) > epsilon) { error = true; } - } else if (this.selectedQuestion.answerParts[i] == 'angle of normal force') { + } else if (this.selectedQuestion.answerParts[i] === 'angle of normal force') { if (Math.abs(NumCast(this.dataDoc.review_NormalAngle) - this.selectedSolutions[i]) > epsilon) { error = true; } - } else if (this.selectedQuestion.answerParts[i] == 'force of static friction') { + } else if (this.selectedQuestion.answerParts[i] === 'force of static friction') { if (Math.abs(NumCast(this.dataDoc.review_StaticMagnitude) - this.selectedSolutions[i]) > epsilon) { error = true; } - } else if (this.selectedQuestion.answerParts[i] == 'angle of static friction') { + } else if (this.selectedQuestion.answerParts[i] === 'angle of static friction') { if (Math.abs(NumCast(this.dataDoc.review_StaticAngle) - this.selectedSolutions[i]) > epsilon) { error = true; } - } else if (this.selectedQuestion.answerParts[i] == 'coefficient of static friction') { + } else if (this.selectedQuestion.answerParts[i] === 'coefficient of static friction') { if (Math.abs(NumCast(this.dataDoc.coefficientOfStaticFriction) - this.selectedSolutions[i]) > epsilon) { error = true; } - } else if (this.selectedQuestion.answerParts[i] == 'wedge angle') { + } else if (this.selectedQuestion.answerParts[i] === 'wedge angle') { if (Math.abs(this.wedgeAngle - this.selectedSolutions[i]) > epsilon) { error = true; } @@ -539,7 +552,7 @@ export class PhysicsSimulationBox extends ViewBoxAnnotatableComponent<FieldViewP this.dataDoc.simulation_paused = false; setTimeout(() => (this.dataDoc.simulation_paused = true), 3000); } - if (this.selectedQuestion.goal == 'noMovement') { + if (this.selectedQuestion.goal === 'noMovement') { this.dataDoc.noMovement = !error; } }; @@ -571,12 +584,12 @@ export class PhysicsSimulationBox extends ViewBoxAnnotatableComponent<FieldViewP let wedge_angle = 0; for (let i = 0; i < question.variablesForQuestionSetup.length; i++) { - if (question.variablesForQuestionSetup[i] == 'theta - max 45') { - let randValue = Math.floor(Math.random() * 44 + 1); + if (question.variablesForQuestionSetup[i] === 'theta - max 45') { + const randValue = Math.floor(Math.random() * 44 + 1); vars.push(randValue); wedge_angle = randValue; - } else if (question.variablesForQuestionSetup[i] == 'coefficient of static friction') { - let randValue = Math.round(Math.random() * 1000) / 1000; + } else if (question.variablesForQuestionSetup[i] === 'coefficient of static friction') { + const randValue = Math.round(Math.random() * 1000) / 1000; vars.push(randValue); coefficient = randValue; } @@ -589,7 +602,7 @@ export class PhysicsSimulationBox extends ViewBoxAnnotatableComponent<FieldViewP let q = ''; for (let i = 0; i < question.questionSetup.length; i++) { q += question.questionSetup[i]; - if (i != question.questionSetup.length - 1) { + if (i !== question.questionSetup.length - 1) { q += vars[i]; if (question.variablesForQuestionSetup[i].includes('theta')) { q += ' degree (≈' + Math.round((1000 * (vars[i] * Math.PI)) / 180) / 1000 + ' rad)'; @@ -601,7 +614,7 @@ export class PhysicsSimulationBox extends ViewBoxAnnotatableComponent<FieldViewP this.dataDoc.questionPartOne = q; this.dataDoc.questionPartTwo = question.question; this.dataDoc.answers = new List<number>(this.getAnswersToQuestion(question, vars)); - //this.dataDoc.simulation_reset = (!this.dataDoc.simulation_reset); + // this.dataDoc.simulation_reset = (!this.dataDoc.simulation_reset); }; // Default setup for uniform circular motion simulation @@ -610,8 +623,8 @@ export class PhysicsSimulationBox extends ViewBoxAnnotatableComponent<FieldViewP this.dataDoc.simulation_showComponentForces = false; this.dataDoc.mass1_velocityYstart = 0; this.dataDoc.mass1_velocityXstart = value; - let xPos = (this.xMax + this.xMin) / 2 - this.mass1Radius; - let yPos = (this.yMax + this.yMin) / 2 + this.circularMotionRadius - this.mass1Radius; + const xPos = (this.xMax + this.xMin) / 2 - this.mass1Radius; + const yPos = (this.yMax + this.yMin) / 2 + this.circularMotionRadius - this.mass1Radius; this.dataDoc.mass1_positionYstart = yPos; this.dataDoc.mass1_positionXstart = xPos; const tensionForce: IForce = { @@ -680,13 +693,13 @@ export class PhysicsSimulationBox extends ViewBoxAnnotatableComponent<FieldViewP // Default setup for suspension simulation @action setupSuspension = () => { - let xPos = (this.xMax + this.xMin) / 2 - this.mass1Radius; - let yPos = this.yMin + 200; + const xPos = (this.xMax + this.xMin) / 2 - this.mass1Radius; + const yPos = this.yMin + 200; this.dataDoc.mass1_positionYstart = yPos; this.dataDoc.mass1_positionXstart = xPos; this.dataDoc.mass1_positionY = this.getDisplayYPos(yPos); this.dataDoc.mass1_positionX = xPos; - let tensionMag = (this.mass1 * Math.abs(this.gravity)) / (2 * Math.sin(Math.PI / 4)); + const tensionMag = (this.mass1 * Math.abs(this.gravity)) / (2 * Math.sin(Math.PI / 4)); const tensionForce1: IForce = { description: 'Tension', magnitude: tensionMag, @@ -891,7 +904,7 @@ export class PhysicsSimulationBox extends ViewBoxAnnotatableComponent<FieldViewP setVelocity={this.setVelocity1} setAcceleration={this.setAcceleration1} /> - {this.simulationType == 'Pulley' && ( + {this.simulationType === 'Pulley' && ( <Weight {...commonWeightProps} color="green" @@ -916,7 +929,7 @@ export class PhysicsSimulationBox extends ViewBoxAnnotatableComponent<FieldViewP )} </div> <div style={{ position: 'absolute', transformOrigin: 'top left', top: 0, left: 0, width: '100%', height: '100%' }}> - {(this.simulationType == 'One Weight' || this.simulationType == 'Inclined Plane') && + {(this.simulationType === 'One Weight' || this.simulationType === 'Inclined Plane') && this.wallPositions?.map((element, index) => <Wall key={index} length={element.length} xPos={element.xPos} yPos={element.yPos} angleInDegrees={element.angleInDegrees} />)} </div> </div> @@ -927,17 +940,17 @@ export class PhysicsSimulationBox extends ViewBoxAnnotatableComponent<FieldViewP style={{ overflow: 'auto', height: `${Math.max(1, 800 / this._props.PanelWidth()) * 100}%`, transform: `scale(${Math.min(1, this._props.PanelWidth() / 850)})` }}> <div className="mechanicsSimulationControls"> <Stack direction="row" spacing={1}> - {this.dataDoc.simulation_paused && this.simulationMode != 'Tutorial' && ( + {this.dataDoc.simulation_paused && this.simulationMode !== 'Tutorial' && ( <IconButton onClick={() => (this.dataDoc.simulation_paused = false)}> <PlayArrowIcon /> </IconButton> )} - {!this.dataDoc.simulation_paused && this.simulationMode != 'Tutorial' && ( + {!this.dataDoc.simulation_paused && this.simulationMode !== 'Tutorial' && ( <IconButton onClick={() => (this.dataDoc.simulation_paused = true)}> <PauseIcon /> </IconButton> )} - {this.dataDoc.simulation_paused && this.simulationMode != 'Tutorial' && ( + {this.dataDoc.simulation_paused && this.simulationMode !== 'Tutorial' && ( <IconButton onClick={action(() => this._simReset++)}> <ReplayIcon /> </IconButton> @@ -974,15 +987,13 @@ export class PhysicsSimulationBox extends ViewBoxAnnotatableComponent<FieldViewP </select> </div> </div> - {this.simulationMode == 'Review' && this.simulationType != 'Inclined Plane' && ( + {this.simulationMode === 'Review' && this.simulationType !== 'Inclined Plane' && ( <div className="wordProblemBox"> - <p> - <>{this.simulationType} review problems in progress!</> - </p> + <p>{this.simulationType} review problems in progress!</p> <hr /> </div> )} - {this.simulationMode == 'Review' && this.simulationType == 'Inclined Plane' && ( + {this.simulationMode === 'Review' && this.simulationType === 'Inclined Plane' && ( <div> {!this.dataDoc.hintDialogueOpen && ( <IconButton @@ -995,7 +1006,7 @@ export class PhysicsSimulationBox extends ViewBoxAnnotatableComponent<FieldViewP <QuestionMarkIcon /> </IconButton> )} - <Dialog maxWidth={'sm'} fullWidth={true} open={BoolCast(this.dataDoc.hintDialogueOpen)} onClose={() => (this.dataDoc.hintDialogueOpen = false)}> + <Dialog maxWidth="sm" fullWidth open={BoolCast(this.dataDoc.hintDialogueOpen)} onClose={() => (this.dataDoc.hintDialogueOpen = false)}> <DialogTitle>Hints</DialogTitle> <DialogContent> {this.selectedQuestion.hints?.map((hint: any, index: number) => ( @@ -1030,12 +1041,12 @@ export class PhysicsSimulationBox extends ViewBoxAnnotatableComponent<FieldViewP dataDoc={this.dataDoc} prop="review_GravityMagnitude" step={0.1} - unit={'N'} + unit="N" upperBound={50} value={NumCast(this.dataDoc.review_GravityMagnitude)} showIcon={BoolCast(this.dataDoc.simulation_showIcon)} correctValue={NumListCast(this.dataDoc.answers)[this.selectedQuestion.answerParts.indexOf('force of gravity')]} - labelWidth={'7em'} + labelWidth="7em" /> )} {this.selectedQuestion.answerParts.includes('angle of gravity') && ( @@ -1045,13 +1056,13 @@ export class PhysicsSimulationBox extends ViewBoxAnnotatableComponent<FieldViewP dataDoc={this.dataDoc} prop="review_GravityAngle" step={1} - unit={'°'} + unit="°" upperBound={360} value={NumCast(this.dataDoc.review_GravityAngle)} - radianEquivalent={true} + radianEquivalent showIcon={BoolCast(this.dataDoc.simulation_showIcon)} correctValue={NumListCast(this.dataDoc.answers)[this.selectedQuestion.answerParts.indexOf('angle of gravity')]} - labelWidth={'7em'} + labelWidth="7em" /> )} {this.selectedQuestion.answerParts.includes('normal force') && ( @@ -1061,12 +1072,12 @@ export class PhysicsSimulationBox extends ViewBoxAnnotatableComponent<FieldViewP dataDoc={this.dataDoc} prop="review_NormalMagnitude" step={0.1} - unit={'N'} + unit="N" upperBound={50} value={NumCast(this.dataDoc.review_NormalMagnitude)} showIcon={BoolCast(this.dataDoc.simulation_showIcon)} correctValue={NumListCast(this.dataDoc.answers)[this.selectedQuestion.answerParts.indexOf('normal force')]} - labelWidth={'7em'} + labelWidth="7em" /> )} {this.selectedQuestion.answerParts.includes('angle of normal force') && ( @@ -1076,13 +1087,13 @@ export class PhysicsSimulationBox extends ViewBoxAnnotatableComponent<FieldViewP dataDoc={this.dataDoc} prop="review_NormalAngle" step={1} - unit={'°'} + unit="°" upperBound={360} value={NumCast(this.dataDoc.review_NormalAngle)} - radianEquivalent={true} + radianEquivalent showIcon={BoolCast(this.dataDoc.simulation_showIcon)} correctValue={NumListCast(this.dataDoc.answers)[this.selectedQuestion.answerParts.indexOf('angle of normal force')]} - labelWidth={'7em'} + labelWidth="7em" /> )} {this.selectedQuestion.answerParts.includes('force of static friction') && ( @@ -1092,12 +1103,12 @@ export class PhysicsSimulationBox extends ViewBoxAnnotatableComponent<FieldViewP dataDoc={this.dataDoc} prop="review_StaticMagnitude" step={0.1} - unit={'N'} + unit="N" upperBound={50} value={NumCast(this.dataDoc.review_StaticMagnitude)} showIcon={BoolCast(this.dataDoc.simulation_showIcon)} correctValue={NumListCast(this.dataDoc.answers)[this.selectedQuestion.answerParts.indexOf('force of static friction')]} - labelWidth={'7em'} + labelWidth="7em" /> )} {this.selectedQuestion.answerParts.includes('angle of static friction') && ( @@ -1107,13 +1118,13 @@ export class PhysicsSimulationBox extends ViewBoxAnnotatableComponent<FieldViewP dataDoc={this.dataDoc} prop="review_StaticAngle" step={1} - unit={'°'} + unit="°" upperBound={360} value={NumCast(this.dataDoc.review_StaticAngle)} - radianEquivalent={true} + radianEquivalent showIcon={BoolCast(this.dataDoc.simulation_showIcon)} correctValue={NumListCast(this.dataDoc.answers)[this.selectedQuestion.answerParts.indexOf('angle of static friction')]} - labelWidth={'7em'} + labelWidth="7em" /> )} {this.selectedQuestion.answerParts.includes('coefficient of static friction') && ( @@ -1127,7 +1138,7 @@ export class PhysicsSimulationBox extends ViewBoxAnnotatableComponent<FieldViewP dataDoc={this.dataDoc} prop="coefficientOfStaticFriction" step={0.1} - unit={''} + unit="" upperBound={1} value={NumCast(this.dataDoc.coefficientOfStaticFriction)} effect={this.updateReviewForcesBasedOnCoefficient} @@ -1142,14 +1153,14 @@ export class PhysicsSimulationBox extends ViewBoxAnnotatableComponent<FieldViewP dataDoc={this.dataDoc} prop="wedge_angle" step={1} - unit={'°'} + unit="°" upperBound={49} value={this.wedgeAngle} effect={(val: number) => { this.changeWedgeBasedOnNewAngle(val); this.updateReviewForcesBasedOnAngle(val); }} - radianEquivalent={true} + radianEquivalent showIcon={BoolCast(this.dataDoc.simulation_showIcon)} correctValue={NumListCast(this.dataDoc.answers)[this.selectedQuestion.answerParts.indexOf('wedge angle')]} /> @@ -1158,7 +1169,7 @@ export class PhysicsSimulationBox extends ViewBoxAnnotatableComponent<FieldViewP </div> </div> )} - {this.simulationMode == 'Tutorial' && ( + {this.simulationMode === 'Tutorial' && ( <div className="wordProblemBox"> <div className="question"> <h2>Problem</h2> @@ -1180,7 +1191,7 @@ export class PhysicsSimulationBox extends ViewBoxAnnotatableComponent<FieldViewP this.dataDoc.mass1_forcesUpdated = JSON.stringify(this.tutorial.steps[step].forces); this.dataDoc.simulation_showForceMagnitudes = this.tutorial.steps[step].showMagnitude; }} - disabled={this.dataDoc.tutorial_stepNumber == 0}> + disabled={this.dataDoc.tutorial_stepNumber === 0}> <ArrowLeftIcon /> </IconButton> <div> @@ -1204,8 +1215,8 @@ export class PhysicsSimulationBox extends ViewBoxAnnotatableComponent<FieldViewP </IconButton> </div> <div> - {(this.simulationType == 'One Weight' || this.simulationType == 'Inclined Plane' || this.simulationType == 'Pendulum') && <p>Resources</p>} - {this.simulationType == 'One Weight' && ( + {(this.simulationType === 'One Weight' || this.simulationType === 'Inclined Plane' || this.simulationType === 'Pendulum') && <p>Resources</p>} + {this.simulationType === 'One Weight' && ( <ul> <li> <a @@ -1233,7 +1244,7 @@ export class PhysicsSimulationBox extends ViewBoxAnnotatableComponent<FieldViewP </li> </ul> )} - {this.simulationType == 'Inclined Plane' && ( + {this.simulationType === 'Inclined Plane' && ( <ul> <li> <a @@ -1261,7 +1272,7 @@ export class PhysicsSimulationBox extends ViewBoxAnnotatableComponent<FieldViewP </li> </ul> )} - {this.simulationType == 'Pendulum' && ( + {this.simulationType === 'Pendulum' && ( <ul> <li> <a @@ -1280,7 +1291,7 @@ export class PhysicsSimulationBox extends ViewBoxAnnotatableComponent<FieldViewP </div> </div> )} - {this.simulationMode == 'Review' && this.simulationType == 'Inclined Plane' && ( + {this.simulationMode === 'Review' && this.simulationType === 'Inclined Plane' && ( <div style={{ display: 'flex', @@ -1318,11 +1329,11 @@ export class PhysicsSimulationBox extends ViewBoxAnnotatableComponent<FieldViewP </div> </div> )} - {this.simulationMode == 'Freeform' && ( + {this.simulationMode === 'Freeform' && ( <div className="vars"> <FormControl component="fieldset"> <FormGroup> - {this.simulationType == 'One Weight' && ( + {this.simulationType === 'One Weight' && ( <FormControlLabel control={<Checkbox checked={BoolCast(this.dataDoc.elasticCollisions)} onChange={() => (this.dataDoc.elasticCollisions = !this.dataDoc.elasticCollisions)} />} label="Make collisions elastic" @@ -1334,7 +1345,7 @@ export class PhysicsSimulationBox extends ViewBoxAnnotatableComponent<FieldViewP label="Show force vectors" labelPlacement="start" /> - {(this.simulationType == 'Inclined Plane' || this.simulationType == 'Pendulum') && ( + {(this.simulationType === 'Inclined Plane' || this.simulationType === 'Pendulum') && ( <FormControlLabel control={<Checkbox checked={BoolCast(this.dataDoc.simulation_showComponentForces)} onChange={() => (this.dataDoc.simulation_showComponentForces = !this.dataDoc.simulation_showComponentForces)} />} label="Show component force vectors" @@ -1351,80 +1362,80 @@ export class PhysicsSimulationBox extends ViewBoxAnnotatableComponent<FieldViewP label="Show velocity vector" labelPlacement="start" /> - <InputField label={<Box>Speed</Box>} lowerBound={1} dataDoc={this.dataDoc} prop="simulation_speed" step={1} unit={'x'} upperBound={10} value={NumCast(this.dataDoc.simulation_speed, 2)} labelWidth={'5em'} /> - {this.dataDoc.simulation_paused && this.simulationType != 'Circular Motion' && ( + <InputField label={<Box>Speed</Box>} lowerBound={1} dataDoc={this.dataDoc} prop="simulation_speed" step={1} unit="x" upperBound={10} value={NumCast(this.dataDoc.simulation_speed, 2)} labelWidth="5em" /> + {this.dataDoc.simulation_paused && this.simulationType !== 'Circular Motion' && ( <InputField label={<Box>Gravity</Box>} lowerBound={-30} dataDoc={this.dataDoc} prop="gravity" step={0.01} - unit={'m/s2'} + unit="m/s2" upperBound={0} value={NumCast(this.dataDoc.simulation_gravity, -9.81)} effect={(val: number) => this.setupSimulation()} - labelWidth={'5em'} + labelWidth="5em" /> )} - {this.dataDoc.simulation_paused && this.simulationType != 'Pulley' && ( + {this.dataDoc.simulation_paused && this.simulationType !== 'Pulley' && ( <InputField label={<Box>Mass</Box>} lowerBound={1} dataDoc={this.dataDoc} prop="mass1" step={0.1} - unit={'kg'} + unit="kg" upperBound={5} value={this.mass1 ?? 1} effect={(val: number) => this.setupSimulation()} - labelWidth={'5em'} + labelWidth="5em" /> )} - {this.dataDoc.simulation_paused && this.simulationType == 'Pulley' && ( + {this.dataDoc.simulation_paused && this.simulationType === 'Pulley' && ( <InputField label={<Box>Red mass</Box>} lowerBound={1} dataDoc={this.dataDoc} prop="mass1" step={0.1} - unit={'kg'} + unit="kg" upperBound={5} value={this.mass1 ?? 1} effect={(val: number) => this.setupSimulation()} - labelWidth={'5em'} + labelWidth="5em" /> )} - {this.dataDoc.simulation_paused && this.simulationType == 'Pulley' && ( + {this.dataDoc.simulation_paused && this.simulationType === 'Pulley' && ( <InputField label={<Box>Blue mass</Box>} lowerBound={1} dataDoc={this.dataDoc} prop="mass2" step={0.1} - unit={'kg'} + unit="kg" upperBound={5} value={this.mass2 ?? 1} effect={(val: number) => this.setupSimulation()} - labelWidth={'5em'} + labelWidth="5em" /> )} - {this.dataDoc.simulation_paused && this.simulationType == 'Circular Motion' && ( + {this.dataDoc.simulation_paused && this.simulationType === 'Circular Motion' && ( <InputField label={<Box>Rod length</Box>} lowerBound={100} dataDoc={this.dataDoc} prop="circularMotionRadius" step={5} - unit={'m'} + unit="m" upperBound={250} value={this.circularMotionRadius} effect={(val: number) => this.setupSimulation()} - labelWidth={'5em'} + labelWidth="5em" /> )} </FormGroup> </FormControl> - {this.simulationType == 'Spring' && this.dataDoc.simulation_paused && ( + {this.simulationType === 'Spring' && this.dataDoc.simulation_paused && ( <div> <InputField label={<Typography color="inherit">Spring stiffness</Typography>} @@ -1432,13 +1443,13 @@ export class PhysicsSimulationBox extends ViewBoxAnnotatableComponent<FieldViewP dataDoc={this.dataDoc} prop="spring_constant" step={1} - unit={'N/m'} + unit="N/m" upperBound={500} value={this.springConstant} effect={action(() => this._simReset++)} radianEquivalent={false} - mode={'Freeform'} - labelWidth={'7em'} + mode="Freeform" + labelWidth="7em" /> <InputField label={<Typography color="inherit">Rest length</Typography>} @@ -1452,7 +1463,7 @@ export class PhysicsSimulationBox extends ViewBoxAnnotatableComponent<FieldViewP effect={action(() => this._simReset++)} radianEquivalent={false} mode="Freeform" - labelWidth={'7em'} + labelWidth="7em" /> <InputField label={<Typography color="inherit">Starting displacement</Typography>} @@ -1470,11 +1481,11 @@ export class PhysicsSimulationBox extends ViewBoxAnnotatableComponent<FieldViewP })} radianEquivalent={false} mode="Freeform" - labelWidth={'7em'} + labelWidth="7em" /> </div> )} - {this.simulationType == 'Inclined Plane' && this.dataDoc.simulation_paused && ( + {this.simulationType === 'Inclined Plane' && this.dataDoc.simulation_paused && ( <div> <InputField label={<Box>θ</Box>} @@ -1482,16 +1493,16 @@ export class PhysicsSimulationBox extends ViewBoxAnnotatableComponent<FieldViewP dataDoc={this.dataDoc} prop="wedge_angle" step={1} - unit={'°'} + unit="°" upperBound={49} value={this.wedgeAngle} effect={action((val: number) => { this.changeWedgeBasedOnNewAngle(val); this._simReset++; })} - radianEquivalent={true} - mode={'Freeform'} - labelWidth={'2em'} + radianEquivalent + mode="Freeform" + labelWidth="2em" /> <InputField label={ @@ -1503,7 +1514,7 @@ export class PhysicsSimulationBox extends ViewBoxAnnotatableComponent<FieldViewP dataDoc={this.dataDoc} prop="coefficientOfStaticFriction" step={0.1} - unit={''} + unit="" upperBound={1} value={NumCast(this.dataDoc.coefficientOfStaticFriction) ?? 0} effect={action((val: number) => { @@ -1513,8 +1524,8 @@ export class PhysicsSimulationBox extends ViewBoxAnnotatableComponent<FieldViewP } this._simReset++; })} - mode={'Freeform'} - labelWidth={'2em'} + mode="Freeform" + labelWidth="2em" /> <InputField label={ @@ -1526,16 +1537,16 @@ export class PhysicsSimulationBox extends ViewBoxAnnotatableComponent<FieldViewP dataDoc={this.dataDoc} prop="coefficientOfKineticFriction" step={0.1} - unit={''} + unit="" upperBound={NumCast(this.dataDoc.coefficientOfStaticFriction)} value={NumCast(this.dataDoc.coefficientOfKineticFriction) ?? 0} effect={action(() => this._simReset++)} - mode={'Freeform'} - labelWidth={'2em'} + mode="Freeform" + labelWidth="2em" /> </div> )} - {this.simulationType == 'Inclined Plane' && !this.dataDoc.simulation_paused && ( + {this.simulationType === 'Inclined Plane' && !this.dataDoc.simulation_paused && ( <Typography> <> θ: {Math.round(this.wedgeAngle * 100) / 100}° ≈ {Math.round(((this.wedgeAngle * Math.PI) / 180) * 100) / 100} rad @@ -1546,12 +1557,12 @@ export class PhysicsSimulationBox extends ViewBoxAnnotatableComponent<FieldViewP </> </Typography> )} - {this.simulationType == 'Pendulum' && !this.dataDoc.simulation_paused && ( + {this.simulationType === 'Pendulum' && !this.dataDoc.simulation_paused && ( <Typography> θ: {Math.round(this.pendulumAngle * 100) / 100}° ≈ {Math.round(((this.pendulumAngle * Math.PI) / 180) * 100) / 100} rad </Typography> )} - {this.simulationType == 'Pendulum' && this.dataDoc.simulation_paused && ( + {this.simulationType === 'Pendulum' && this.dataDoc.simulation_paused && ( <div> <InputField label={<Box>Angle</Box>} @@ -1559,13 +1570,13 @@ export class PhysicsSimulationBox extends ViewBoxAnnotatableComponent<FieldViewP dataDoc={this.dataDoc} prop="pendulum_angle" step={1} - unit={'°'} + unit="°" upperBound={59} value={NumCast(this.dataDoc.pendulum_angle, 30)} effect={action(value => { this.dataDoc.pendulum_angleStart = value; this.dataDoc.pendulum_lengthStart = this.dataDoc.pendulum_length; - if (this.simulationType == 'Pendulum') { + if (this.simulationType === 'Pendulum') { const mag = this.mass1 * Math.abs(this.gravity) * Math.cos((value * Math.PI) / 180); const forceOfTension: IForce = { @@ -1598,7 +1609,7 @@ export class PhysicsSimulationBox extends ViewBoxAnnotatableComponent<FieldViewP this._simReset++; } })} - radianEquivalent={true} + radianEquivalent mode="Freeform" labelWidth="5em" /> @@ -1612,7 +1623,7 @@ export class PhysicsSimulationBox extends ViewBoxAnnotatableComponent<FieldViewP upperBound={400} value={Math.round(this.pendulumLength)} effect={action(value => { - if (this.simulationType == 'Pendulum') { + if (this.simulationType === 'Pendulum') { this.dataDoc.pendulum_angleStart = this.pendulumAngle; this.dataDoc.pendulum_lengthStart = value; this._simReset++; @@ -1627,11 +1638,11 @@ export class PhysicsSimulationBox extends ViewBoxAnnotatableComponent<FieldViewP </div> )} <div className="mechanicsSimulationEquation"> - {this.simulationMode == 'Freeform' && ( + {this.simulationMode === 'Freeform' && ( <table> <tbody> <tr> - <td>{this.simulationType == 'Pulley' ? 'Red Weight' : ''}</td> + <td>{this.simulationType === 'Pulley' ? 'Red Weight' : ''}</td> <td>X</td> <td>Y</td> </tr> @@ -1646,36 +1657,34 @@ export class PhysicsSimulationBox extends ViewBoxAnnotatableComponent<FieldViewP > <Box>Position</Box> </td> - {(!this.dataDoc.simulation_paused || this.simulationType == 'Inclined Plane' || this.simulationType == 'Circular Motion' || this.simulationType == 'Pulley') && ( - <td style={{ cursor: 'default' }}> - <>{this.dataDoc.mass1_positionX} m</> - </td> + {(!this.dataDoc.simulation_paused || this.simulationType === 'Inclined Plane' || this.simulationType === 'Circular Motion' || this.simulationType === 'Pulley') && ( + <td style={{ cursor: 'default' }}>{this.dataDoc.mass1_positionX + ''} m</td> )}{' '} - {this.dataDoc.simulation_paused && this.simulationType != 'Inclined Plane' && this.simulationType != 'Circular Motion' && this.simulationType != 'Pulley' && ( + {this.dataDoc.simulation_paused && this.simulationType !== 'Inclined Plane' && this.simulationType !== 'Circular Motion' && this.simulationType !== 'Pulley' && ( <td style={{ cursor: 'default', }}> <InputField - lowerBound={this.simulationType == 'Projectile' ? 1 : (this.xMax + this.xMin) / 4 - this.radius - 15} + lowerBound={this.simulationType === 'Projectile' ? 1 : (this.xMax + this.xMin) / 4 - this.radius - 15} dataDoc={this.dataDoc} prop="mass1_positionX" step={1} - unit={'m'} - upperBound={this.simulationType == 'Projectile' ? this.xMax - 110 : (3 * (this.xMax + this.xMin)) / 4 - this.radius / 2 - 15} + unit="m" + upperBound={this.simulationType === 'Projectile' ? this.xMax - 110 : (3 * (this.xMax + this.xMin)) / 4 - this.radius / 2 - 15} value={NumCast(this.dataDoc.mass1_positionX)} effect={value => { this.dataDoc.mass1_xChange = value; - if (this.simulationType == 'Suspension') { - let x1rod = (this.xMax + this.xMin) / 2 - this.radius - this.yMin - 200; - let x2rod = (this.xMax + this.xMin) / 2 + this.yMin + 200 + this.radius; - let deltaX1 = value + this.radius - x1rod; - let deltaX2 = x2rod - (value + this.radius); - let deltaY = this.getYPosFromDisplay(NumCast(this.dataDoc.mass1_positionY)) + this.radius; + if (this.simulationType === 'Suspension') { + const x1rod = (this.xMax + this.xMin) / 2 - this.radius - this.yMin - 200; + const x2rod = (this.xMax + this.xMin) / 2 + this.yMin + 200 + this.radius; + const deltaX1 = value + this.radius - x1rod; + const deltaX2 = x2rod - (value + this.radius); + const deltaY = this.getYPosFromDisplay(NumCast(this.dataDoc.mass1_positionY)) + this.radius; let dir1T = Math.PI - Math.atan(deltaY / deltaX1); let dir2T = Math.atan(deltaY / deltaX2); - let tensionMag2 = (this.mass1 * Math.abs(this.gravity)) / ((-Math.cos(dir2T) / Math.cos(dir1T)) * Math.sin(dir1T) + Math.sin(dir2T)); - let tensionMag1 = (-tensionMag2 * Math.cos(dir2T)) / Math.cos(dir1T); + const tensionMag2 = (this.mass1 * Math.abs(this.gravity)) / ((-Math.cos(dir2T) / Math.cos(dir1T)) * Math.sin(dir1T) + Math.sin(dir2T)); + const tensionMag1 = (-tensionMag2 * Math.cos(dir2T)) / Math.cos(dir1T); dir1T = (dir1T * 180) / Math.PI; dir2T = (dir2T * 180) / Math.PI; const tensionForce1: IForce = { @@ -1692,15 +1701,15 @@ export class PhysicsSimulationBox extends ViewBoxAnnotatableComponent<FieldViewP this.dataDoc.mass1_forcesUpdated = JSON.stringify([tensionForce1, tensionForce2, gravity]); } }} - small={true} + small mode="Freeform" /> </td> )}{' '} - {(!this.dataDoc.simulation_paused || this.simulationType == 'Inclined Plane' || this.simulationType == 'Circular Motion' || this.simulationType == 'Pulley') && ( + {(!this.dataDoc.simulation_paused || this.simulationType === 'Inclined Plane' || this.simulationType === 'Circular Motion' || this.simulationType === 'Pulley') && ( <td style={{ cursor: 'default' }}>{`${NumCast(this.dataDoc.mass1_positionY)} m`}</td> )}{' '} - {this.dataDoc.simulation_paused && this.simulationType != 'Inclined Plane' && this.simulationType != 'Circular Motion' && this.simulationType != 'Pulley' && ( + {this.dataDoc.simulation_paused && this.simulationType !== 'Inclined Plane' && this.simulationType !== 'Circular Motion' && this.simulationType !== 'Pulley' && ( <td style={{ cursor: 'default', @@ -1715,16 +1724,16 @@ export class PhysicsSimulationBox extends ViewBoxAnnotatableComponent<FieldViewP value={NumCast(this.dataDoc.mass1_positionY)} effect={value => { this.dataDoc.mass1_yChange = value; - if (this.simulationType == 'Suspension') { - let x1rod = (this.xMax + this.xMin) / 2 - this.radius - this.yMin - 200; - let x2rod = (this.xMax + this.xMin) / 2 + this.yMin + 200 + this.radius; - let deltaX1 = NumCast(this.dataDoc.mass1_positionX) + this.radius - x1rod; - let deltaX2 = x2rod - (NumCast(this.dataDoc.mass1_positionX) + this.radius); - let deltaY = this.getYPosFromDisplay(value) + this.radius; + if (this.simulationType === 'Suspension') { + const x1rod = (this.xMax + this.xMin) / 2 - this.radius - this.yMin - 200; + const x2rod = (this.xMax + this.xMin) / 2 + this.yMin + 200 + this.radius; + const deltaX1 = NumCast(this.dataDoc.mass1_positionX) + this.radius - x1rod; + const deltaX2 = x2rod - (NumCast(this.dataDoc.mass1_positionX) + this.radius); + const deltaY = this.getYPosFromDisplay(value) + this.radius; let dir1T = Math.PI - Math.atan(deltaY / deltaX1); let dir2T = Math.atan(deltaY / deltaX2); - let tensionMag2 = (this.mass1 * Math.abs(this.gravity)) / ((-Math.cos(dir2T) / Math.cos(dir1T)) * Math.sin(dir1T) + Math.sin(dir2T)); - let tensionMag1 = (-tensionMag2 * Math.cos(dir2T)) / Math.cos(dir1T); + const tensionMag2 = (this.mass1 * Math.abs(this.gravity)) / ((-Math.cos(dir2T) / Math.cos(dir1T)) * Math.sin(dir1T) + Math.sin(dir2T)); + const tensionMag1 = (-tensionMag2 * Math.cos(dir2T)) / Math.cos(dir1T); dir1T = (dir1T * 180) / Math.PI; dir2T = (dir2T * 180) / Math.PI; const tensionForce1: IForce = { @@ -1741,7 +1750,7 @@ export class PhysicsSimulationBox extends ViewBoxAnnotatableComponent<FieldViewP this.dataDoc.mass1_forcesUpdated = JSON.stringify([tensionForce1, tensionForce2, gravity]); } }} - small={true} + small mode="Freeform" /> </td> @@ -1758,10 +1767,10 @@ export class PhysicsSimulationBox extends ViewBoxAnnotatableComponent<FieldViewP > <Box>Velocity</Box> </td> - {(!this.dataDoc.simulation_paused || (this.simulationType != 'One Weight' && this.simulationType != 'Circular Motion')) && ( + {(!this.dataDoc.simulation_paused || (this.simulationType !== 'One Weight' && this.simulationType !== 'Circular Motion')) && ( <td style={{ cursor: 'default' }}>{`${NumCast(this.dataDoc.mass1_velocityX)} m/s`}</td> )}{' '} - {this.dataDoc.simulation_paused && (this.simulationType == 'One Weight' || this.simulationType == 'Circular Motion') && ( + {this.dataDoc.simulation_paused && (this.simulationType === 'One Weight' || this.simulationType === 'Circular Motion') && ( <td style={{ cursor: 'default', @@ -1771,24 +1780,20 @@ export class PhysicsSimulationBox extends ViewBoxAnnotatableComponent<FieldViewP dataDoc={this.dataDoc} prop="mass1_velocityX" step={1} - unit={'m/s'} + unit="m/s" upperBound={50} value={NumCast(this.dataDoc.mass1_velocityX)} effect={action(value => { this.dataDoc.mass1_velocityXstart = value; this._simReset++; })} - small={true} + small mode="Freeform" /> </td> )}{' '} - {(!this.dataDoc.simulation_paused || this.simulationType != 'One Weight') && ( - <td style={{ cursor: 'default' }}> - <>{this.dataDoc.mass1_velocityY} m/s</> - </td> - )}{' '} - {this.dataDoc.simulation_paused && this.simulationType == 'One Weight' && ( + {(!this.dataDoc.simulation_paused || this.simulationType !== 'One Weight') && <td style={{ cursor: 'default' }}>{this.dataDoc.mass1_velocityY + ''} m/s</td>}{' '} + {this.dataDoc.simulation_paused && this.simulationType === 'One Weight' && ( <td style={{ cursor: 'default', @@ -1804,7 +1809,7 @@ export class PhysicsSimulationBox extends ViewBoxAnnotatableComponent<FieldViewP effect={value => { this.dataDoc.mass1_velocityYstart = -value; }} - small={true} + small mode="Freeform" /> </td> @@ -1822,14 +1827,10 @@ export class PhysicsSimulationBox extends ViewBoxAnnotatableComponent<FieldViewP <Box>Acceleration</Box> </td> <td style={{ cursor: 'default' }}> - <> - {this.dataDoc.mass1_accelerationX} m/s<sup>2</sup> - </> + {this.dataDoc.mass1_accelerationX + ''} m/s<sup>2</sup> </td> <td style={{ cursor: 'default' }}> - <> - {this.dataDoc.mass1_accelerationY} m/s<sup>2</sup> - </> + {this.dataDoc.mass1_accelerationY + ''} m/s<sup>2</sup> </td> </tr> <tr> @@ -1842,7 +1843,7 @@ export class PhysicsSimulationBox extends ViewBoxAnnotatableComponent<FieldViewP </tbody> </table> )} - {this.simulationMode == 'Freeform' && this.simulationType == 'Pulley' && ( + {this.simulationMode === 'Freeform' && this.simulationType === 'Pulley' && ( <table> <tbody> <tr> @@ -1869,14 +1870,10 @@ export class PhysicsSimulationBox extends ViewBoxAnnotatableComponent<FieldViewP <Box>Acceleration</Box> </td> <td style={{ cursor: 'default' }}> - <> - {this.dataDoc.mass2_accelerationX} m/s<sup>2</sup> - </> + {this.dataDoc.mass2_accelerationX + ''} m/s<sup>2</sup> </td> <td style={{ cursor: 'default' }}> - <> - {this.dataDoc.mass2_accelerationY} m/s<sup>2</sup> - </> + {this.dataDoc.mass2_accelerationY + ''} m/s<sup>2</sup> </td> </tr> <tr> @@ -1890,7 +1887,7 @@ export class PhysicsSimulationBox extends ViewBoxAnnotatableComponent<FieldViewP </table> )} </div> - {this.simulationType != 'Pendulum' && this.simulationType != 'Spring' && ( + {this.simulationType !== 'Pendulum' && this.simulationType !== 'Spring' && ( <div> <p>Kinematic Equations</p> <ul> @@ -1907,7 +1904,7 @@ export class PhysicsSimulationBox extends ViewBoxAnnotatableComponent<FieldViewP </ul> </div> )} - {this.simulationType == 'Spring' && ( + {this.simulationType === 'Spring' && ( <div> <p>Harmonic Motion Equations: Spring</p> <ul> @@ -1936,7 +1933,7 @@ export class PhysicsSimulationBox extends ViewBoxAnnotatableComponent<FieldViewP </ul> </div> )} - {this.simulationType == 'Pendulum' && ( + {this.simulationType === 'Pendulum' && ( <div> <p>Harmonic Motion Equations: Pendulum</p> <ul> @@ -1959,11 +1956,11 @@ export class PhysicsSimulationBox extends ViewBoxAnnotatableComponent<FieldViewP <svg width={100 + 'px'} height={100 + 'px'}> <defs> <marker id="miniArrow" markerWidth="20" markerHeight="20" refX="0" refY="3" orient="auto" markerUnits="strokeWidth"> - <path d="M0,0 L0,6 L9,3 z" fill={'#000000'} /> + <path d="M0,0 L0,6 L9,3 z" fill="#000000" /> </marker> </defs> - <line x1={20} y1={70} x2={70} y2={70} stroke={'#000000'} strokeWidth="2" markerEnd="url(#miniArrow)" /> - <line x1={20} y1={70} x2={20} y2={20} stroke={'#000000'} strokeWidth="2" markerEnd="url(#miniArrow)" /> + <line x1={20} y1={70} x2={70} y2={70} stroke="#000000" strokeWidth="2" markerEnd="url(#miniArrow)" /> + <line x1={20} y1={70} x2={20} y2={20} stroke="#000000" strokeWidth="2" markerEnd="url(#miniArrow)" /> </svg> <p style={{ @@ -1971,7 +1968,7 @@ export class PhysicsSimulationBox extends ViewBoxAnnotatableComponent<FieldViewP top: this.yMax - 120 + 40 + 'px', left: this.xMin + 90 - 80 + 'px', }}> - {this.simulationType == 'Circular Motion' ? 'Z' : 'Y'} + {this.simulationType === 'Circular Motion' ? 'Z' : 'Y'} </p> <p style={{ @@ -1986,3 +1983,9 @@ export class PhysicsSimulationBox extends ViewBoxAnnotatableComponent<FieldViewP ); } } + +Docs.Prototypes.TemplateMap.set(DocumentType.SIMULATION, { + data: '', + layout: { view: PhysicsSimulationBox, dataField: 'data' }, + options: { acl: '', _width: 1000, _height: 800, mass1: '', mass2: '', layout_nativeDimEditable: true, position: '', acceleration: '', pendulum: '', spring: '', wedge: '', simulation: '', review: '', systemIcon: 'BsShareFill' }, +}); diff --git a/src/client/views/nodes/RadialMenu.tsx b/src/client/views/nodes/RadialMenu.tsx index 16450c359..48da4937a 100644 --- a/src/client/views/nodes/RadialMenu.tsx +++ b/src/client/views/nodes/RadialMenu.tsx @@ -6,22 +6,57 @@ import { RadialMenuItem, RadialMenuProps } from './RadialMenuItem'; @observer export class RadialMenu extends React.Component { + // eslint-disable-next-line no-use-before-define static Instance: RadialMenu; static readonly buffer = 20; + @observable private _mouseX: number = -1; + @observable private _mouseY: number = -1; + @observable private _shouldDisplay: boolean = false; + @observable private _mouseDown: boolean = false; + @observable private _closest: number = -1; + @observable private _pageX: number = 0; + @observable private _pageY: number = 0; + @observable _display: boolean = false; + @observable private _yRelativeToTop: boolean = true; + @observable private _items: Array<RadialMenuProps> = []; + private _reactionDisposer?: IReactionDisposer; + constructor(props: any) { super(props); makeObservable(this); RadialMenu.Instance = this; } - @observable private _mouseX: number = -1; - @observable private _mouseY: number = -1; - @observable private _shouldDisplay: boolean = false; - @observable private _mouseDown: boolean = false; - private _reactionDisposer?: IReactionDisposer; + componentDidMount() { + document.addEventListener('pointerdown', this.onPointerDown); + document.addEventListener('pointerup', this.onPointerUp); + this.previewcircle(); + this._reactionDisposer = reaction( + () => this._shouldDisplay, + () => + this._shouldDisplay && + !this._mouseDown && + runInAction(() => { + this._display = true; + }) + ); + } - public used: boolean = false; + componentDidUpdate() { + this.previewcircle(); + } + componentWillUnmount() { + document.removeEventListener('pointerdown', this.onPointerDown); + + document.removeEventListener('pointerup', this.onPointerUp); + this._reactionDisposer && this._reactionDisposer(); + } + + @computed get menuItems() { + // eslint-disable-next-line react/jsx-props-no-spreading + return this._items.map((item, index) => <RadialMenuItem {...item} key={item.description} closeMenu={this.closeMenu} max={this._items.length} min={index} selected={this._closest} />); + } catchTouch = (te: React.TouchEvent) => { te.stopPropagation(); @@ -33,13 +68,9 @@ export class RadialMenu extends React.Component { this._mouseDown = true; this._mouseX = e.clientX; this._mouseY = e.clientY; - this.used = false; document.addEventListener('pointermove', this.onPointerMove); }; - @observable - private _closest: number = -1; - @action onPointerMove = (e: PointerEvent) => { const curX = e.clientX; @@ -65,7 +96,6 @@ export class RadialMenu extends React.Component { }; @action onPointerUp = (e: PointerEvent) => { - this.used = true; this._mouseDown = false; const curX = e.clientX; const curY = e.clientY; @@ -78,86 +108,6 @@ export class RadialMenu extends React.Component { this._items[this._closest].event(); } }; - componentWillUnmount() { - document.removeEventListener('pointerdown', this.onPointerDown); - - document.removeEventListener('pointerup', this.onPointerUp); - this._reactionDisposer && this._reactionDisposer(); - } - - @action - componentDidMount() { - document.addEventListener('pointerdown', this.onPointerDown); - document.addEventListener('pointerup', this.onPointerUp); - this.previewcircle(); - this._reactionDisposer = reaction( - () => this._shouldDisplay, - () => this._shouldDisplay && !this._mouseDown && runInAction(() => (this._display = true)) - ); - } - - componentDidUpdate = () => { - this.previewcircle(); - }; - - @observable private _pageX: number = 0; - @observable private _pageY: number = 0; - @observable _display: boolean = false; - @observable private _yRelativeToTop: boolean = true; - - @observable private _width: number = 0; - @observable private _height: number = 0; - - getItems() { - return this._items; - } - - @action - addItem(item: RadialMenuProps) { - if (this._items.indexOf(item) === -1) { - this._items.push(item); - } - } - - @observable - private _items: Array<RadialMenuProps> = []; - - @action - displayMenu = (x: number, y: number) => { - //maxX and maxY will change if the UI/font size changes, but will work for any amount - //of items added to the menu - this._mouseX = x; - this._mouseY = y; - this._shouldDisplay = true; - }; - // @computed - // get pageX() { - // const x = this._pageX; - // if (x < 0) { - // return 0; - // } - // const width = this._width; - // if (x + width > window.innerWidth - RadialMenu.buffer) { - // return window.innerWidth - RadialMenu.buffer - width; - // } - // return x; - // } - // @computed - // get pageY() { - // const y = this._pageY; - // if (y < 0) { - // return 0; - // } - // const height = this._height; - // if (y + height > window.innerHeight - RadialMenu.buffer) { - // return window.innerHeight - RadialMenu.buffer - height; - // } - // return y; - // } - - @computed get menuItems() { - return this._items.map((item, index) => <RadialMenuItem {...item} key={item.description} closeMenu={this.closeMenu} max={this._items.length} min={index} selected={this._closest} />); - } @action closeMenu = () => { @@ -167,14 +117,6 @@ export class RadialMenu extends React.Component { }; @action - openMenu = (x: number, y: number) => { - this._pageX = x; - this._pageY = y; - this._shouldDisplay; - this._display = true; - }; - - @action clearItems() { this._items = []; } diff --git a/src/client/views/nodes/RadialMenuItem.tsx b/src/client/views/nodes/RadialMenuItem.tsx index 91dc37d34..6f10e7b65 100644 --- a/src/client/views/nodes/RadialMenuItem.tsx +++ b/src/client/views/nodes/RadialMenuItem.tsx @@ -1,3 +1,4 @@ +/* eslint-disable react/require-default-props */ import { IconProp } from '@fortawesome/fontawesome-svg-core'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { observer } from 'mobx-react'; @@ -53,6 +54,7 @@ export class RadialMenuItem extends React.Component<RadialMenuProps> { case 2: color = 'lightgray'; break; + default: } if (circlemax % 3 === 1 && circlemin === circlemax - 1) { color = '#c2c2c5'; @@ -80,7 +82,6 @@ export class RadialMenuItem extends React.Component<RadialMenuProps> { const avg = (circlemin / circlemax + (circlemin + 1) / circlemax) / 2; const degrees = 360 * avg; const x = 100 * Math.cos((degrees * Math.PI) / 180); - const y = -125 * Math.sin((degrees * Math.PI) / 180); return x; } @@ -91,7 +92,6 @@ export class RadialMenuItem extends React.Component<RadialMenuProps> { this.props.max ? (circlemax = this.props.max) : null; const avg = (circlemin / circlemax + (circlemin + 1) / circlemax) / 2; const degrees = 360 * avg; - const x = 125 * Math.cos((degrees * Math.PI) / 180); const y = -100 * Math.sin((degrees * Math.PI) / 180); return y; } diff --git a/src/client/views/nodes/RecordingBox/ProgressBar.tsx b/src/client/views/nodes/RecordingBox/ProgressBar.tsx index 1bb2b7c84..62798bc2f 100644 --- a/src/client/views/nodes/RecordingBox/ProgressBar.tsx +++ b/src/client/views/nodes/RecordingBox/ProgressBar.tsx @@ -1,31 +1,33 @@ +/* eslint-disable react/no-array-index-key */ +/* eslint-disable react/require-default-props */ import * as React from 'react'; -import { useEffect, useState, useCallback, useRef } from "react" -import "./ProgressBar.scss" +import { useEffect, useState, useRef } from 'react'; +import './ProgressBar.scss'; import { MediaSegment } from './RecordingView'; interface ProgressBarProps { - videos: MediaSegment[], - setVideos: React.Dispatch<React.SetStateAction<MediaSegment[]>>, - orderVideos: boolean, - progress: number, - recording: boolean, - doUndo: boolean, - setCanUndo?: React.Dispatch<React.SetStateAction<boolean>>, + videos: MediaSegment[]; + setVideos: React.Dispatch<React.SetStateAction<MediaSegment[]>>; + orderVideos: boolean; + progress: number; + recording: boolean; + doUndo: boolean; + setCanUndo?: React.Dispatch<React.SetStateAction<boolean>>; } interface SegmentBox { - endTime: number, - startTime: number, - order: number, + endTime: number; + startTime: number; + order: number; } interface CurrentHover { - index: number, - minX: number, - maxX: number + index: number; + minX: number; + maxX: number; } export function ProgressBar(props: ProgressBarProps) { - const progressBarRef = useRef<HTMLDivElement | null>(null) + const progressBarRef = useRef<HTMLDivElement | null>(null); // the actual list of JSX elements rendered as segments const [segments, setSegments] = useState<JSX.Element[]>([]); @@ -47,8 +49,6 @@ export function ProgressBar(props: ProgressBarProps) { // update the canUndo props based on undo stack useEffect(() => props.setCanUndo?.(undoStack.length > 0), [undoStack.length]); - // useEffect for undo - brings back the most recently deleted segment - useEffect(() => handleUndo(), [props.doUndo]) const handleUndo = () => { // get the last element from the undo if it exists if (undoStack.length === 0) return; @@ -59,27 +59,36 @@ export function ProgressBar(props: ProgressBarProps) { // update the removed time and place element back into ordered setTotalRemovedTime(prevRemoved => prevRemoved - (last.endTime - last.startTime)); setOrdered(prevOrdered => [...prevOrdered, last]); - } + }; + // useEffect for undo - brings back the most recently deleted segment + useEffect(() => handleUndo(), [props.doUndo]); // useEffect for recording changes - changes style to disabled and adds the "expanding-segment" useEffect(() => { // get segments segment's html using it's id -> make them appeared disabled (or enabled) - segments.forEach((seg) => document.getElementById(seg.props.id)?.classList.toggle('segment-disabled', props.recording)); + segments.forEach(seg => document.getElementById(seg.props.id)?.classList.toggle('segment-disabled', props.recording)); progressBarRef.current?.classList.toggle('progressbar-disabled', props.recording); if (props.recording) - setSegments(prevSegments => [...prevSegments, <div key='segment-expanding' id='segment-expanding' className='segment segment-expanding blink' style={{ width: 'fit-content' }}>{props.videos.length + 1}</div>]); - }, [props.recording]) - + setSegments(prevSegments => [ + ...prevSegments, + <div key="segment-expanding" id="segment-expanding" className="segment segment-expanding blink" style={{ width: 'fit-content' }}> + {props.videos.length + 1} + </div>, + ]); + }, [props.recording]); // useEffect that updates the segmentsJSX, which is rendered // only updated when ordered is updated or if the user is dragging around a segment useEffect(() => { const totalTime = props.progress * 1000 - totalRemovedTime; - const segmentsJSX = ordered.map((seg, i) => - <div key={`segment-${i}`} id={`segment-${i}`} className={dragged === i ? 'segment-hide' : 'segment'} style={{ width: `${((seg.endTime - seg.startTime) / totalTime) * 100}%` }}>{seg.order + 1}</div>); + const segmentsJSX = ordered.map((seg, i) => ( + <div key={`segment-${i}`} id={`segment-${i}`} className={dragged === i ? 'segment-hide' : 'segment'} style={{ width: `${((seg.endTime - seg.startTime) / totalTime) * 100}%` }}> + {seg.order + 1} + </div> + )); - setSegments(segmentsJSX) + setSegments(segmentsJSX); }, [dragged, ordered]); // useEffect for dragged - update the cursor to be grabbing while grabbing @@ -89,14 +98,14 @@ export function ProgressBar(props: ProgressBarProps) { // to imporve performance, only want to update the CSS width, not re-render the whole JSXList useEffect(() => { - if (!props.recording) return + if (!props.recording) return; const totalTime = props.progress * 1000 - totalRemovedTime; let remainingTime = totalTime; segments.forEach((seg, i) => { // for the last segment, we need to set that directly if (i === segments.length - 1) return; // update remaining time - remainingTime -= (ordered[i].endTime - ordered[i].startTime); + remainingTime -= ordered[i].endTime - ordered[i].startTime; // update the width for this segment const htmlId = seg.props.id; @@ -106,8 +115,7 @@ export function ProgressBar(props: ProgressBarProps) { // update the width of the expanding segment using the remaining time const segExapandHtml = document.getElementById('segment-expanding'); - if (segExapandHtml) - segExapandHtml.style.width = ordered.length === 0 ? '100%' : `${(remainingTime / totalTime) * 100}%`; + if (segExapandHtml) segExapandHtml.style.width = ordered.length === 0 ? '100%' : `${(remainingTime / totalTime) * 100}%`; }, [props.progress]); // useEffect for props.videos - update the ordered array when a new video is added @@ -120,9 +128,7 @@ export function ProgressBar(props: ProgressBarProps) { // in this case, a new video is added -> push it onto ordered if (order >= ordered.length) { const { endTime, startTime } = props.videos.lastElement(); - setOrdered(prevOrdered => { - return [...prevOrdered, { endTime, startTime, order }]; - }); + setOrdered(prevOrdered => [...prevOrdered, { endTime, startTime, order }]); } // in this case, a video is removed @@ -132,7 +138,7 @@ export function ProgressBar(props: ProgressBarProps) { }, [props.videos]); // useEffect for props.orderVideos - matched the order array with the videos array before the export - useEffect(() => props.setVideos(vids => ordered.map((seg) => vids[seg.order])), [props.orderVideos]); + useEffect(() => props.setVideos(vids => ordered.map(seg => vids[seg.order])), [props.orderVideos]); // useEffect for removed - handles logic for removing a segment useEffect(() => { @@ -151,36 +157,68 @@ export function ProgressBar(props: ProgressBarProps) { // returns the new currentHover based on the new index const updateCurrentHover = (segId: number): CurrentHover | null => { // get the segId of the segment that will become the new bounding area - const rect = progressBarRef.current?.children[segId].getBoundingClientRect() - if (rect == null) return null + const rect = progressBarRef.current?.children[segId].getBoundingClientRect(); + if (rect == null) return null; return { index: segId, minX: rect.x, maxX: rect.x + rect.width, - } - } + }; + }; + + const swapSegments = (oldIndex: number, newIndex: number) => { + if (newIndex == null) return; + setOrdered(prevOrdered => { + const temp = { ...prevOrdered[oldIndex] }; + prevOrdered[oldIndex] = prevOrdered[newIndex]; + prevOrdered[newIndex] = temp; + return prevOrdered; + }); + // update visually where the segment is hovering over + setDragged(newIndex); + }; + + // functions for the floating segment that tracks the cursor while grabbing it + const initDetachSegment = (dot: HTMLDivElement, rect: DOMRect) => { + dot.classList.add('segment-selected'); + dot.style.transitionDuration = '0s'; + dot.style.position = 'absolute'; + dot.style.zIndex = '999'; + dot.style.width = `${rect.width}px`; + dot.style.height = `${rect.height}px`; + dot.style.left = `${rect.x}px`; + dot.style.top = `${rect.y}px`; + dot.draggable = false; + document.body.append(dot); + }; + const followCursor = (event: PointerEvent, dot: HTMLDivElement): void => { + // event.stopPropagation() + const { width, height } = dot.getBoundingClientRect(); + dot.style.left = `${event.clientX - width / 2}px`; + dot.style.top = `${event.clientY - height / 2}px`; + }; // pointerdown event for the progress bar - const onPointerDown = (e: React.PointerEvent<HTMLDivElement>) => { - // don't move the videobox element - e.stopPropagation(); + const onPointerDown = (e: React.PointerEvent<HTMLDivElement>) => { + // don't move the videobox element + e.stopPropagation(); // if recording, do nothing - if (props.recording) return; + if (props.recording) return; // get the segment the user clicked on to be dragged - const clickedSegment = e.target as HTMLDivElement & EventTarget + const clickedSegment = e.target as HTMLDivElement & EventTarget; // get the profess bar ro add event listeners // don't do anything if null - const progressBar = progressBarRef.current - if (progressBar == null || clickedSegment.id === progressBar.id) return + const progressBar = progressBarRef.current; + if (progressBar == null || clickedSegment.id === progressBar.id) return; // if holding shift key, let's remove that segment if (e.shiftKey) { const segId = parseInt(clickedSegment.id.split('-')[1]); setRemoved(segId); - return + return; } // if holding ctrl key and click, let's undo that segment #hiddenfeature lol @@ -192,26 +230,26 @@ export function ProgressBar(props: ProgressBarProps) { // if we're here, the user is dragging a segment around // let the progress bar capture all the pointer events until the user releases (pointerUp) const ptrId = e.pointerId; - progressBar.setPointerCapture(ptrId) + progressBar.setPointerCapture(ptrId); - const rect = clickedSegment.getBoundingClientRect() - // id for segment is like 'segment-1' or 'segment-10', + const rect = clickedSegment.getBoundingClientRect(); + // id for segment is like 'segment-1' or 'segment-10', // so this works to get the id - const segId = parseInt(clickedSegment.id.split('-')[1]) + const segId = parseInt(clickedSegment.id.split('-')[1]); // set the selected segment to be the one dragged - setDragged(segId) + setDragged(segId); - // this is the logic for storing the lower X bound and upper X bound to know + // this is the logic for storing the lower X bound and upper X bound to know // whether a swap is needed between two segments let currentHover: CurrentHover = { index: segId, minX: rect.x, maxX: rect.x + rect.width, - } + }; // create the floating segment that tracks the cursor - const detchedSegment = document.createElement("div") - initDeatchSegment(detchedSegment, rect); + const detchedSegment = document.createElement('div'); + initDetachSegment(detchedSegment, rect); const updateSegmentOrder = (event: PointerEvent): void => { event.stopPropagation(); @@ -219,6 +257,7 @@ export function ProgressBar(props: ProgressBarProps) { // this fixes a bug where pointerup doesn't fire while cursor is upped while being dragged if (!progressBar.hasPointerCapture(ptrId)) { + // eslint-disable-next-line no-use-before-define placeSegmentandCleanup(); return; } @@ -228,24 +267,23 @@ export function ProgressBar(props: ProgressBarProps) { const curX = event.clientX; // handle the left bound if (curX < currentHover.minX && currentHover.index > 0) { - swapSegments(currentHover.index, currentHover.index - 1) - currentHover = updateCurrentHover(currentHover.index - 1) ?? currentHover + swapSegments(currentHover.index, currentHover.index - 1); + currentHover = updateCurrentHover(currentHover.index - 1) ?? currentHover; } // handle the right bound else if (curX > currentHover.maxX && currentHover.index < segments.length - 1) { - swapSegments(currentHover.index, currentHover.index + 1) - currentHover = updateCurrentHover(currentHover.index + 1) ?? currentHover + swapSegments(currentHover.index, currentHover.index + 1); + currentHover = updateCurrentHover(currentHover.index + 1) ?? currentHover; } - } + }; // handles when the user is done dragging the segment (pointerUp) const placeSegmentandCleanup = (event?: PointerEvent): void => { event?.stopPropagation(); event?.preventDefault(); // if they put the segment outside of the bounds, remove it - if (event && (event.clientX < 0 || event.clientX > document.body.clientWidth || event.clientY < 0 || event.clientY > document.body.clientHeight)) - setRemoved(currentHover.index); - + if (event && (event.clientX < 0 || event.clientX > document.body.clientWidth || event.clientY < 0 || event.clientY > document.body.clientHeight)) setRemoved(currentHover.index); + // remove the update event listener for pointermove progressBar.removeEventListener('pointermove', updateSegmentOrder); // remove the floating segment from the DOM @@ -253,49 +291,16 @@ export function ProgressBar(props: ProgressBarProps) { // dragged is -1 is equiv to nothing being dragged, so the normal state // so this will place the segment in it's location and update the segment bar setDragged(-1); - } + }; // event listeners that allow the user to drag and release the floating segment progressBar.addEventListener('pointermove', updateSegmentOrder); progressBar.addEventListener('pointerup', placeSegmentandCleanup, { once: true }); - } - - const swapSegments = (oldIndex: number, newIndex: number) => { - if (newIndex == null) return; - setOrdered(prevOrdered => { - const temp = { ...prevOrdered[oldIndex] } - prevOrdered[oldIndex] = prevOrdered[newIndex] - prevOrdered[newIndex] = temp - return prevOrdered - }); - // update visually where the segment is hovering over - setDragged(newIndex); - } - - // functions for the floating segment that tracks the cursor while grabbing it - const initDeatchSegment = (dot: HTMLDivElement, rect: DOMRect) => { - dot.classList.add("segment-selected"); - dot.style.transitionDuration = '0s'; - dot.style.position = 'absolute'; - dot.style.zIndex = '999'; - dot.style.width = `${rect.width}px`; - dot.style.height = `${rect.height}px`; - dot.style.left = `${rect.x}px`; - dot.style.top = `${rect.y}px`; - dot.draggable = false; - document.body.append(dot); - } - const followCursor = (event: PointerEvent, dot: HTMLDivElement): void => { - // event.stopPropagation() - const { width, height } = dot.getBoundingClientRect(); - dot.style.left = `${event.clientX - width / 2}px`; - dot.style.top = `${event.clientY - height / 2}px`; - } - + }; return ( <div className="progressbar" id="progressbar" onPointerDown={onPointerDown} ref={progressBarRef}> {segments} </div> - ) -}
\ No newline at end of file + ); +} diff --git a/src/client/views/nodes/RecordingBox/RecordingBox.tsx b/src/client/views/nodes/RecordingBox/RecordingBox.tsx index 1f976f926..e46e40bfe 100644 --- a/src/client/views/nodes/RecordingBox/RecordingBox.tsx +++ b/src/client/views/nodes/RecordingBox/RecordingBox.tsx @@ -3,6 +3,7 @@ import { observer } from 'mobx-react'; import * as React from 'react'; import { DateField } from '../../../../fields/DateField'; import { Doc, DocListCast } from '../../../../fields/Doc'; +import { DocData } from '../../../../fields/DocSymbols'; import { Id } from '../../../../fields/FieldSymbols'; import { List } from '../../../../fields/List'; import { BoolCast, DocCast } from '../../../../fields/Types'; @@ -10,18 +11,18 @@ import { VideoField } from '../../../../fields/URLField'; import { Upload } from '../../../../server/SharedMediaTypes'; import { DocumentType } from '../../../documents/DocumentTypes'; import { Docs } from '../../../documents/Documents'; -import { DocumentManager } from '../../../util/DocumentManager'; -import { DragManager, dropActionType } from '../../../util/DragManager'; +import { DragManager } from '../../../util/DragManager'; +import { dropActionType } from '../../../util/DropActionTypes'; import { ScriptingGlobals } from '../../../util/ScriptingGlobals'; import { Presentation } from '../../../util/TrackMovements'; import { undoBatch } from '../../../util/UndoManager'; import { ViewBoxBaseComponent } from '../../DocComponent'; import { CollectionFreeFormView } from '../../collections/collectionFreeForm/CollectionFreeFormView'; -import { media_state } from '../AudioBox'; +import { mediaState } from '../AudioBox'; +import { DocumentView } from '../DocumentView'; import { FieldView, FieldViewProps } from '../FieldView'; import { VideoBox } from '../VideoBox'; import { RecordingView } from './RecordingView'; -import { DocData } from '../../../../fields/DocSymbols'; @observer export class RecordingBox extends ViewBoxBaseComponent<FieldViewProps>() { @@ -43,16 +44,11 @@ export class RecordingBox extends ViewBoxBaseComponent<FieldViewProps>() { } @observable result: Upload.AccessPathInfo | undefined = undefined; - @observable videoDuration: number | undefined = undefined; - - @action - setVideoDuration = (duration: number) => (this.videoDuration = duration); @action setResult = (info: Upload.AccessPathInfo, presentation?: Presentation) => { this.result = info; this.dataDoc.type = DocumentType.VID; - this.dataDoc[this.fieldKey + '_duration'] = this.videoDuration; this.dataDoc.layout = VideoBox.LayoutString(this.fieldKey); this.dataDoc[this._props.fieldKey] = new VideoField(this.result.accessPaths.client); @@ -68,17 +64,17 @@ export class RecordingBox extends ViewBoxBaseComponent<FieldViewProps>() { public static WorkspaceStopRecording() { const remDoc = RecordingBox.screengrabber?.Document; if (remDoc) { - //if recordingbox is true; when we press the stop button. changed vals temporarily to see if changes happening + // if recordingbox is true; when we press the stop button. changed vals temporarily to see if changes happening RecordingBox.screengrabber?.Pause?.(); setTimeout(() => { RecordingBox.screengrabber?.Finish?.(); - remDoc.overlayX = 70; //was 100 + remDoc.overlayX = 70; // was 100 remDoc.overlayY = 590; RecordingBox.screengrabber = undefined; }, 100); - //could break if recording takes too long to turn into videobox. If so, either increase time on setTimeout below or find diff place to do this + // could break if recording takes too long to turn into videobox. If so, either increase time on setTimeout below or find diff place to do this setTimeout(() => Doc.RemFromMyOverlay(remDoc), 1000); - Doc.UserDoc().workspaceRecordingState = media_state.Paused; + Doc.UserDoc().workspaceRecordingState = mediaState.Paused; Doc.AddDocToList(Doc.UserDoc(), 'workspaceRecordings', remDoc); } } @@ -102,15 +98,15 @@ export class RecordingBox extends ViewBoxBaseComponent<FieldViewProps>() { _width: 205, _height: 115, }); - screengrabber.overlayX = 70; //was -400 - screengrabber.overlayY = 590; //was 0 + screengrabber.overlayX = 70; // was -400 + screengrabber.overlayY = 590; // was 0 screengrabber[DocData][Doc.LayoutFieldKey(screengrabber) + '_trackScreen'] = true; - Doc.AddToMyOverlay(screengrabber); //just adds doc to overlay - DocumentManager.Instance.AddViewRenderedCb(screengrabber, docView => { + Doc.AddToMyOverlay(screengrabber); // just adds doc to overlay + DocumentView.addViewRenderedCb(screengrabber, docView => { RecordingBox.screengrabber = docView.ComponentView as RecordingBox; RecordingBox.screengrabber.Record?.(); }); - Doc.UserDoc().workspaceRecordingState = media_state.Recording; + Doc.UserDoc().workspaceRecordingState = mediaState.Recording; } /** @@ -123,7 +119,7 @@ export class RecordingBox extends ViewBoxBaseComponent<FieldViewProps>() { value.overlayX = 70; value.overlayY = window.innerHeight - 180; Doc.AddToMyOverlay(value); - DocumentManager.Instance.AddViewRenderedCb(value, docView => { + DocumentView.addViewRenderedCb(value, docView => { Doc.UserDoc().currentRecording = docView.Document; docView.select(false); RecordingBox.resumeWorkspaceReplaying(value); @@ -136,7 +132,7 @@ export class RecordingBox extends ViewBoxBaseComponent<FieldViewProps>() { */ @undoBatch public static addRecToWorkspace(value: RecordingBox) { - let ffView = Array.from(DocumentManager.Instance.DocumentViews).find(view => view.ComponentView instanceof CollectionFreeFormView); + const ffView = DocumentView.allViews().find(view => view.ComponentView instanceof CollectionFreeFormView); (ffView?.ComponentView as CollectionFreeFormView)._props.addDocument?.(value.Document); Doc.RemoveDocFromList(Doc.UserDoc(), 'workspaceRecordings', value.Document); Doc.RemFromMyOverlay(value.Document); @@ -146,20 +142,20 @@ export class RecordingBox extends ViewBoxBaseComponent<FieldViewProps>() { } public static resumeWorkspaceReplaying(doc: Doc) { - const docView = DocumentManager.Instance.getDocumentView(doc); + const docView = DocumentView.getDocumentView(doc); if (docView?.ComponentView instanceof VideoBox) { docView.ComponentView.Play(); } - Doc.UserDoc().workspaceReplayingState = media_state.Playing; + Doc.UserDoc().workspaceReplayingState = mediaState.Playing; } public static pauseWorkspaceReplaying(doc: Doc) { - const docView = DocumentManager.Instance.getDocumentView(doc); + const docView = DocumentView.getDocumentView(doc); const videoBox = docView?.ComponentView as VideoBox; if (videoBox) { videoBox.Pause(); } - Doc.UserDoc().workspaceReplayingState = media_state.Paused; + Doc.UserDoc().workspaceReplayingState = mediaState.Paused; } public static stopWorkspaceReplaying(value: Doc) { @@ -191,66 +187,78 @@ export class RecordingBox extends ViewBoxBaseComponent<FieldViewProps>() { render() { return ( <div className="recordingBox" style={{ width: '100%' }} ref={this._ref}> - {!this.result && ( - <RecordingView - forceTrackScreen={BoolCast(this.layoutDoc[this.fieldKey + '_trackScreen'])} - getControls={this.getControls} - setResult={this.setResult} - setDuration={this.setVideoDuration} - id={DocCast(this.Document.proto)?.[Id] || ''} - /> - )} + {!this.result && <RecordingView forceTrackScreen={BoolCast(this.layoutDoc[this.fieldKey + '_trackScreen'])} getControls={this.getControls} setResult={this.setResult} id={DocCast(this.Document.proto)?.[Id] || ''} />} </div> ); } + // eslint-disable-next-line no-use-before-define static screengrabber: RecordingBox | undefined; } +// eslint-disable-next-line prefer-arrow-callback ScriptingGlobals.add(function stopWorkspaceRecording() { RecordingBox.WorkspaceStopRecording(); }); +// eslint-disable-next-line prefer-arrow-callback ScriptingGlobals.add(function stopWorkspaceReplaying(value: Doc) { RecordingBox.stopWorkspaceReplaying(value); }); +// eslint-disable-next-line prefer-arrow-callback ScriptingGlobals.add(function removeWorkspaceReplaying(value: Doc) { RecordingBox.removeWorkspaceReplaying(value); }); +// eslint-disable-next-line prefer-arrow-callback ScriptingGlobals.add(function getCurrentRecording() { return Doc.UserDoc().currentRecording; }); +// eslint-disable-next-line prefer-arrow-callback ScriptingGlobals.add(function getWorkspaceRecordings() { return new List<any>(['Record Workspace', `Record Webcam`, ...DocListCast(Doc.UserDoc().workspaceRecordings)]); }); +// eslint-disable-next-line prefer-arrow-callback ScriptingGlobals.add(function isWorkspaceRecording() { - return Doc.UserDoc().workspaceRecordingState === media_state.Recording; + return Doc.UserDoc().workspaceRecordingState === mediaState.Recording; }); +// eslint-disable-next-line prefer-arrow-callback ScriptingGlobals.add(function isWorkspaceReplaying() { return Doc.UserDoc().workspaceReplayingState; }); +// eslint-disable-next-line prefer-arrow-callback ScriptingGlobals.add(function replayWorkspace(value: Doc | string, _readOnly_: boolean) { if (_readOnly_) return DocCast(Doc.UserDoc().currentRecording) ?? 'Record Workspace'; if (typeof value === 'string') RecordingBox.WorkspaceStartRecording(value); else RecordingBox.replayWorkspace(value); + return undefined; }); -ScriptingGlobals.add(function pauseWorkspaceReplaying(value: Doc, _readOnly_: boolean) { +// eslint-disable-next-line prefer-arrow-callback +ScriptingGlobals.add(function pauseWorkspaceReplaying(value: Doc) { RecordingBox.pauseWorkspaceReplaying(value); }); -ScriptingGlobals.add(function resumeWorkspaceReplaying(value: Doc, _readOnly_: boolean) { +// eslint-disable-next-line prefer-arrow-callback +ScriptingGlobals.add(function resumeWorkspaceReplaying(value: Doc) { RecordingBox.resumeWorkspaceReplaying(value); }); +// eslint-disable-next-line prefer-arrow-callback ScriptingGlobals.add(function startRecordingDrag(value: { doc: Doc | string; e: React.PointerEvent }) { if (DocCast(value.doc)) { DragManager.StartDocumentDrag([value.e.target as HTMLElement], new DragManager.DocumentDragData([DocCast(value.doc)], dropActionType.embed), value.e.clientX, value.e.clientY); value.e.preventDefault(); return true; } + return undefined; }); +// eslint-disable-next-line prefer-arrow-callback ScriptingGlobals.add(function renderDropdown() { if (!Doc.UserDoc().workspaceRecordings || DocListCast(Doc.UserDoc().workspaceRecordings).length === 0) { return true; } return false; }); + +Docs.Prototypes.TemplateMap.set(DocumentType.WEBCAM, { + layout: { view: RecordingBox, dataField: 'data' }, + options: { acl: '', systemIcon: 'BsFillCameraVideoFill' }, +}); diff --git a/src/client/views/nodes/RecordingBox/RecordingView.tsx b/src/client/views/nodes/RecordingBox/RecordingView.tsx index f7ed82643..b8451fe60 100644 --- a/src/client/views/nodes/RecordingBox/RecordingView.tsx +++ b/src/client/views/nodes/RecordingBox/RecordingView.tsx @@ -1,10 +1,13 @@ +/* eslint-disable jsx-a11y/label-has-associated-control */ +/* eslint-disable react/button-has-type */ +/* eslint-disable jsx-a11y/control-has-associated-label */ import * as React from 'react'; import { useEffect, useRef, useState } from 'react'; import { IconContext } from 'react-icons'; import { FaCheckCircle } from 'react-icons/fa'; import { MdBackspace } from 'react-icons/md'; import { Upload } from '../../../../server/SharedMediaTypes'; -import { returnFalse, returnTrue, setupMoveUpEvents } from '../../../../Utils'; +import { returnFalse, returnTrue, setupMoveUpEvents } from '../../../../ClientUtils'; import { Networking } from '../../../Network'; import { Presentation, TrackMovements } from '../../../util/TrackMovements'; import { ProgressBar } from './ProgressBar'; @@ -19,19 +22,18 @@ export interface MediaSegment { interface IRecordingViewProps { setResult: (info: Upload.AccessPathInfo, presentation?: Presentation) => void; - setDuration: (seconds: number) => void; id: string; getControls: (record: () => void, pause: () => void, finish: () => void) => void; forceTrackScreen: boolean; } const MAXTIME = 100000; +const iconVals = { color: '#cc1c08', className: 'video-edit-buttons' }; export function RecordingView(props: IRecordingViewProps) { const [recording, setRecording] = useState(false); const recordingTimerRef = useRef<number>(0); const [recordingTimer, setRecordingTimer] = useState(0); // unit is 0.01 second - const [playing, setPlaying] = useState(false); const [progress, setProgress] = useState(0); // acts as a "refresh state" to tell progressBar when to undo @@ -62,7 +64,7 @@ export function RecordingView(props: IRecordingViewProps) { useEffect(() => { if (finished) { // make the total presentation that'll match the concatted video - let concatPres = (trackScreen || props.forceTrackScreen) && TrackMovements.Instance.concatPresentations(videos.map(v => v.presentation as Presentation)); + const concatPres = (trackScreen || props.forceTrackScreen) && TrackMovements.Instance.concatPresentations(videos.map(v => v.presentation as Presentation)); // this async function uses the server to create the concatted video and then sets the result to it's accessPaths (async () => { @@ -100,16 +102,16 @@ export function RecordingView(props: IRecordingViewProps) { return () => clearInterval(interval); }, [recording]); + const setVideoProgressHelper = (curProgrss: number) => { + const newProgress = (curProgrss / MAXTIME) * 100; + setProgress(newProgress); + }; + useEffect(() => { setVideoProgressHelper(recordingTimer); recordingTimerRef.current = recordingTimer; }, [recordingTimer]); - const setVideoProgressHelper = (progress: number) => { - const newProgress = (progress / MAXTIME) * 100; - setProgress(newProgress); - }; - const startShowingStream = async (mediaConstraints = DEFAULT_MEDIA_CONSTRAINTS) => { const stream = await navigator.mediaDevices.getUserMedia(mediaConstraints); @@ -131,7 +133,7 @@ export function RecordingView(props: IRecordingViewProps) { if (event.data.size > 0) videoChunks.push(event.data); }; - videoRecorder.current.onstart = (event: any) => { + videoRecorder.current.onstart = () => { setRecording(true); // start the recording api when the video recorder starts (trackScreen || props.forceTrackScreen) && TrackMovements.Instance.start(); @@ -149,7 +151,7 @@ export function RecordingView(props: IRecordingViewProps) { // depending on if a presenation exists, add it to the video const presentation = TrackMovements.Instance.yieldPresentation(); - setVideos(videos => [...videos, presentation != null && (trackScreen || props.forceTrackScreen) ? { ...nextVideo, presentation } : nextVideo]); + setVideos(theVideos => [...theVideos, presentation != null && (trackScreen || props.forceTrackScreen) ? { ...nextVideo, presentation } : nextVideo]); } // reset the temporary chunks @@ -186,7 +188,7 @@ export function RecordingView(props: IRecordingViewProps) { e, returnTrue, returnFalse, - e => { + () => { // start recording if not already recording if (!videoRecorder.current || videoRecorder.current.state === 'inactive') record(); @@ -202,14 +204,8 @@ export function RecordingView(props: IRecordingViewProps) { setDoUndo(prev => !prev); }; - const handleOnTimeUpdate = () => { - playing && setVideoProgressHelper(videoElementRef.current!.currentTime); - }; - const millisecondToMinuteSecond = (milliseconds: number) => { - const toTwoDigit = (digit: number) => { - return String(digit).length == 1 ? '0' + digit : digit; - }; + const toTwoDigit = (digit: number) => (String(digit).length === 1 ? '0' + digit : digit); const minutes = Math.floor((milliseconds % (1000 * 60 * 60)) / (1000 * 60)); const seconds = Math.floor((milliseconds % (1000 * 60)) / 1000); return toTwoDigit(minutes) + ' : ' + toTwoDigit(seconds); @@ -219,10 +215,11 @@ export function RecordingView(props: IRecordingViewProps) { props.getControls(record, pause, finish); }, []); + const iconUndoVals = React.useMemo(() => ({ color: 'grey', className: 'video-edit-buttons', style: { display: canUndo ? 'inherit' : 'none' } }), []); return ( <div className="recording-container"> <div className="video-wrapper"> - <video id={`video-${props.id}`} autoPlay muted onTimeUpdate={() => handleOnTimeUpdate()} ref={videoElementRef} /> + <video id={`video-${props.id}`} autoPlay muted ref={videoElementRef} /> <div className="recording-sign"> <span className="dot" /> <p className="timer">{millisecondToMinuteSecond(recordingTimer * 10)}</p> @@ -246,10 +243,10 @@ export function RecordingView(props: IRecordingViewProps) { {!recording && (videos.length > 0 ? ( <div className="options-wrapper video-edit-wrapper"> - <IconContext.Provider value={{ color: 'grey', className: 'video-edit-buttons', style: { display: canUndo ? 'inherit' : 'none' } }}> + <IconContext.Provider value={iconUndoVals}> <MdBackspace onPointerDown={undoPrevious} /> </IconContext.Provider> - <IconContext.Provider value={{ color: '#cc1c08', className: 'video-edit-buttons' }}> + <IconContext.Provider value={iconVals}> <FaCheckCircle onPointerDown={e => { e.stopPropagation(); @@ -268,7 +265,7 @@ export function RecordingView(props: IRecordingViewProps) { setTrackScreen(e.target.checked); }} /> - <span className="checkmark"></span> + <span className="checkmark" /> Track Screen </label> </div> diff --git a/src/client/views/nodes/RecordingBox/index.ts b/src/client/views/nodes/RecordingBox/index.ts index ff21eaed6..e4f9b5e55 100644 --- a/src/client/views/nodes/RecordingBox/index.ts +++ b/src/client/views/nodes/RecordingBox/index.ts @@ -1,2 +1,2 @@ -export * from './RecordingView' -export * from './RecordingBox'
\ No newline at end of file +export * from './RecordingView'; +export * from './RecordingBox'; diff --git a/src/client/views/nodes/ScreenshotBox.tsx b/src/client/views/nodes/ScreenshotBox.tsx index 1e3933ac3..3be50f5e6 100644 --- a/src/client/views/nodes/ScreenshotBox.tsx +++ b/src/client/views/nodes/ScreenshotBox.tsx @@ -1,33 +1,37 @@ +/* eslint-disable jsx-a11y/media-has-caption */ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import * as React from 'react'; // import { Canvas } from '@react-three/fiber'; import { computed, makeObservable, observable, runInAction } from 'mobx'; import { observer } from 'mobx-react'; // import { BufferAttribute, Camera, Vector2, Vector3 } from 'three'; +import { returnFalse, returnOne, returnZero } from '../../../ClientUtils'; +import { emptyFunction } from '../../../Utils'; import { DateField } from '../../../fields/DateField'; import { Doc } from '../../../fields/Doc'; +import { DocData } from '../../../fields/DocSymbols'; import { Id } from '../../../fields/FieldSymbols'; import { ComputedField } from '../../../fields/ScriptField'; import { Cast, DocCast, NumCast } from '../../../fields/Types'; import { AudioField, VideoField } from '../../../fields/URLField'; import { TraceMobx } from '../../../fields/util'; -import { emptyFunction, returnFalse, returnOne, returnZero } from '../../../Utils'; -import { DocUtils } from '../../documents/Documents'; -import { DocumentType } from '../../documents/DocumentTypes'; import { Networking } from '../../Network'; +import { DocUtils } from '../../documents/DocUtils'; +import { DocumentType } from '../../documents/DocumentTypes'; +import { Docs } from '../../documents/Documents'; import { CaptureManager } from '../../util/CaptureManager'; import { SettingsManager } from '../../util/SettingsManager'; import { TrackMovements } from '../../util/TrackMovements'; -import { CollectionFreeFormView } from '../collections/collectionFreeForm/CollectionFreeFormView'; -import { CollectionStackedTimeline } from '../collections/CollectionStackedTimeline'; import { ContextMenu } from '../ContextMenu'; import { ViewBoxAnnotatableComponent } from '../DocComponent'; -import { media_state } from './AudioBox'; +import { DocViewUtils } from '../DocViewUtils'; +import { CollectionStackedTimeline } from '../collections/CollectionStackedTimeline'; +import { CollectionFreeFormView } from '../collections/collectionFreeForm/CollectionFreeFormView'; +import { mediaState } from './AudioBox'; import { FieldView, FieldViewProps } from './FieldView'; -import { FormattedTextBox } from './formattedText/FormattedTextBox'; import './ScreenshotBox.scss'; import { VideoBox } from './VideoBox'; -import { DocData } from '../../../fields/DocSymbols'; +import { FormattedTextBox } from './formattedText/FormattedTextBox'; declare class MediaRecorder { constructor(e: any, options?: any); // whatever MediaRecorder has @@ -158,11 +162,11 @@ export class ScreenshotBox extends ViewBoxAnnotatableComponent<FieldViewProps>() // }); } componentWillUnmount() { - const ind = DocUtils.ActiveRecordings.indexOf(this); - ind !== -1 && DocUtils.ActiveRecordings.splice(ind, 1); + const ind = DocViewUtils.ActiveRecordings.indexOf(this); + ind !== -1 && DocViewUtils.ActiveRecordings.splice(ind, 1); } - specificContextMenu = (e: React.MouseEvent): void => { + specificContextMenu = (): void => { const subitems = [{ description: 'Screen Capture', event: this.toggleRecording, icon: 'expand-arrows-alt' as any }]; ContextMenu.Instance.addItem({ description: 'Options...', subitems, icon: 'video' }); }; @@ -170,12 +174,12 @@ export class ScreenshotBox extends ViewBoxAnnotatableComponent<FieldViewProps>() @computed get content() { return ( <video - className={'videoBox-content'} + className="videoBox-content" key="video" ref={r => { this._videoRef = r; setTimeout(() => { - if (this.layoutDoc.mediaState === media_state.PendingRecording && this._videoRef) { + if (this.layoutDoc.mediaState === mediaState.PendingRecording && this._videoRef) { this.toggleRecording(); } }, 100); @@ -183,7 +187,7 @@ export class ScreenshotBox extends ViewBoxAnnotatableComponent<FieldViewProps>() autoPlay={this._screenCapture} style={{ width: this._screenCapture ? '100%' : undefined, height: this._screenCapture ? '100%' : undefined }} onCanPlay={this.videoLoad} - controls={true} + controls onClick={e => e.preventDefault()}> <source type="video/mp4" /> Not supported. @@ -220,23 +224,23 @@ export class ScreenshotBox extends ViewBoxAnnotatableComponent<FieldViewProps>() toggleRecording = async () => { if (!this._screenCapture) { this._audioRec = new MediaRecorder(await navigator.mediaDevices.getUserMedia({ audio: true })); - const aud_chunks: any = []; - this._audioRec.ondataavailable = (e: any) => aud_chunks.push(e.data); - this._audioRec.onstop = async (e: any) => { - const [{ result }] = await Networking.UploadFilesToServer(aud_chunks.map((file: any) => ({ file }))); + const audChunks: any = []; + this._audioRec.ondataavailable = (e: any) => audChunks.push(e.data); + this._audioRec.onstop = async () => { + const [{ result }] = await Networking.UploadFilesToServer(audChunks.map((file: any) => ({ file }))); if (!(result instanceof Error)) { this.dataDoc[this._props.fieldKey + '_audio'] = new AudioField(result.accessPaths.agnostic.client); } }; this._videoRef!.srcObject = await (navigator.mediaDevices as any).getDisplayMedia({ video: true }); this._videoRec = new MediaRecorder(this._videoRef!.srcObject); - const vid_chunks: any = []; + const vidChunks: any = []; this._videoRec.onstart = () => { if (this.dataDoc[this._props.fieldKey + '_trackScreen']) TrackMovements.Instance.start(); this.dataDoc[this._props.fieldKey + '_recordingStart'] = new DateField(new Date()); }; - this._videoRec.ondataavailable = (e: any) => vid_chunks.push(e.data); - this._videoRec.onstop = async (e: any) => { + this._videoRec.ondataavailable = (e: any) => vidChunks.push(e.data); + this._videoRec.onstop = async () => { const presentation = TrackMovements.Instance.yieldPresentation(); if (presentation?.movements) { const presCopy = { ...presentation }; @@ -244,7 +248,7 @@ export class ScreenshotBox extends ViewBoxAnnotatableComponent<FieldViewProps>() this.dataDoc[this.fieldKey + '_presentation'] = JSON.stringify(presCopy); } TrackMovements.Instance.finish(); - const file = new File(vid_chunks, `${this.Document[Id]}.mkv`, { type: vid_chunks[0].type, lastModified: Date.now() }); + const file = new File(vidChunks, `${this.Document[Id]}.mkv`, { type: vidChunks[0].type, lastModified: Date.now() }); const [{ result }] = await Networking.UploadFilesToServer({ file }); this.dataDoc[this.fieldKey + '_duration'] = (new Date().getTime() - this.recordingStart!) / 1000; if (!(result instanceof Error)) { @@ -262,7 +266,7 @@ export class ScreenshotBox extends ViewBoxAnnotatableComponent<FieldViewProps>() this._screenCapture = true; this.dataDoc.mediaState = 'recording'; }); - DocUtils.ActiveRecordings.push(this); + DocViewUtils.ActiveRecordings.push(this); } else { this._audioRec?.stop(); this._videoRec?.stop(); @@ -270,8 +274,8 @@ export class ScreenshotBox extends ViewBoxAnnotatableComponent<FieldViewProps>() this._screenCapture = false; this.dataDoc.mediaState = 'paused'; }); - const ind = DocUtils.ActiveRecordings.indexOf(this); - ind !== -1 && DocUtils.ActiveRecordings.splice(ind, 1); + const ind = DocViewUtils.ActiveRecordings.indexOf(this); + ind !== -1 && DocViewUtils.ActiveRecordings.splice(ind, 1); CaptureManager.Instance.open(this.Document); } @@ -297,6 +301,7 @@ export class ScreenshotBox extends ViewBoxAnnotatableComponent<FieldViewProps>() <div className="videoBox-viewer"> <div style={{ position: 'relative', height: this.videoPanelHeight() }}> <CollectionFreeFormView + // eslint-disable-next-line react/jsx-props-no-spreading {...this._props} setContentViewBox={emptyFunction} NativeWidth={returnZero} @@ -305,7 +310,7 @@ export class ScreenshotBox extends ViewBoxAnnotatableComponent<FieldViewProps>() PanelWidth={this._props.PanelWidth} focus={this._props.focus} isSelected={this._props.isSelected} - isAnnotationOverlay={true} + isAnnotationOverlay select={emptyFunction} isContentActive={returnFalse} NativeDimScaling={returnOne} @@ -324,9 +329,10 @@ export class ScreenshotBox extends ViewBoxAnnotatableComponent<FieldViewProps>() <div style={{ background: SettingsManager.userColor, position: 'relative', height: this.formattedPanelHeight() }}> {!(this.dataDoc[this.fieldKey + '_dictation'] instanceof Doc) ? null : ( <FormattedTextBox + // eslint-disable-next-line react/jsx-props-no-spreading {...this._props} Document={DocCast(this.dataDoc[this.fieldKey + '_dictation'])} - fieldKey={'text'} + fieldKey="text" PanelHeight={this.formattedPanelHeight} select={emptyFunction} isContentActive={emptyFunction} @@ -353,3 +359,8 @@ export class ScreenshotBox extends ViewBoxAnnotatableComponent<FieldViewProps>() ); } } + +Docs.Prototypes.TemplateMap.set(DocumentType.SCREENSHOT, { + layout: { view: ScreenshotBox, dataField: 'data' }, + options: { acl: '', _layout_nativeDimEditable: true, systemIcon: 'BsCameraFill' }, +}); diff --git a/src/client/views/nodes/ScriptingBox.tsx b/src/client/views/nodes/ScriptingBox.tsx index 8c65fd34e..bc19d7ad1 100644 --- a/src/client/views/nodes/ScriptingBox.tsx +++ b/src/client/views/nodes/ScriptingBox.tsx @@ -1,8 +1,9 @@ -let ReactTextareaAutocomplete = require('@webscopeio/react-textarea-autocomplete').default; +/* eslint-disable react/button-has-type */ +/* eslint-disable jsx-a11y/no-static-element-interactions */ import { action, computed, makeObservable, observable } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; -import { returnAlways, returnEmptyString } from '../../../Utils'; +import { returnAlways, returnEmptyString } from '../../../ClientUtils'; import { Doc } from '../../../fields/Doc'; import { List } from '../../../fields/List'; import { listSpec } from '../../../fields/Schema'; @@ -17,10 +18,14 @@ import { ContextMenu } from '../ContextMenu'; import { ViewBoxAnnotatableComponent } from '../DocComponent'; import { EditableView } from '../EditableView'; import { OverlayView } from '../OverlayView'; -import { FieldView, FieldViewProps } from '../nodes/FieldView'; +import { FieldView, FieldViewProps } from './FieldView'; import { DocumentIconContainer } from './DocumentIcon'; import './ScriptingBox.scss'; +import { Docs } from '../../documents/Documents'; +import { DocumentType } from '../../documents/DocumentTypes'; + const _global = (window /* browser */ || global) /* node */ as any; +const ReactTextareaAutocomplete = require('@webscopeio/react-textarea-autocomplete').default; @observer export class ScriptingBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { @@ -79,26 +84,24 @@ export class ScriptingBox extends ViewBoxAnnotatableComponent<FieldViewProps>() @computed({ keepAlive: true }) get rawScript() { return ScriptCast(this.dataDoc[this.fieldKey])?.script.originalScript ?? ''; } - @computed({ keepAlive: true }) get functionName() { - return StrCast(this.dataDoc[this.fieldKey + '-functionName'], ''); - } - @computed({ keepAlive: true }) get functionDescription() { - return StrCast(this.dataDoc[this.fieldKey + '-functionDescription'], ''); - } - @computed({ keepAlive: true }) get compileParams() { - return Cast(this.dataDoc[this.fieldKey + '-params'], listSpec('string'), []); - } - set rawScript(value) { this.dataDoc[this.fieldKey] = new ScriptField(undefined, undefined, value); } + @computed({ keepAlive: true }) get functionName() { + return StrCast(this.dataDoc[this.fieldKey + '-functionName'], ''); + } set functionName(value) { this.dataDoc[this.fieldKey + '-functionName'] = value; } + @computed({ keepAlive: true }) get functionDescription() { + return StrCast(this.dataDoc[this.fieldKey + '-functionDescription'], ''); + } set functionDescription(value) { this.dataDoc[this.fieldKey + '-functionDescription'] = value; } - + @computed({ keepAlive: true }) get compileParams() { + return Cast(this.dataDoc[this.fieldKey + '-params'], listSpec('string'), []); + } set compileParams(value) { this.dataDoc[this.fieldKey + '-params'] = new List<string>(value); } @@ -107,9 +110,8 @@ export class ScriptingBox extends ViewBoxAnnotatableComponent<FieldViewProps>() if (typeof result === 'object') { const text = descrip ? result[1] : result[2]; return text !== undefined ? text : ''; - } else { - return ''; } + return ''; } onClickScriptDisable = returnAlways; @@ -118,19 +120,18 @@ export class ScriptingBox extends ViewBoxAnnotatableComponent<FieldViewProps>() componentDidMount() { this._props.setContentViewBox?.(this); this.rawText = this.rawScript; - const observer = new _global.ResizeObserver( - action((entries: any) => { + const resizeObserver = new _global.ResizeObserver( + action(() => { const area = document.querySelector('textarea'); if (area) { - for (const {} of entries) { - const getCaretCoordinates = require('textarea-caret'); - const caret = getCaretCoordinates(area, this._selection); - this.resetSuggestionPos(caret); - } + // eslint-disable-next-line global-require + const getCaretCoordinates = require('textarea-caret'); + const caret = getCaretCoordinates(area, this._selection); + this.resetSuggestionPos(caret); } }) ); - observer.observe(document.getElementsByClassName('scriptingBox-outerDiv')[0]); + resizeObserver.observe(document.getElementsByClassName('scriptingBox-outerDiv')[0]); } @action @@ -138,12 +139,12 @@ export class ScriptingBox extends ViewBoxAnnotatableComponent<FieldViewProps>() if (!this._suggestionRef.current || !this._scriptTextRef.current) return; const suggestionWidth = this._suggestionRef.current.offsetWidth; const scriptWidth = this._scriptTextRef.current.offsetWidth; - const top = caret.top; - const x = this.dataDoc.x; - let left = caret.left; + const { top } = caret; + const { x } = this.dataDoc; + let { left } = caret; if (left + suggestionWidth > x + scriptWidth) { const diff = left + suggestionWidth - (x + scriptWidth); - left = left - diff; + left -= diff; } this._suggestionBoxX = left; @@ -155,7 +156,7 @@ export class ScriptingBox extends ViewBoxAnnotatableComponent<FieldViewProps>() } protected createDashEventsTarget = (ele: HTMLDivElement, dropFunc: (e: Event, de: DragManager.DropEvent) => void) => { - //used for stacking and masonry view + // used for stacking and masonry view if (ele) { this.dropDisposer?.(); this.dropDisposer = DragManager.MakeDropTarget(ele, dropFunc, this.layoutDoc); @@ -164,7 +165,9 @@ export class ScriptingBox extends ViewBoxAnnotatableComponent<FieldViewProps>() // only included in buttons, transforms scripting UI to a button @action - onFinish = () => (this.layoutDoc.layout_fieldKey = 'layout'); + onFinish = () => { + this.layoutDoc.layout_fieldKey = 'layout'; + }; // displays error message @action @@ -176,7 +179,9 @@ export class ScriptingBox extends ViewBoxAnnotatableComponent<FieldViewProps>() @action onCompile = () => { const params: ScriptParam = {}; - this.compileParams.forEach(p => (params[p.split(':')[0].trim()] = p.split(':')[1].trim())); + this.compileParams.forEach(p => { + params[p.split(':')[0].trim()] = p.split(':')[1].trim(); + }); const result = !this.rawText.trim() ? ({ compiled: false, errors: undefined } as any) @@ -196,7 +201,9 @@ export class ScriptingBox extends ViewBoxAnnotatableComponent<FieldViewProps>() onRun = () => { if (this.onCompile()) { const bindings: { [name: string]: any } = {}; - this.paramsNames.forEach(key => (bindings[key] = this.dataDoc[key])); + this.paramsNames.forEach(key => { + bindings[key] = this.dataDoc[key]; + }); // binds vars so user doesnt have to refer to everything as this.<var> ScriptCast(this.dataDoc[this.fieldKey], null)?.script.run({ ...bindings, this: this.Document }, this.onError); } @@ -247,7 +254,7 @@ export class ScriptingBox extends ViewBoxAnnotatableComponent<FieldViewProps>() this.dataDoc.name = this.functionName; this.dataDoc.description = this.functionDescription; - //this.dataDoc.parameters = this.compileParams; + // this.dataDoc.parameters = this.compileParams; this.dataDoc.script = this.rawScript; ScriptManager.Instance.addScript(this.dataDoc); @@ -255,6 +262,7 @@ export class ScriptingBox extends ViewBoxAnnotatableComponent<FieldViewProps>() this._scriptKeys = ScriptingGlobals.getGlobals(); this._scriptingDescriptions = ScriptingGlobals.getDescriptions(); this._scriptingParams = ScriptingGlobals.getParameters(); + return undefined; }; // overlays document numbers (ex. d32) over all documents when clicked on @@ -267,7 +275,9 @@ export class ScriptingBox extends ViewBoxAnnotatableComponent<FieldViewProps>() @action onDrop = (e: Event, de: DragManager.DropEvent, fieldKey: string) => { if (de.complete.docDragData) { - de.complete.docDragData.droppedDocuments.forEach(doc => (this.dataDoc[fieldKey] = doc)); + de.complete.docDragData.droppedDocuments.forEach(doc => { + this.dataDoc[fieldKey] = doc; + }); e.stopPropagation(); return true; } @@ -285,8 +295,7 @@ export class ScriptingBox extends ViewBoxAnnotatableComponent<FieldViewProps>() // sets field of the param name to the selected value in drop down box @action viewChanged = (e: React.ChangeEvent, name: string) => { - //@ts-ignore - const val = e.target.selectedOptions[0].value; + const val = (e.target as any).selectedOptions[0].value; this.dataDoc[name] = val[0] === 'S' ? val.substring(1) : val[0] === 'N' ? parseInt(val.substring(1)) : val.substring(1) === 'true'; }; @@ -306,8 +315,26 @@ export class ScriptingBox extends ViewBoxAnnotatableComponent<FieldViewProps>() }; renderFunctionInputs() { - const descriptionInput = <textarea className="scriptingBox-textarea-inputs" onChange={e => (this.functionDescription = e.target.value)} placeholder="enter description here" value={this.functionDescription} />; - const nameInput = <textarea className="scriptingBox-textarea-inputs" onChange={e => (this.functionName = e.target.value)} placeholder="enter name here" value={this.functionName} />; + const descriptionInput = ( + <textarea + className="scriptingBox-textarea-inputs" + onChange={e => { + this.functionDescription = e.target.value; + }} + placeholder="enter description here" + value={this.functionDescription} + /> + ); + const nameInput = ( + <textarea + className="scriptingBox-textarea-inputs" + onChange={e => { + this.functionName = e.target.value; + }} + placeholder="enter name here" + value={this.functionName} + /> + ); return ( <div className="scriptingBox-inputDiv" onPointerDown={e => this._props.isSelected() && e.stopPropagation()}> @@ -339,7 +366,7 @@ export class ScriptingBox extends ViewBoxAnnotatableComponent<FieldViewProps>() return ( <div className="scriptingBox-paramInputs" onFocus={this.onFocus} onBlur={() => this._overlayDisposer?.()} ref={ele => ele && this.createDashEventsTarget(ele, (e, de) => this.onDrop(e, de, parameter))}> <EditableView - display={'block'} + display="block" maxHeight={72} height={35} fontSize={14} @@ -371,7 +398,7 @@ export class ScriptingBox extends ViewBoxAnnotatableComponent<FieldViewProps>() return ( <div className="scriptingBox-paramInputs" style={{ overflowY: 'hidden' }}> <EditableView - display={'block'} + display="block" maxHeight={72} height={35} fontSize={14} @@ -404,6 +431,7 @@ export class ScriptingBox extends ViewBoxAnnotatableComponent<FieldViewProps>() onChange={e => this.viewChanged(e, parameter)} value={typeof this.dataDoc[parameter] === 'string' ? 'S' + StrCast(this.dataDoc[parameter]) : typeof this.dataDoc[parameter] === 'number' ? 'N' + NumCast(this.dataDoc[parameter]) : 'B' + BoolCast(this.dataDoc[parameter])}> {types.map((type, i) => ( + // eslint-disable-next-line react/no-array-index-key <option key={i} className="scriptingBox-viewOption" value={(typeof type === 'string' ? 'S' : typeof type === 'number' ? 'N' : 'B') + type}> {' '} {type.toString()}{' '} @@ -496,6 +524,7 @@ export class ScriptingBox extends ViewBoxAnnotatableComponent<FieldViewProps>() @action suggestionPos = () => { + // eslint-disable-next-line global-require const getCaretCoordinates = require('textarea-caret'); const This = this; document.querySelector('textarea')?.addEventListener('input', function () { @@ -509,7 +538,7 @@ export class ScriptingBox extends ViewBoxAnnotatableComponent<FieldViewProps>() keyHandler(e: any, pos: number) { e.stopPropagation(); if (this._lastChar === 'Enter') { - this.rawText = this.rawText + ' '; + this.rawText += ' '; } if (e.key === '(') { this.suggestionPos(); @@ -524,20 +553,18 @@ export class ScriptingBox extends ViewBoxAnnotatableComponent<FieldViewProps>() } } else if (e.key === ')') { this._paramSuggestion = false; - } else { - if (e.key === 'Backspace') { - if (this._lastChar === '(') { - this._paramSuggestion = false; - } else if (this._lastChar === ')') { - if (this.rawText.slice(0, this.rawText.length - 1).split('(').length - 1 > this.rawText.slice(0, this.rawText.length - 1).split(')').length - 1) { - if (this._scriptParamsText.length > 0) { - this._paramSuggestion = true; - } + } else if (e.key === 'Backspace') { + if (this._lastChar === '(') { + this._paramSuggestion = false; + } else if (this._lastChar === ')') { + if (this.rawText.slice(0, this.rawText.length - 1).split('(').length - 1 > this.rawText.slice(0, this.rawText.length - 1).split(')').length - 1) { + if (this._scriptParamsText.length > 0) { + this._paramSuggestion = true; } } - } else if (this.rawText.split('(').length - 1 <= this.rawText.split(')').length - 1) { - this._paramSuggestion = false; } + } else if (this.rawText.split('(').length - 1 <= this.rawText.split(')').length - 1) { + this._paramSuggestion = false; } this._lastChar = e.key === 'Backspace' ? this.rawText[this.rawText.length - 2] : e.key; @@ -559,9 +586,9 @@ export class ScriptingBox extends ViewBoxAnnotatableComponent<FieldViewProps>() parameters.forEach((element: string, i: number) => { if (i < numEntered - 1) { - first = first + element; + first += element; } else if (i > numEntered - 1) { - last = last + element; + last += element; } }); @@ -598,16 +625,16 @@ export class ScriptingBox extends ViewBoxAnnotatableComponent<FieldViewProps>() placeholder="write your script here" onFocus={this.onFocus} onBlur={() => this._overlayDisposer?.()} - onChange={action((e: any) => (this.rawText = e.target.value))} + onChange={action((e: any) => { + this.rawText = e.target.value; + })} value={this.rawText} - movePopupAsYouType={true} + movePopupAsYouType loadingComponent={() => <span>Loading</span>} trigger={{ ' ': { dataProvider: (token: any) => this.handleToken(token), - component: (blob: any) => { - return this.renderFuncListElement(blob.entity); - }, + component: (blob: any) => this.renderFuncListElement(blob.entity), output: (item: any, trigger: any) => { this._spaced = true; return trigger + item.trim(); @@ -615,9 +642,7 @@ export class ScriptingBox extends ViewBoxAnnotatableComponent<FieldViewProps>() }, '.': { dataProvider: (token: any) => this.handleToken(token), - component: (blob: any) => { - return this.renderFuncListElement(blob.entity); - }, + component: (blob: any) => this.renderFuncListElement(blob.entity), output: (item: any, trigger: any) => { this._spaced = true; return trigger + item.trim(); @@ -653,16 +678,7 @@ export class ScriptingBox extends ViewBoxAnnotatableComponent<FieldViewProps>() // params box on bottom const parameterInput = ( <div className="scriptingBox-params"> - <EditableView - display={'block'} - maxHeight={72} - height={35} - fontSize={22} - contents={''} - GetValue={returnEmptyString} - SetValue={value => (value && value !== ' ' ? this.compileParam(value) : false)} - placeholder={'enter parameters here'} - /> + <EditableView display="block" maxHeight={72} height={35} fontSize={22} contents="" GetValue={returnEmptyString} SetValue={value => (value && value !== ' ' ? this.compileParam(value) : false)} placeholder="enter parameters here" /> </div> ); @@ -670,13 +686,14 @@ export class ScriptingBox extends ViewBoxAnnotatableComponent<FieldViewProps>() const definedParameters = !this.compileParams.length ? null : ( <div className="scriptingBox-plist" style={{ width: '30%' }}> {this.compileParams.map((parameter, i) => ( + // eslint-disable-next-line react/no-array-index-key <div key={i} className="scriptingBox-pborder" onKeyDown={e => e.key === 'Enter' && this._overlayDisposer?.()}> <EditableView - display={'block'} + display="block" maxHeight={72} height={35} fontSize={12} - background-color={'beige'} + background-color="beige" contents={parameter} GetValue={() => parameter} SetValue={value => (value && value !== ' ' ? this.compileParam(value, i) : this.onDelete(i))} @@ -749,6 +766,7 @@ export class ScriptingBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {!this.compileParams.length || !this.paramsNames ? null : ( <div className="scriptingBox-plist"> {this.paramsNames.map((parameter: string, i: number) => ( + // eslint-disable-next-line react/no-array-index-key <div key={i} className="scriptingBox-pborder" onKeyDown={e => e.key === 'Enter' && this._overlayDisposer?.()}> <div className="scriptingBox-wrapper" style={{ maxHeight: '40px' }}> <div className="scriptingBox-paramNames"> {`${parameter}:${this.paramsTypes[i]} = `} </div> @@ -829,3 +847,8 @@ export class ScriptingBox extends ViewBoxAnnotatableComponent<FieldViewProps>() ); } } + +Docs.Prototypes.TemplateMap.set(DocumentType.SCRIPTING, { + layout: { view: ScriptingBox, dataField: 'data' }, + options: { acl: '', systemIcon: 'BsFileEarmarkCodeFill' }, +}); diff --git a/src/client/views/nodes/SliderBox-components.tsx b/src/client/views/nodes/SliderBox-components.tsx index e19f28f08..04efaac8e 100644 --- a/src/client/views/nodes/SliderBox-components.tsx +++ b/src/client/views/nodes/SliderBox-components.tsx @@ -1,6 +1,6 @@ -import * as React from "react"; -import { SliderItem } from "react-compound-slider"; -import "./SliderBox-tooltip.css"; +import * as React from 'react'; +import { SliderItem } from 'react-compound-slider'; +import './SliderBox-tooltip.css'; const { Component, Fragment } = React; @@ -8,24 +8,24 @@ const { Component, Fragment } = React; // TOOLTIP RAIL // ******************************************************* const railStyle: React.CSSProperties = { - position: "absolute", - width: "100%", + position: 'absolute', + width: '100%', height: 40, borderRadius: 7, - cursor: "pointer", + cursor: 'pointer', opacity: 0.3, zIndex: 300, - border: "1px solid grey" + border: '1px solid grey', }; const railCenterStyle: React.CSSProperties = { - position: "absolute", - width: "100%", + position: 'absolute', + width: '100%', height: 14, borderRadius: 7, - cursor: "pointer", - pointerEvents: "none", - backgroundColor: "rgb(155,155,155)" + cursor: 'pointer', + pointerEvents: 'none', + backgroundColor: 'rgb(155,155,155)', }; interface TooltipRailProps { @@ -37,21 +37,21 @@ interface TooltipRailProps { export class TooltipRail extends Component<TooltipRailProps> { state = { value: null, - percent: null + percent: null, }; static defaultProps = { - disabled: false + disabled: false, }; onMouseEnter = () => { - document.addEventListener("mousemove", this.onMouseMove); - } + document.addEventListener('mousemove', this.onMouseMove); + }; onMouseLeave = () => { this.setState({ value: null, percent: null }); - document.removeEventListener("mousemove", this.onMouseMove); - } + document.removeEventListener('mousemove', this.onMouseMove); + }; onMouseMove = (e: Event) => { const { activeHandleID, getEventData } = this.props; @@ -61,7 +61,7 @@ export class TooltipRail extends Component<TooltipRailProps> { } else { this.setState(getEventData(e)); } - } + }; render() { const { value, percent } = this.state; @@ -73,11 +73,10 @@ export class TooltipRail extends Component<TooltipRailProps> { <div style={{ left: `${percent}%`, - position: "absolute", - marginLeft: "-11px", - marginTop: "-35px" - }} - > + position: 'absolute', + marginLeft: '-11px', + marginTop: '-35px', + }}> <div className="tooltip"> <span className="tooltiptext">Value: {value}</span> </div> @@ -87,7 +86,7 @@ export class TooltipRail extends Component<TooltipRailProps> { style={railStyle} {...getRailProps({ onMouseEnter: this.onMouseEnter, - onMouseLeave: this.onMouseLeave + onMouseLeave: this.onMouseLeave, })} /> <div style={railCenterStyle} /> @@ -102,28 +101,28 @@ export class TooltipRail extends Component<TooltipRailProps> { interface HandleProps { key: string; handle: SliderItem; - isActive: Boolean; - disabled?: Boolean; + isActive: boolean; + disabled?: boolean; domain: number[]; getHandleProps: (id: string, config: object) => object; } export class Handle extends Component<HandleProps> { static defaultProps = { - disabled: false + disabled: false, }; state = { - mouseOver: false + mouseOver: false, }; onMouseEnter = () => { this.setState({ mouseOver: true }); - } + }; onMouseLeave = () => { this.setState({ mouseOver: false }); - } + }; render() { const { @@ -131,7 +130,7 @@ export class Handle extends Component<HandleProps> { handle: { id, value, percent }, isActive, disabled, - getHandleProps + getHandleProps, } = this.props; const { mouseOver } = this.state; @@ -141,11 +140,10 @@ export class Handle extends Component<HandleProps> { <div style={{ left: `${percent}%`, - position: "absolute", - marginLeft: "-11px", - marginTop: "-35px" - }} - > + position: 'absolute', + marginLeft: '-11px', + marginTop: '-35px', + }}> <div className="tooltip"> <span className="tooltiptext">Value: {value}</span> </div> @@ -158,21 +156,21 @@ export class Handle extends Component<HandleProps> { aria-valuenow={value} style={{ left: `${percent}%`, - position: "absolute", - marginLeft: "-11px", - marginTop: "-6px", + position: 'absolute', + marginLeft: '-11px', + marginTop: '-6px', zIndex: 400, width: 24, height: 24, - cursor: "pointer", + cursor: 'pointer', border: 0, - borderRadius: "50%", - boxShadow: "1px 1px 1px 1px rgba(0, 0, 0, 0.4)", - backgroundColor: disabled ? "#666" : "#3e1db3" + borderRadius: '50%', + boxShadow: '1px 1px 1px 1px rgba(0, 0, 0, 0.4)', + backgroundColor: disabled ? '#666' : '#3e1db3', }} {...getHandleProps(id, { onMouseEnter: this.onMouseEnter, - onMouseLeave: this.onMouseLeave + onMouseLeave: this.onMouseLeave, })} /> </Fragment> @@ -186,27 +184,22 @@ export class Handle extends Component<HandleProps> { interface TrackProps { source: SliderItem; target: SliderItem; - disabled: Boolean; + disabled: boolean; getTrackProps: () => object; } -export function Track({ - source, - target, - getTrackProps, - disabled = false -}: TrackProps) { +export function Track({ source, target, getTrackProps, disabled = false }: TrackProps) { return ( <div style={{ - position: "absolute", + position: 'absolute', height: 14, zIndex: 1, - backgroundColor: disabled ? "#999" : "#3e1db3", + backgroundColor: disabled ? '#999' : '#3e1db3', borderRadius: 7, - cursor: "pointer", + cursor: 'pointer', left: `${source.percent}%`, - width: `${target.percent - source.percent}%` + width: `${target.percent - source.percent}%`, }} {...getTrackProps()} /> @@ -222,32 +215,31 @@ interface TickProps { format: (val: number) => string; } -const defaultFormat = (d: number) => `d`; +const defaultFormat = () => `d`; export function Tick({ tick, count, format = defaultFormat }: TickProps) { return ( <div> <div style={{ - position: "absolute", + position: 'absolute', marginTop: 20, width: 1, height: 5, - backgroundColor: "rgb(200,200,200)", - left: `${tick.percent}%` + backgroundColor: 'rgb(200,200,200)', + left: `${tick.percent}%`, }} /> <div style={{ - position: "absolute", + position: 'absolute', marginTop: 25, fontSize: 10, - textAlign: "center", + textAlign: 'center', marginLeft: `${-(100 / count) / 2}%`, width: `${100 / count}%`, - left: `${tick.percent}%` - }} - > + left: `${tick.percent}%`, + }}> {format(tick.value)} </div> </div> diff --git a/src/client/views/nodes/TaskCompletedBox.tsx b/src/client/views/nodes/TaskCompletedBox.tsx index c9e15d314..6f11cd73a 100644 --- a/src/client/views/nodes/TaskCompletedBox.tsx +++ b/src/client/views/nodes/TaskCompletedBox.tsx @@ -5,7 +5,7 @@ import * as React from 'react'; import './TaskCompletedBox.scss'; @observer -export class TaskCompletionBox extends React.Component<{}> { +export class TaskCompletionBox extends React.Component { @observable public static taskCompleted: boolean = false; @observable public static popupX: number = 500; @observable public static popupY: number = 150; diff --git a/src/client/views/nodes/VideoBox.tsx b/src/client/views/nodes/VideoBox.tsx index 4773a21c9..afd73cfe8 100644 --- a/src/client/views/nodes/VideoBox.tsx +++ b/src/client/views/nodes/VideoBox.tsx @@ -1,22 +1,22 @@ +/* eslint-disable jsx-a11y/media-has-caption */ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { action, computed, IReactionDisposer, makeObservable, observable, ObservableMap, reaction, runInAction } from 'mobx'; import { observer } from 'mobx-react'; import { basename } from 'path'; import * as React from 'react'; +import { ClientUtils, returnEmptyString, returnFalse, returnOne, returnZero, setupMoveUpEvents } from '../../../ClientUtils'; import { Doc, Opt, StrListCast } from '../../../fields/Doc'; import { DocData } from '../../../fields/DocSymbols'; import { InkTool } from '../../../fields/InkField'; import { List } from '../../../fields/List'; import { ObjectField } from '../../../fields/ObjectField'; -import { Cast, NumCast, StrCast } from '../../../fields/Types'; +import { Cast, NumCast, StrCast, toList } from '../../../fields/Types'; import { AudioField, ImageField, VideoField } from '../../../fields/URLField'; -import { emptyFunction, formatTime, returnEmptyString, returnFalse, returnOne, returnZero, setupMoveUpEvents, Utils } from '../../../Utils'; -import { Docs, DocUtils } from '../../documents/Documents'; +import { emptyFunction, formatTime } from '../../../Utils'; +import { Docs } from '../../documents/Documents'; import { DocumentType } from '../../documents/DocumentTypes'; -import { DocumentManager } from '../../util/DocumentManager'; -import { dropActionType } from '../../util/DragManager'; -import { FollowLinkScript } from '../../util/LinkFollower'; -import { LinkManager } from '../../util/LinkManager'; +import { DocUtils, FollowLinkScript } from '../../documents/DocUtils'; +import { dropActionType } from '../../util/DropActionTypes'; import { ReplayMovements } from '../../util/ReplayMovements'; import { undoBatch } from '../../util/UndoManager'; import { CollectionFreeFormView } from '../collections/collectionFreeForm/CollectionFreeFormView'; @@ -24,13 +24,15 @@ import { CollectionStackedTimeline, TrimScope } from '../collections/CollectionS import { ContextMenu } from '../ContextMenu'; import { ContextMenuProps } from '../ContextMenuItem'; import { ViewBoxAnnotatableComponent, ViewBoxInterface } from '../DocComponent'; +import { VideoThumbnails } from '../global/globalEnums'; import { MarqueeAnnotator } from '../MarqueeAnnotator'; import { AnchorMenu } from '../pdf/AnchorMenu'; -import { StyleProp } from '../StyleProvider'; +import { PinDocView, PinProps } from '../PinFuncs'; +import { StyleProp } from '../StyleProp'; import { DocumentView } from './DocumentView'; -import { FieldView, FieldViewProps, FocusViewOptions } from './FieldView'; +import { FieldView, FieldViewProps } from './FieldView'; +import { FocusViewOptions } from './FocusViewOptions'; import { RecordingBox } from './RecordingBox'; -import { PinProps, PresBox } from './trails'; import './VideoBox.scss'; /** @@ -51,7 +53,6 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps>() impl return FieldView.LayoutString(VideoBox, fieldKey); } static heightPercent = 80; // height of video relative to videoBox when timeline is open - static numThumbnails = 20; private unmounting = false; private _disposers: { [name: string]: IReactionDisposer } = {}; private _videoRef: HTMLVideoElement | null = null; // <video> ref @@ -62,6 +63,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps>() impl private _annotationLayer: React.RefObject<HTMLDivElement> = React.createRef(); private _playRegionTimer: any = null; // timeout for playback private _controlsFadeTimer: any = null; // timeout for controls fade + private _ffref = React.createRef<CollectionFreeFormView>(); constructor(props: FieldViewProps) { super(props); @@ -84,7 +86,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps>() impl @observable _scrubbing: boolean = false; @computed get links() { - return LinkManager.Links(this.dataDoc); + return Doc.Links(this.dataDoc); } @computed get heightPercent() { return NumCast(this.layoutDoc._layout_timelineHeightPercent, 100); @@ -152,11 +154,14 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps>() impl clearTimeout(this._controlsFadeTimer); this._scrubbing = true; this._controlsFadeTimer = setTimeout( - action(() => (this._scrubbing = false)), + action(() => { + this._scrubbing = false; + }), 500 ); e.stopPropagation(); break; + default: } } }; @@ -203,7 +208,9 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps>() impl else { this._keepCurrentlyPlaying = true; this.pause(); - setTimeout(() => (this._keepCurrentlyPlaying = false)); + setTimeout(() => { + this._keepCurrentlyPlaying = false; + }); } }; @@ -246,7 +253,9 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps>() impl clearTimeout(this._controlsFadeTimer); this._controlsVisible = true; this._controlsFadeTimer = setTimeout( - action(() => (this._controlsVisible = false)), + action(() => { + this._controlsVisible = false; + }), 3000 ); } @@ -262,7 +271,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps>() impl setupMoveUpEvents( e.target, e, - action((e, down, delta) => { + action((moveEv, down, delta) => { if (this._controlsTransform) { this._controlsTransform.X = Math.max(0, Math.min(delta[0] + this._controlsTransform.X, window.innerWidth)); this._controlsTransform.Y = Math.max(0, Math.min(delta[1] + this._controlsTransform.Y, window.innerHeight)); @@ -280,7 +289,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps>() impl const canvas = document.createElement('canvas'); canvas.width = 640; canvas.height = (640 * Doc.NativeHeight(this.layoutDoc)) / (Doc.NativeWidth(this.layoutDoc) || 1); - const ctx = canvas.getContext('2d'); //draw image to canvas. scale to target dimensions + const ctx = canvas.getContext('2d'); // draw image to canvas. scale to target dimensions if (ctx) { this._videoRef && ctx.drawImage(this._videoRef, 0, 0, canvas.width, canvas.height); } @@ -297,13 +306,13 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps>() impl this._props.addDocument?.(b); DocUtils.MakeLink(b, this.Document, { link_relationship: 'video snapshot' }); } else { - //convert to desired file format + // convert to desired file format const dataUrl = canvas.toDataURL('image/png'); // can also use 'image/png' // if you want to preview the captured image, - const retitled = StrCast(this.Document.title).replace(/[ -\.:]/g, ''); - const encodedFilename = encodeURIComponent(('snapshot' + retitled + '_' + (this.layoutDoc._layout_currentTimecode || 0).toString()).replace(/[\.\/\?\=]/g, '_')); + const retitled = StrCast(this.Document.title).replace(/[ -.:]/g, ''); + const encodedFilename = encodeURIComponent(('snapshot' + retitled + '_' + (this.layoutDoc._layout_currentTimecode || 0).toString()).replace(/[./?=]/g, '_')); const filename = basename(encodedFilename); - Utils.convertDataUri(dataUrl, filename).then((returnedFilename: string) => returnedFilename && (cb ?? this.createSnapshotLink)(returnedFilename, downX, downY)); + ClientUtils.convertDataUri(dataUrl, filename).then((returnedFilename: string) => returnedFilename && (cb ?? this.createSnapshotLink)(returnedFilename, downX, downY)); } }; @@ -318,7 +327,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps>() impl // creates link for snapshot createSnapshotLink = (imagePath: string, downX?: number, downY?: number) => { - const url = !imagePath.startsWith('/') ? Utils.CorsProxy(imagePath) : imagePath; + const url = !imagePath.startsWith('/') ? ClientUtils.CorsProxy(imagePath) : imagePath; const width = NumCast(this.layoutDoc._width) || 1; const height = NumCast(this.layoutDoc._height); const imageSnapshot = Docs.Create.ImageDocument(url, { @@ -334,9 +343,9 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps>() impl Doc.SetNativeWidth(imageSnapshot[DocData], Doc.NativeWidth(this.layoutDoc)); Doc.SetNativeHeight(imageSnapshot[DocData], Doc.NativeHeight(this.layoutDoc)); this._props.addDocument?.(imageSnapshot); - const link = DocUtils.MakeLink(imageSnapshot, this.getAnchor(true), { link_relationship: 'video snapshot' }); + DocUtils.MakeLink(imageSnapshot, this.getAnchor(true), { link_relationship: 'video snapshot' }); // link && (DocCast(link.link_anchor_2)[DocData].timecodeToHide = NumCast(DocCast(link.link_anchor_2).timecodeToShow) + 3); // do we need to set an end time? should default to +0.1 - setTimeout(() => downX !== undefined && downY !== undefined && DocumentManager.Instance.getFirstDocumentView(imageSnapshot)?.startDragging(downX, downY, dropActionType.move, true)); + setTimeout(() => downX !== undefined && downY !== undefined && DocumentView.getFirstDocumentView(imageSnapshot)?.startDragging(downX, downY, dropActionType.move, true)); }; getAnchor = (addAsAnnotation: boolean, pinProps?: PinProps) => { @@ -345,9 +354,9 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps>() impl if (!addAsAnnotation && marquee) marquee.backgroundColor = 'transparent'; const anchor = addAsAnnotation && marquee - ? CollectionStackedTimeline.createAnchor(this.Document, this.dataDoc, this.annotationKey, timecode ? timecode : undefined, undefined, marquee, addAsAnnotation) || this.Document + ? CollectionStackedTimeline.createAnchor(this.Document, this.dataDoc, this.annotationKey, timecode || undefined, undefined, marquee, addAsAnnotation) || this.Document : Docs.Create.ConfigDocument({ title: '#' + timecode, _timecodeToShow: timecode, annotationOn: this.Document }); - PresBox.pinDocView(anchor, { pinDocLayout: pinProps?.pinDocLayout, pinData: { ...(pinProps?.pinData ?? {}), temporal: true, pannable: true } }, this.Document); + PinDocView(anchor, { pinDocLayout: pinProps?.pinDocLayout, pinData: { ...(pinProps?.pinData ?? {}), temporal: true, pannable: true } }, this.Document); return anchor; }; @@ -375,12 +384,14 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps>() impl if (this._stackedTimeline?.makeDocUnfiltered(doc)) { if (this.heightPercent === 100) { // do we want to always open up the timeline when followin a link? kind of clunky visually - //this.layoutDoc._layout_timelineHeightPercent = VideoBox.heightPercent; + // this.layoutDoc._layout_timelineHeightPercent = VideoBox.heightPercent; options.didMove = true; } return this._stackedTimeline.getView(doc, options); } - return new Promise<Opt<DocumentView>>(res => DocumentManager.Instance.AddViewRenderedCb(doc, dv => res(dv))); + return new Promise<Opt<DocumentView>>(res => { + DocumentView.addViewRenderedCb(doc, dv => res(dv)); + }); }; // extracts video thumbnails and saves them as field of doc @@ -390,21 +401,25 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps>() impl const thumbnailPromises: Promise<any>[] = []; const video = document.createElement('video'); - video.onloadedmetadata = () => (video.currentTime = 0); + video.onloadedmetadata = () => { + video.currentTime = 0; + }; video.onseeked = () => { const canvas = document.createElement('canvas'); canvas.height = 100; canvas.width = 100; canvas.getContext('2d')?.drawImage(video, 0, 0, video.videoWidth, video.videoHeight, 0, 0, 100, 100); - const retitled = StrCast(this.Document.title).replace(/[ -\.:]/g, ''); + const retitled = StrCast(this.Document.title).replace(/[ -.:]/g, ''); const encodedFilename = encodeURIComponent('thumbnail' + retitled + '_' + video.currentTime.toString().replace(/\./, '_')); - thumbnailPromises?.push(Utils.convertDataUri(canvas.toDataURL(), basename(encodedFilename), true)); - const newTime = video.currentTime + video.duration / (VideoBox.numThumbnails - 1); + thumbnailPromises?.push(ClientUtils.convertDataUri(canvas.toDataURL(), basename(encodedFilename), true)); + const newTime = video.currentTime + video.duration / (VideoThumbnails.DENSE - 1); if (newTime < video.duration) { video.currentTime = newTime; } else { - Promise.all(thumbnailPromises).then(thumbnails => (this.dataDoc[this.fieldKey + '_thumbnails'] = new List<string>(thumbnails))); + Promise.all(thumbnailPromises).then(thumbnails => { + this.dataDoc[this.fieldKey + '_thumbnails'] = new List<string>(thumbnails); + }); } }; @@ -423,11 +438,13 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps>() impl this._disposers.reactionDisposer?.(); this._disposers.reactionDisposer = reaction( () => NumCast(this.layoutDoc._layout_currentTimecode), - time => !this._playing && (vref.currentTime = time), + time => { + !this._playing && (vref.currentTime = time); + }, { fireImmediately: true } ); - (!this.dataDoc[this.fieldKey + '_thumbnails'] || StrListCast(this.dataDoc[this.fieldKey + '_thumbnails']).length != VideoBox.numThumbnails) && this.getVideoThumbnails(); + (!this.dataDoc[this.fieldKey + '_thumbnails'] || StrListCast(this.dataDoc[this.fieldKey + '_thumbnails']).length !== VideoThumbnails.DENSE) && this.getVideoThumbnails(); } }; @@ -436,7 +453,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps>() impl setContentRef = (cref: HTMLDivElement | null) => { this._contentRef = cref; if (cref) { - cref.onfullscreenchange = action(e => { + cref.onfullscreenchange = action(() => { this._fullScreen = document.fullscreenElement === cref; this._controlsVisible = true; this._scrubbing = false; @@ -451,7 +468,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps>() impl }; // context menu - specificContextMenu = (e: React.MouseEvent): void => { + specificContextMenu = (): void => { const field = Cast(this.dataDoc[this._props.fieldKey], VideoField); if (field) { const url = field.url.href; @@ -462,25 +479,41 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps>() impl subitems.push({ description: 'Screen Capture', event: async () => { - runInAction(() => (this._screenCapture = !this._screenCapture)); + runInAction(() => { + this._screenCapture = !this._screenCapture; + }); this._videoRef!.srcObject = !this._screenCapture ? undefined : await (navigator.mediaDevices as any).getDisplayMedia({ video: true }); }, icon: 'expand-arrows-alt', }); - subitems.push({ description: (this.layoutDoc.dontAutoFollowLinks ? '' : "Don't") + ' follow links when encountered', event: () => (this.layoutDoc.dontAutoFollowLinks = !this.layoutDoc.dontAutoFollowLinks), icon: 'expand-arrows-alt' }); + subitems.push({ + description: (this.layoutDoc.dontAutoFollowLinks ? '' : "Don't") + ' follow links when encountered', + event: () => { + this.layoutDoc.dontAutoFollowLinks = !this.layoutDoc.dontAutoFollowLinks; + }, + icon: 'expand-arrows-alt', + }); subitems.push({ description: (this.layoutDoc.dontAutoPlayFollowedLinks ? '' : "Don't") + ' play when link is selected', - event: () => (this.layoutDoc.dontAutoPlayFollowedLinks = !this.layoutDoc.dontAutoPlayFollowedLinks), + event: () => { + this.layoutDoc.dontAutoPlayFollowedLinks = !this.layoutDoc.dontAutoPlayFollowedLinks; + }, + icon: 'expand-arrows-alt', + }); + subitems.push({ + description: (this.layoutDoc.autoPlayAnchors ? "Don't auto play" : 'Auto play') + ' anchors onClick', + event: () => { + this.layoutDoc.autoPlayAnchors = !this.layoutDoc.autoPlayAnchors; + }, icon: 'expand-arrows-alt', }); - subitems.push({ description: (this.layoutDoc.autoPlayAnchors ? "Don't auto play" : 'Auto play') + ' anchors onClick', event: () => (this.layoutDoc.autoPlayAnchors = !this.layoutDoc.autoPlayAnchors), icon: 'expand-arrows-alt' }); // subitems.push({ description: "Start Trim All", event: () => this.startTrim(TrimScope.All), icon: "expand-arrows-alt" }); // subitems.push({ description: "Start Trim Clip", event: () => this.startTrim(TrimScope.Clip), icon: "expand-arrows-alt" }); // subitems.push({ description: "Stop Trim", event: () => this.finishTrim(), icon: "expand-arrows-alt" }); subitems.push({ description: 'Copy path', event: () => { - Utils.CopyText(url); + ClientUtils.CopyText(url); }, icon: 'expand-arrows-alt', }); @@ -504,7 +537,9 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps>() impl }; // ref for updating time - setAudioRef = (e: HTMLAudioElement | null) => (this._audioPlayer = e); + setAudioRef = (e: HTMLAudioElement | null) => { + this._audioPlayer = e; + }; // renders the video and audio @computed get content() { @@ -570,8 +605,8 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps>() impl setupMoveUpEvents( this, e, - e => { - this.Snapshot(e.clientX, e.clientY); + moveEv => { + this.Snapshot(moveEv.clientX, moveEv.clientY); return true; }, emptyFunction, @@ -586,7 +621,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps>() impl setupMoveUpEvents( this, e, - action(encodeURIComponent => { + action(() => { this._clicking = false; if (this._props.isContentActive()) { // const local = this.ScreenToLocalTransform().scale(this._props.scaling?.() || 1).transformPoint(e.clientX, e.clientY); @@ -600,7 +635,9 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps>() impl () => { this.layoutDoc._layout_timelineHeightPercent = this.heightPercent !== 100 ? 100 : VideoBox.heightPercent; setTimeout( - action(() => (this._clicking = false)), + action(() => { + this._clicking = false; + }), 500 ); }, @@ -613,30 +650,32 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps>() impl @action removeCurrentlyPlaying = () => { const docView = this.DocumentView?.(); - if (CollectionStackedTimeline.CurrentlyPlaying && docView) { - const index = CollectionStackedTimeline.CurrentlyPlaying.indexOf(docView); - index !== -1 && CollectionStackedTimeline.CurrentlyPlaying.splice(index, 1); + if (DocumentView.CurrentlyPlaying && docView) { + const index = DocumentView.CurrentlyPlaying.indexOf(docView); + index !== -1 && DocumentView.CurrentlyPlaying.splice(index, 1); } }; // adds doc to currently playing display @action addCurrentlyPlaying = () => { const docView = this.DocumentView?.(); - if (!CollectionStackedTimeline.CurrentlyPlaying) { - CollectionStackedTimeline.CurrentlyPlaying = []; + if (!DocumentView.CurrentlyPlaying) { + DocumentView.CurrentlyPlaying = []; } - if (docView && CollectionStackedTimeline.CurrentlyPlaying.indexOf(docView) === -1) { - CollectionStackedTimeline.CurrentlyPlaying.push(docView); + if (docView && DocumentView.CurrentlyPlaying.indexOf(docView) === -1) { + DocumentView.CurrentlyPlaying.push(docView); } }; // for annotating, adds doc with time info @action.bound - addDocWithTimecode(doc: Doc | Doc[]): boolean { - const docs = doc instanceof Doc ? [doc] : doc; + addDocWithTimecode(docIn: Doc | Doc[]): boolean { + const docs = toList(docIn); const curTime = NumCast(this.layoutDoc._layout_currentTimecode); - docs.forEach(doc => (doc._timecodeToHide = (doc._timecodeToShow = curTime) + 1)); - return this.addDocument(doc); + docs.forEach(doc => { + doc._timecodeToHide = (doc._timecodeToShow = curTime) + 1; + }); + return this.addDocument(docs); } // play back the audio from seekTimeInSeconds, fullPlay tells whether clip is being played to end vs link range @@ -644,7 +683,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps>() impl playFrom = (seekTimeInSeconds: number, endTime?: number, fullPlay: boolean = false) => { clearTimeout(this._playRegionTimer); this._playRegionTimer = undefined; - if (Number.isNaN(this.player?.duration)) { + if (this.player?.duration === undefined || isNaN(this.player.duration)) { setTimeout(() => this.playFrom(seekTimeInSeconds, endTime), 500); } else if (this.player) { // trimBounds override requested playback bounds @@ -696,7 +735,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps>() impl e, returnFalse, returnFalse, - action((e: PointerEvent, doubleTap?: boolean) => { + action((clickEv: PointerEvent, doubleTap?: boolean) => { if (doubleTap) { this.startTrim(TrimScope.All); } else if (this.timeline) { @@ -731,11 +770,9 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps>() impl // stretches vertically or horizontally depending on video orientation so video fits full screen fullScreenSize() { if (this._videoRef && this._videoRef.videoHeight / this._videoRef.videoWidth > 1) { - //prettier-ignore - return ({ height: '100%' }); + return { height: '100%' }; } - //prettier-ignore - return ({ width: '100%' }); + return ({ width: '100%' }); // prettier-ignore } // for zoom slider, sets timeline waveform zoom @@ -757,9 +794,9 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps>() impl setupMoveUpEvents( this, e, - action(e => { + action(moveEv => { MarqueeAnnotator.clearAnnotations(this._savedAnnotations); - this._marqueeref.current?.onInitiateSelection([e.clientX, e.clientY]); + this._marqueeref.current?.onInitiateSelection([moveEv.clientX, moveEv.clientY]); return true; }), returnFalse, @@ -777,7 +814,10 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps>() impl this._props.select(true); }; - timelineWhenChildContentsActiveChanged = action((isActive: boolean) => this._props.whenChildContentsActiveChanged((this._isAnyChildContentActive = isActive))); + timelineWhenChildContentsActiveChanged = action((isActive: boolean) => { + this._isAnyChildContentActive = isActive; + this._props.whenChildContentsActiveChanged(isActive); + }); timelineScreenToLocal = () => this._props @@ -785,7 +825,9 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps>() impl .scale(this.scaling()) .translate(0, (-this.heightPercent / 100) * this._props.PanelHeight()); - setPlayheadTime = (time: number) => (this.player!.currentTime = this.layoutDoc._layout_currentTimecode = time); + setPlayheadTime = (time: number) => { + this.player!.currentTime = this.layoutDoc._layout_currentTimecode = time; + }; timelineHeight = () => (this._props.PanelHeight() * (100 - this.heightPercent)) / 100; @@ -806,7 +848,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps>() impl marqueeOffset = () => [((this.panelWidth() / 2) * (1 - this.heightPercent / 100)) / (this.heightPercent / 100), 0]; - timelineDocFilter = () => [`_isTimelineLabel:true,${Utils.noRecursionHack}:x`]; + timelineDocFilter = () => [`_isTimelineLabel:true,${ClientUtils.noRecursionHack}:x`]; // renders video controls componentUI = (boundsLeft: number, boundsTop: number) => { @@ -848,7 +890,10 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps>() impl return ( <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))} + ref={action((r: any) => { + this._stackedTimeline = r; + })} + // eslint-disable-next-line react/jsx-props-no-spreading {...this._props} dataFieldKey={this.fieldKey} fieldKey={this.annotationKey} @@ -886,7 +931,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps>() impl } crop = (region: Doc | undefined, addCrop?: boolean) => { - if (!region) return; + if (!region) return undefined; const cropping = Doc.MakeCopy(region, true); const regionData = region[DocData]; regionData.backgroundColor = 'transparent'; @@ -915,8 +960,8 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps>() impl croppingProto.type = DocumentType.VID; croppingProto.layout = VideoBox.LayoutString('data'); croppingProto.data = ObjectField.MakeCopy(this.dataDoc[this.fieldKey] as ObjectField); - croppingProto['data_nativeWidth'] = anchw; - croppingProto['data_nativeHeight'] = anchh; + croppingProto.data_nativeWidth = anchw; + croppingProto.data_nativeHeight = anchh; croppingProto.videoCrop = true; croppingProto.layout_currentTimecode = this.layoutDoc._layout_currentTimecode; croppingProto.freeform_scale = viewScale; @@ -933,6 +978,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps>() impl this._props.bringToFront?.(cropping); return cropping; }; + focus = (anchor: Doc, options: FocusViewOptions) => (anchor.type === DocumentType.CONFIG ? undefined : this._ffref.current?.focus(anchor, options)); savedAnnotations = () => this._savedAnnotations; render() { const borderRad = this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.BorderRounding); @@ -958,14 +1004,16 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps>() impl left: (this._props.PanelWidth() - this.panelWidth()) / 2, }}> <CollectionFreeFormView + // eslint-disable-next-line react/jsx-props-no-spreading {...this._props} + ref={this._ffref} setContentViewBox={emptyFunction} NativeWidth={returnZero} NativeHeight={returnZero} renderDepth={this._props.renderDepth + 1} fieldKey={this.annotationKey} - isAnnotationOverlay={true} - annotationLayerHostsContent={true} + isAnnotationOverlay + annotationLayerHostsContent PanelWidth={this._props.PanelWidth} PanelHeight={this._props.PanelHeight} isAnyChildContentActive={returnFalse} @@ -1049,12 +1097,12 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps>() impl </div> )} - <div className="videobox-button" title={'full screen'} onPointerDown={this.onFullDown}> + <div className="videobox-button" title="full screen" onPointerDown={this.onFullDown}> <FontAwesomeIcon icon="expand" /> </div> {!this._fullScreen && width > 300 && ( - <div className="videobox-button" title={'show timeline'} onPointerDown={this.onTimelineHdlDown}> + <div className="videobox-button" title="show timeline" onPointerDown={this.onTimelineHdlDown}> <FontAwesomeIcon icon="eye" /> </div> )} @@ -1113,3 +1161,12 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps>() impl ); } } + +Docs.Prototypes.TemplateMap.set(DocumentType.VID, { + layout: { view: VideoBox, dataField: 'data' }, + options: { acl: '', _layout_currentTimecode: 0, systemIcon: 'BsFileEarmarkPlayFill' }, +}); +Docs.Prototypes.TemplateMap.set(DocumentType.REC, { + layout: { view: VideoBox, dataField: 'data' }, + options: { acl: '', _height: 100, backgroundColor: 'pink', systemIcon: 'BsFillMicFill' }, +}); diff --git a/src/client/views/nodes/WebBox.tsx b/src/client/views/nodes/WebBox.tsx index 033b01d24..198652310 100644 --- a/src/client/views/nodes/WebBox.tsx +++ b/src/client/views/nodes/WebBox.tsx @@ -1,22 +1,26 @@ +/* eslint-disable jsx-a11y/control-has-associated-label */ +/* eslint-disable jsx-a11y/no-static-element-interactions */ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { htmlToText } from 'html-to-text'; import { action, computed, IReactionDisposer, makeObservable, observable, ObservableMap, reaction, runInAction } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; import * as WebRequest from 'web-request'; -import { Doc, DocListCast, Field, Opt } from '../../../fields/Doc'; +import { addStyleSheet, addStyleSheetRule, clearStyleSheetRules, ClientUtils, DivHeight, getWordAtPoint, lightOrDark, returnFalse, returnOne, returnZero, setupMoveUpEvents, smoothScroll } from '../../../ClientUtils'; +import { Doc, DocListCast, Field, FieldType, Opt } from '../../../fields/Doc'; import { Id } from '../../../fields/FieldSymbols'; import { HtmlField } from '../../../fields/HtmlField'; import { InkTool } from '../../../fields/InkField'; import { List } from '../../../fields/List'; import { RefField } from '../../../fields/RefField'; import { listSpec } from '../../../fields/Schema'; -import { Cast, NumCast, StrCast, WebCast } from '../../../fields/Types'; +import { Cast, NumCast, StrCast, toList, WebCast } from '../../../fields/Types'; import { ImageField, WebField } from '../../../fields/URLField'; import { TraceMobx } from '../../../fields/util'; -import { addStyleSheet, addStyleSheetRule, clearStyleSheetRules, DivHeight, emptyFunction, getWordAtPoint, lightOrDark, returnFalse, returnOne, returnZero, setupMoveUpEvents, smoothScroll, stringHash, Utils } from '../../../Utils'; -import { Docs, DocUtils } from '../../documents/Documents'; -import { DocumentManager } from '../../util/DocumentManager'; +import { emptyFunction, stringHash } from '../../../Utils'; +import { Docs } from '../../documents/Documents'; +import { DocumentType } from '../../documents/DocumentTypes'; +import { DocUtils } from '../../documents/DocUtils'; import { ScriptingGlobals } from '../../util/ScriptingGlobals'; import { SnappingManager } from '../../util/SnappingManager'; import { undoBatch, UndoManager } from '../../util/UndoManager'; @@ -31,17 +35,20 @@ import { MarqueeAnnotator } from '../MarqueeAnnotator'; import { AnchorMenu } from '../pdf/AnchorMenu'; import { Annotation } from '../pdf/Annotation'; import { GPTPopup } from '../pdf/GPTPopup/GPTPopup'; +import { PinDocView, PinProps } from '../PinFuncs'; import { SidebarAnnos } from '../SidebarAnnos'; -import { StyleProp } from '../StyleProvider'; -import { DocumentView, OpenWhere } from './DocumentView'; -import { FieldView, FieldViewProps, FocusViewOptions } from './FieldView'; +import { StyleProp } from '../StyleProp'; +import { DocumentView } from './DocumentView'; +import { FieldView, FieldViewProps } from './FieldView'; +import { FocusViewOptions } from './FocusViewOptions'; import { LinkInfo } from './LinkDocPreview'; -import { PinProps, PresBox } from './trails'; +import { OpenWhere } from './OpenWhere'; import './WebBox.scss'; + const { CreateImage } = require('./WebBoxRenderer'); -const _global = (window /* browser */ || global) /* node */ as any; + @observer -export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implements ViewBoxInterface { +export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { public static LayoutString(fieldKey: string) { return FieldView.LayoutString(WebBox, fieldKey); } @@ -105,7 +112,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem } @action - search = (searchString: string, bwd?: boolean, clear: boolean = false) => { + override search = (searchString: string, bwd?: boolean, clear: boolean = false) => { if (!this._searching && !clear) { this._searching = true; setTimeout(() => { @@ -141,19 +148,19 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem const scrollTop = NumCast(this.layoutDoc._layout_scrollTop); const nativeWidth = NumCast(this.layoutDoc.nativeWidth); const nativeHeight = (nativeWidth * this._props.PanelHeight()) / this._props.PanelWidth(); - var htmlString = this._iframe.contentDocument && new XMLSerializer().serializeToString(this._iframe.contentDocument); + let htmlString = this._iframe.contentDocument && new XMLSerializer().serializeToString(this._iframe.contentDocument); if (!htmlString) { - htmlString = await (await fetch(Utils.CorsProxy(this.webField!.href))).text(); + htmlString = await (await fetch(ClientUtils.CorsProxy(this.webField!.href))).text(); } this.layoutDoc.thumb = undefined; this.Document.thumbLockout = true; // lock to prevent multiple thumb updates. CreateImage(this._webUrl.endsWith('/') ? this._webUrl.substring(0, this._webUrl.length - 1) : this._webUrl, this._iframe.contentDocument?.styleSheets ?? [], htmlString, nativeWidth, nativeHeight, scrollTop) - .then((data_url: any) => { - if (data_url.includes('<!DOCTYPE')) { + .then((dataUrl: any) => { + if (dataUrl.includes('<!DOCTYPE')) { console.log('BAD DATA IN THUMB CREATION'); return; } - Utils.convertDataUri(data_url, this.layoutDoc[Id] + '-icon' + new Date().getTime(), true, this.layoutDoc[Id] + '-icon').then(returnedfilename => + ClientUtils.convertDataUri(dataUrl, this.layoutDoc[Id] + '-icon' + new Date().getTime(), true, this.layoutDoc[Id] + '-icon').then(returnedfilename => setTimeout( action(() => { this.Document.thumbLockout = false; @@ -166,7 +173,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem ) ); }) - .catch(function (error: any) { + .catch((error: any) => { console.error('oops, something went wrong!', error); }); }; @@ -187,7 +194,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem }); this._disposers.urlchange = reaction( () => WebCast(this.dataDoc.data), - url => this.submitURL(false, false) + () => this.submitURL(false, false) ); this._disposers.titling = reaction( () => StrCast(this.Document.title), @@ -199,8 +206,8 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem this._disposers.layout_autoHeight = reaction( () => this.layoutDoc._layout_autoHeight, - layout_autoHeight => { - if (layout_autoHeight) { + layoutAutoHeight => { + if (layoutAutoHeight) { this.layoutDoc._nativeHeight = NumCast(this.Document[this._props.fieldKey + '_nativeHeight']); this._props.setHeight?.(NumCast(this.Document[this._props.fieldKey + '_nativeHeight']) * (this._props.NativeDimScaling?.() || 1)); } @@ -219,8 +226,10 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem } } // else it's an HTMLfield } else if (this.webField && !this.dataDoc.text) { - WebRequest.get(Utils.CorsProxy(this.webField.href)) // - .then(result => result && (this.dataDoc.text = htmlToText(result.content))); + WebRequest.get(ClientUtils.CorsProxy(this.webField.href)) // + .then(result => { + result && (this.dataDoc.text = htmlToText(result.content)); + }); } this._disposers.scrollReaction = reaction( @@ -254,7 +263,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem const clientRects = selRange.getClientRects(); for (let i = 0; i < clientRects.length; i++) { const rect = clientRects.item(i); - const mainrect = this._url ? { translateX: 0, translateY: 0, scale: 1 } : Utils.GetScreenTransform(this._mainCont.current); + const mainrect = this._url ? { translateX: 0, translateY: 0, scale: 1 } : ClientUtils.GetScreenTransform(this._mainCont.current); if (rect && rect.width !== this._mainCont.current.clientWidth) { const annoBox = document.createElement('div'); annoBox.className = 'marqueeAnnotator-annotationBox'; @@ -283,27 +292,39 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem focus = (anchor: Doc, options: FocusViewOptions) => { if (anchor !== this.Document && this._outerRef.current) { const windowHeight = this._props.PanelHeight() / (this._props.NativeDimScaling?.() || 1); - const scrollTo = Utils.scrollIntoView(NumCast(anchor.y), NumCast(anchor._height), NumCast(this.layoutDoc._layout_scrollTop), windowHeight, windowHeight * 0.1, Math.max(NumCast(anchor.y) + NumCast(anchor._height), this._scrollHeight)); + const scrollTo = ClientUtils.scrollIntoView( + NumCast(anchor.y), + NumCast(anchor._height), + NumCast(this.layoutDoc._layout_scrollTop), + windowHeight, + windowHeight * 0.1, + Math.max(NumCast(anchor.y) + NumCast(anchor._height), this._scrollHeight) + ); if (scrollTo !== undefined) { if (this._initialScroll === undefined) { const focusTime = options.zoomTime ?? 500; this.goTo(scrollTo, focusTime, options.easeFunc); return focusTime; - } else { - this._initialScroll = scrollTo; } + this._initialScroll = scrollTo; } } + return undefined; }; @action - getView = (doc: Doc, options: FocusViewOptions) => { - if (Doc.AreProtosEqual(doc, this.Document)) return new Promise<Opt<DocumentView>>(res => res(this.DocumentView?.())); + getView = (doc: Doc /* , options: FocusViewOptions */) => { + if (Doc.AreProtosEqual(doc, this.Document)) + return new Promise<Opt<DocumentView>>(res => { + res(this.DocumentView?.()); + }); if (this.Document.layout_fieldKey === 'layout_icon') this.DocumentView?.().iconify(); const webUrl = WebCast(doc.config_data)?.url; if (this._url && webUrl && webUrl.href !== this._url) this.setData(webUrl.href); if (this._sidebarRef?.current?.makeDocUnfiltered(doc) && !this.SidebarShown) this.toggleSidebar(false); - return new Promise<Opt<DocumentView>>(res => DocumentManager.Instance.AddViewRenderedCb(doc, dv => res(dv))); + return new Promise<Opt<DocumentView>>(res => { + DocumentView.addViewRenderedCb(doc, dv => res(dv)); + }); }; sidebarAddDocTab = (doc: Doc, where: OpenWhere) => { @@ -314,14 +335,16 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem return this._props.addDocTab(doc, where); }; getAnchor = (addAsAnnotation: boolean, pinProps?: PinProps) => { - let ele: Opt<HTMLDivElement> = undefined; + let ele: Opt<HTMLDivElement>; try { const contents = this._iframe?.contentWindow?.getSelection()?.getRangeAt(0).cloneContents(); if (contents) { ele = document.createElement('div'); ele.append(contents); } - } catch (e) {} + } catch (e) { + /* empty */ + } const visibleAnchor = this._getAnchor(this._savedAnnotations, true); const anchor = visibleAnchor ?? @@ -330,7 +353,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem y: NumCast(this.layoutDoc._layout_scrollTop), annotationOn: this.Document, }); - PresBox.pinDocView(anchor, { pinDocLayout: pinProps?.pinDocLayout, pinData: { ...(pinProps?.pinData ?? {}), scrollable: pinProps?.pinData ? true : false, pannable: true } }, this.Document); + PinDocView(anchor, { pinDocLayout: pinProps?.pinDocLayout, pinData: { ...(pinProps?.pinData ?? {}), scrollable: !!pinProps?.pinData, pannable: true } }, this.Document); anchor.text = ele?.textContent ?? ''; anchor.text_html = ele?.innerHTML ?? this._selectionText; addAsAnnotation && this.addDocumentWrapper(anchor); @@ -356,7 +379,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem this._textAnnotationCreator = undefined; this.DocumentView?.()?.cleanupPointerEvents(); // pointerup events aren't generated on containing document view, so we have to invoke it here. if (this._iframe?.contentWindow && this._iframe.contentDocument && !this._iframe.contentWindow.getSelection()?.isCollapsed) { - const mainContBounds = Utils.GetScreenTransform(this._mainCont.current!); + const mainContBounds = ClientUtils.GetScreenTransform(this._mainCont.current!); const scale = (this._props.NativeDimScaling?.() || 1) * mainContBounds.scale; const sel = this._iframe.contentWindow.getSelection(); if (sel) { @@ -387,7 +410,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem e?.stopPropagation(); setTimeout(() => { // if menu comes up right away, the down event can still be active causing a menu item to be selected - this.specificContextMenu(undefined as any); + this.specificContextMenu(); this.DocumentView?.().onContextMenu(undefined, theclick[0], theclick[1]); }); } @@ -462,6 +485,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem const sheets = document.head.appendChild(style); return (sheets as any).sheet; } + return undefined; } addWebStyleSheetRule(sheet: any, selector: any, css: any, selectorPrefix = '.') { const propText = @@ -476,7 +500,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem _iframetimeout: any = undefined; @observable _warning = 0; @action - iframeLoaded = (e: any) => { + iframeLoaded = () => { const iframe = this._iframe; if (this._initialScroll !== undefined) { this.setScrollPos(this._initialScroll); @@ -491,7 +515,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem runInAction(() => this._warning++); href = undefined; } - let requrlraw = decodeURIComponent(href?.replace(Utils.prepend('') + '/corsProxy/', '') ?? this._url.toString()); + let requrlraw = decodeURIComponent(href?.replace(ClientUtils.prepend('') + '/corsProxy/', '') ?? this._url.toString()); if (requrlraw !== this._url.toString()) { if (requrlraw.match(/q=.*&/)?.length && this._url.toString().match(/q=.*&/)?.length) { const matches = requrlraw.match(/[^a-zA-z]q=[^&]*/g); @@ -544,16 +568,16 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem 'click', undoBatch( action((e: MouseEvent) => { - let href = ''; + let eleHref = ''; for (let ele = e.target as any; ele; ele = ele.parentElement) { - href = (typeof ele.href === 'string' ? ele.href : ele.href?.baseVal) || ele.parentElement?.href || href; + eleHref = (typeof ele.href === 'string' ? ele.href : ele.href?.baseVal) || ele.parentElement?.href || eleHref; } const origin = this.webField?.origin; - if (href && origin) { + if (eleHref && origin) { const batch = UndoManager.StartBatch('webclick'); e.stopPropagation(); setTimeout(() => { - this.setData(href.replace(Utils.prepend(''), origin)); + this.setData(eleHref.replace(ClientUtils.prepend(''), origin)); batch.end(); }); if (this._outerRef.current) { @@ -632,12 +656,17 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem this._scrollHeight = 0; if (this._webUrl === this._url) { this._webUrl = curUrl; - setTimeout(action(() => (this._webUrl = this._url))); + setTimeout( + action(() => { + this._webUrl = this._url; + }) + ); } else { this._webUrl = this._url; } return true; } + return undefined; }); return false; }; @@ -655,12 +684,17 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem this._scrollHeight = 0; if (this._webUrl === this._url) { this._webUrl = curUrl; - setTimeout(action(() => (this._webUrl = this._url))); + setTimeout( + action(() => { + this._webUrl = this._url; + }) + ); } else { this._webUrl = this._url; } return true; } + return undefined; }); return false; }; @@ -692,13 +726,13 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem const html = dataTransfer.getData('text/html'); const uri = dataTransfer.getData('text/uri-list'); const url = uri || html || this._url || ''; - const newurl = url.startsWith(window.location.origin) ? url.replace(window.location.origin, this._url?.match(/http[s]?:\/\/[^\/]*/)?.[0] || '') : url; + const newurl = url.startsWith(window.location.origin) ? url.replace(window.location.origin, this._url?.match(/http[s]?:\/\/[^/]*/)?.[0] || '') : url; this.setData(newurl); e.stopPropagation(); }; @action - setData = (data: Field | Promise<RefField | undefined>) => { + setData = (data: FieldType | Promise<RefField | undefined>) => { if (!(typeof data === 'string') && !(data instanceof WebField)) return false; if (Field.toString(data) === this._url) return false; this._scrollHeight = 0; @@ -715,19 +749,31 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem e.stopPropagation(); }; - specificContextMenu = (e: React.MouseEvent | PointerEvent): void => { + specificContextMenu = (): void => { const cm = ContextMenu.Instance; const funcs: ContextMenuProps[] = []; if (!cm.findByDescription('Options...')) { !Doc.noviceMode && - funcs.push({ description: (this.layoutDoc[this.fieldKey + '_useCors'] ? "Don't Use" : 'Use') + ' Cors', event: () => (this.layoutDoc[this.fieldKey + '_useCors'] = !this.layoutDoc[this.fieldKey + '_useCors']), icon: 'snowflake' }); + funcs.push({ + description: (this.layoutDoc[this.fieldKey + '_useCors'] ? "Don't Use" : 'Use') + ' Cors', + event: () => { + this.layoutDoc[this.fieldKey + '_useCors'] = !this.layoutDoc[this.fieldKey + '_useCors']; + }, + icon: 'snowflake', + }); funcs.push({ description: (this.dataDoc[this.fieldKey + '_allowScripts'] ? 'Prevent' : 'Allow') + ' Scripts', event: () => { this.dataDoc[this.fieldKey + '_allowScripts'] = !this.dataDoc[this.fieldKey + '_allowScripts']; if (this._iframe) { - runInAction(() => (this._hackHide = true)); - setTimeout(action(() => (this._hackHide = false))); + runInAction(() => { + this._hackHide = true; + }); + setTimeout( + action(() => { + this._hackHide = false; + }) + ); } }, icon: 'snowflake', @@ -765,7 +811,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem setupMoveUpEvents( this, e, - action(e => { + action(() => { MarqueeAnnotator.clearAnnotations(this._savedAnnotations); return true; }), @@ -789,7 +835,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem @observable lighttext = false; @computed get urlContent() { - if (this.ScreenToLocalBoxXf().Scale > 25) return <div></div>; + if (this.ScreenToLocalBoxXf().Scale > 25) return <div />; setTimeout( action(() => { if (this._initialScroll === undefined && !this._webPageHasBeenRendered) { @@ -811,19 +857,23 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem })} contentEditable onPointerDown={this.webClipDown} + // eslint-disable-next-line react/no-danger dangerouslySetInnerHTML={{ __html: field.html }} /> ); } if (field instanceof WebField) { - const url = this.layoutDoc[this.fieldKey + '_useCors'] ? Utils.CorsProxy(this._webUrl) : this._webUrl; + const url = this.layoutDoc[this.fieldKey + '_useCors'] ? ClientUtils.CorsProxy(this._webUrl) : this._webUrl; const scripts = this.dataDoc[this.fieldKey + '_allowScripts'] || this._webUrl.includes('wikipedia.org') || this._webUrl.includes('google.com') || this._webUrl.startsWith('https://bing'); - //if (!scripts) console.log('No scripts for: ' + url); + // if (!scripts) console.log('No scripts for: ' + url); return ( <iframe + title="web iframe" key={this._warning} className="webBox-iframe" - ref={action((r: HTMLIFrameElement | null) => (this._iframe = r))} + ref={action((r: HTMLIFrameElement | null) => { + this._iframe = r; + })} style={{ pointerEvents: SnappingManager.IsResizing ? 'none' : undefined }} src={url} onLoad={this.iframeLoaded} @@ -834,12 +884,24 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem /> ); } - return <iframe className="webBox-iframe" ref={action((r: HTMLIFrameElement | null) => (this._iframe = r))} src={'https://crossorigin.me/https://cs.brown.edu'} />; + return ( + <iframe + title="web frame" + className="webBox-iframe" + ref={action((r: HTMLIFrameElement | null) => { + this._iframe = r; + })} + src="https://crossorigin.me/https://cs.brown.edu" + /> + ); } - addDocumentWrapper = (doc: Doc | Doc[], annotationKey?: string) => { - this._url && (doc instanceof Doc ? [doc] : doc).forEach(doc => (doc.config_data = new WebField(this._url))); - return this.addDocument(doc, annotationKey); + addDocumentWrapper = (docs: Doc | Doc[], annotationKey?: string) => { + this._url && + toList(docs).forEach(doc => { + doc.config_data = new WebField(this._url); + }); + return this.addDocument(docs, annotationKey); }; sidebarAddDocument = (doc: Doc | Doc[], sidebarKey?: string) => { @@ -853,7 +915,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem setupMoveUpEvents( this, e, - action((e, down, delta) => { + action((moveEv, down, delta) => { this._draggingSidebar = true; const localDelta = this._props .ScreenToLocalTransform() @@ -871,7 +933,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem } return false; }), - action((e, movement, isClick) => { + action((upEv, movement, isClick) => { this._draggingSidebar = false; !isClick && batch.end(); }), @@ -892,14 +954,14 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem backgroundColor: this.SidebarShown ? Colors.MEDIUM_BLUE : Colors.BLACK, }} onPointerDown={e => this.sidebarBtnDown(e, true)}> - <FontAwesomeIcon style={{ color: Colors.WHITE }} icon={'comment-alt'} size="sm" /> + <FontAwesomeIcon style={{ color: Colors.WHITE }} icon="comment-alt" size="sm" /> </div> ); } @observable _previewNativeWidth: Opt<number> = undefined; @observable _previewWidth: Opt<number> = undefined; toggleSidebar = action((preview: boolean = false) => { - var nativeWidth = NumCast(this.layoutDoc[this.fieldKey + '_nativeWidth']); + let nativeWidth = NumCast(this.layoutDoc[this.fieldKey + '_nativeWidth']); if (!nativeWidth) { const defaultNativeWidth = NumCast(this.Document.nativeWidth, this.dataDoc[this.fieldKey] instanceof WebField ? 850 : NumCast(this.Document._width)); Doc.SetNativeWidth(this.dataDoc, Doc.NativeWidth(this.dataDoc) || defaultNativeWidth); @@ -938,7 +1000,9 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem }; _innerCollectionView: CollectionFreeFormView | undefined; zoomScaling = () => this._innerCollectionView?.zoomScaling() ?? 1; - setInnerContent = (component: ViewBoxInterface) => (this._innerCollectionView = component as CollectionFreeFormView); + setInnerContent = (component: ViewBoxInterface<any>) => { + this._innerCollectionView = component as CollectionFreeFormView; + }; @computed get content() { const interactive = this._props.isContentActive() && this._props.pointerEvents?.() !== 'none' && Doc.ActiveTool === InkTool.None; @@ -969,24 +1033,26 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem {this.inlineTextAnnotations .sort((a, b) => NumCast(a.y) - NumCast(b.y)) .map(anno => ( - <Annotation {...this._props} fieldKey={this.annotationKey} pointerEvents={this.pointerEvents} dataDoc={this.dataDoc} anno={anno} key={`${anno[Id]}-annotation`} /> + // eslint-disable-next-line react/jsx-props-no-spreading + <Annotation {...this._props} fieldKey={this.annotationKey} pointerEvents={this.pointerEvents} containerDataDoc={this.dataDoc} annoDoc={anno} key={`${anno[Id]}-annotation`} /> ))} </div> ); } @computed get SidebarShown() { - return this._showSidebar || this.layoutDoc._layout_showSidebar ? true : false; + return !!(this._showSidebar || this.layoutDoc._layout_showSidebar); } renderAnnotations = (childFilters: () => string[]) => ( <CollectionFreeFormView + // eslint-disable-next-line react/jsx-props-no-spreading {...this._props} setContentViewBox={this.setInnerContent} NativeWidth={returnZero} NativeHeight={returnZero} originTopLeft={false} - isAnnotationOverlayScrollable={true} + isAnnotationOverlayScrollable renderDepth={this._props.renderDepth + 1} - isAnnotationOverlay={true} + isAnnotationOverlay fieldKey={this.annotationKey} setPreviewCursor={this.setPreviewCursor} PanelWidth={this.panelWidth} @@ -1029,7 +1095,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem }} // when active, block wheel events from propagating since they're handled by the iframe onWheel={this.onZoomWheel} - onScroll={e => this.setDashScrollTop(this._outerRef.current?.scrollTop || 0)} + onScroll={() => this.setDashScrollTop(this._outerRef.current?.scrollTop || 0)} onPointerDown={this.onMarqueeDown}> <div className="webBox-innerContent" style={{ height: (this._webPageHasBeenRendered && this._scrollHeight > this._props.PanelHeight() && this._scrollHeight) || '100%', pointerEvents }}> {this.content} @@ -1045,7 +1111,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem return ( <div className="webBox-ui" onPointerDown={e => e.stopPropagation()} style={{ display: this._props.isContentActive() ? 'flex' : 'none' }}> <div className="webBox-overlayCont" onPointerDown={e => e.stopPropagation()} style={{ left: `${this._searching ? 0 : 100}%` }}> - <button className="webBox-overlayButton" title={'search'} /> + <button type="button" className="webBox-overlayButton" title="search" /> <input className="webBox-searchBar" placeholder="Search" @@ -1056,13 +1122,14 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem e.stopPropagation(); }} /> - <button className="webBox-search" title="Search" onClick={e => this.search(this._searchString, e.shiftKey)}> + <button type="button" className="webBox-search" title="Search" onClick={e => this.search(this._searchString, e.shiftKey)}> <FontAwesomeIcon icon="search" size="sm" /> </button> </div> <button + type="button" className="webBox-overlayButton" - title={'search'} + title="search" onClick={action(() => { this._searching = !this._searching; this.search('', false, true); @@ -1075,14 +1142,18 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem </div> ); } - searchStringChanged = (e: React.ChangeEvent<HTMLInputElement>) => (this._searchString = e.currentTarget.value); - setPreviewCursor = (func?: (x: number, y: number, drag: boolean, hide: boolean, doc: Opt<Doc>) => void) => (this._setPreviewCursor = func); + searchStringChanged = (e: React.ChangeEvent<HTMLInputElement>) => { + this._searchString = e.currentTarget.value; + }; + setPreviewCursor = (func?: (x: number, y: number, drag: boolean, hide: boolean, doc: Opt<Doc>) => void) => { + this._setPreviewCursor = func; + }; panelWidth = () => this._props.PanelWidth() / (this._props.NativeDimScaling?.() || 1) - this.sidebarWidth() + WebBox.sidebarResizerWidth; panelHeight = () => this._props.PanelHeight() / (this._props.NativeDimScaling?.() || 1); scrollXf = () => this.ScreenToLocalBoxXf().translate(0, NumCast(this.layoutDoc._layout_scrollTop)); anchorMenuClick = () => this._sidebarRef.current?.anchorMenuClick; - transparentFilter = () => [...this._props.childFilters(), Utils.TransparentBackgroundFilter]; - opaqueFilter = () => [...this._props.childFilters(), Utils.noDragDocsFilter, ...(SnappingManager.CanEmbed ? [] : [Utils.OpaqueBackgroundFilter])]; + transparentFilter = () => [...this._props.childFilters(), ClientUtils.TransparentBackgroundFilter]; + opaqueFilter = () => [...this._props.childFilters(), ClientUtils.noDragDocsFilter, ...(SnappingManager.CanEmbed ? [] : [ClientUtils.OpaqueBackgroundFilter])]; childStyleProvider = (doc: Doc | undefined, props: Opt<FieldViewProps>, property: string): any => { if (doc instanceof Doc && property === StyleProp.PointerEvents) { if (this.inlineTextAnnotations.includes(doc)) return 'none'; @@ -1149,6 +1220,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem <div style={{ position: 'absolute', height: '100%', right: 0, top: 0, width: `calc(100 * ${this.sidebarWidth() / this._props.PanelWidth()}%` }}> <SidebarAnnos ref={this._sidebarRef} + // eslint-disable-next-line react/jsx-props-no-spreading {...this._props} whenChildContentsActiveChanged={this.whenChildContentsActiveChanged} fieldKey={this.fieldKey + '_' + this._urlHash} @@ -1169,6 +1241,12 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem ); } } +// eslint-disable-next-line prefer-arrow-callback ScriptingGlobals.add(function urlHash(url: string) { return stringHash(url); }); + +Docs.Prototypes.TemplateMap.set(DocumentType.WEB, { + layout: { view: WebBox, dataField: 'data' }, + options: { acl: '', _height: 300, _layout_fitWidth: true, _layout_nativeDimEditable: true, _layout_reflowVertical: true, waitForDoubleClickToClick: 'always', systemIcon: 'BsGlobe' }, +}); diff --git a/src/client/views/nodes/WebBoxRenderer.js b/src/client/views/nodes/WebBoxRenderer.js index 914adb404..6fb8f4957 100644 --- a/src/client/views/nodes/WebBoxRenderer.js +++ b/src/client/views/nodes/WebBoxRenderer.js @@ -1,3 +1,4 @@ +/* eslint-disable no-undef */ /** * * @param {StyleSheetList} styleSheets @@ -9,15 +10,14 @@ const ForeignHtmlRenderer = function (styleSheets) { * * @param {String} binStr */ - const binaryStringToBase64 = function (binStr) { - return new Promise(resolve => { + const binaryStringToBase64 = binStr => + new Promise(resolve => { const reader = new FileReader(); reader.readAsDataURL(binStr); reader.onloadend = function () { resolve(reader.result); }; }); - }; function prepend(extension) { return window.location.origin + extension; @@ -30,8 +30,8 @@ const ForeignHtmlRenderer = function (styleSheets) { * @param {String} url * @returns {Promise} */ - const getResourceAsBase64 = function (webUrl, inurl) { - return new Promise((resolve, reject) => { + const getResourceAsBase64 = (webUrl, inurl) => + new Promise(resolve => { const xhr = new XMLHttpRequest(); // const url = inurl.startsWith("/") && !inurl.startsWith("//") ? webUrl + inurl : inurl; // const url = CorsProxy(inurl.startsWith("/") && !inurl.startsWith("//") ? webUrl + inurl : inurl);// inurl.startsWith("http") ? CorsProxy(inurl) : inurl; @@ -67,16 +67,15 @@ const ForeignHtmlRenderer = function (styleSheets) { xhr.send(null); }); - }; /** * * @param {String[]} urls * @returns {Promise} */ - const getMultipleResourcesAsBase64 = function (webUrl, urls) { + const getMultipleResourcesAsBase64 = (webUrl, urls) => { const promises = []; - for (let i = 0; i < urls.length; i += 1) { + for (let i = 0; webUrl && i < urls.length; i += 1) { promises.push(getResourceAsBase64(webUrl, urls[i])); } return Promise.all(promises); @@ -130,6 +129,7 @@ const ForeignHtmlRenderer = function (styleSheets) { const urlsFound = []; let searchStartIndex = 0; + // eslint-disable-next-line no-constant-condition while (true) { const url = parseValue(cssRuleStr, searchStartIndex, selector, delimiters); if (url === null) { @@ -155,9 +155,6 @@ const ForeignHtmlRenderer = function (styleSheets) { const getImageUrlsFromFromHtml = function (html) { return getUrlsFromCssString(html, 'src=', [' ', '>', '\t'], true); }; - const getSourceUrlsFromFromHtml = function (html) { - return getUrlsFromCssString(html, 'source=', [' ', '>', '\t'], true); - }; const escapeRegExp = function (string) { return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string @@ -171,46 +168,45 @@ const ForeignHtmlRenderer = function (styleSheets) { * * @returns {Promise<String>} */ - const buildSvgDataUri = async function (webUrl, inputContentHtml, width, height, scroll, xoff) { - return new Promise(async (resolve, reject) => { - /* !! The problems !! - * 1. CORS (not really an issue, expect perhaps for images, as this is a general security consideration to begin with) - * 2. Platform won't wait for external assets to load (fonts, images, etc.) - */ - - // copy styles - let cssStyles = ''; - const urlsFoundInCss = []; - - for (let i = 0; i < styleSheets.length; i += 1) { - try { - const rules = styleSheets[i].cssRules; - for (let j = 0; j < rules.length; j += 1) { - const cssRuleStr = rules[j].cssText; - urlsFoundInCss.push(...getUrlsFromCssString(cssRuleStr)); - cssStyles += cssRuleStr; - } - } catch (e) { - /* empty */ + const buildSvgDataUri = (webUrl, inputContentHtml, width, height, scroll, xoff) => { + /* !! The problems !! + * 1. CORS (not really an issue, expect perhaps for images, as this is a general security consideration to begin with) + * 2. Platform won't wait for external assets to load (fonts, images, etc.) + */ + + // copy styles + let cssStyles = ''; + const urlsFoundInCss = []; + + for (let i = 0; i < styleSheets.length; i += 1) { + try { + const rules = styleSheets[i].cssRules; + for (let j = 0; j < rules.length; j += 1) { + const cssRuleStr = rules[j].cssText; + urlsFoundInCss.push(...getUrlsFromCssString(cssRuleStr)); + cssStyles += cssRuleStr; } + } catch (e) { + /* empty */ } + } - // const fetchedResourcesFromStylesheets = await getMultipleResourcesAsBase64(webUrl, urlsFoundInCss); - // for (let i = 0; i < fetchedResourcesFromStylesheets.length; i++) { - // const r = fetchedResourcesFromStylesheets[i]; - // if (r.resourceUrl) { - // cssStyles = cssStyles.replace(new RegExp(escapeRegExp(r.resourceUrl), "g"), r.resourceBase64); - // } - // } - - let contentHtml = inputContentHtml - .replace(/<source[^>]*>/g, '') // <picture> tags have a <source> which has a srcset field of image refs. instead of converting each, just use the default <img> of the picture - .replace(/noscript/g, 'div') - .replace(/<div class="mediaset"><\/div>/g, '') // when scripting isn't available (ie, rendering web pages here), <noscript> tags should become <div>'s. But for Brown CS, there's a layout problem if you leave the empty <mediaset> tag - .replace(/<link[^>]*>/g, '') // don't need to keep any linked style sheets because we've already processed all style sheets above - .replace(/srcset="([^ "]*)[^"]*"/g, 'src="$1"'); // instead of converting each item in the srcset to a data url, just convert the first one and use that - const urlsFoundInHtml = getImageUrlsFromFromHtml(contentHtml).filter(url => !url.startsWith('data:')); - const fetchedResources = webUrl ? await getMultipleResourcesAsBase64(webUrl, urlsFoundInHtml) : []; + // const fetchedResourcesFromStylesheets = await getMultipleResourcesAsBase64(webUrl, urlsFoundInCss); + // for (let i = 0; i < fetchedResourcesFromStylesheets.length; i++) { + // const r = fetchedResourcesFromStylesheets[i]; + // if (r.resourceUrl) { + // cssStyles = cssStyles.replace(new RegExp(escapeRegExp(r.resourceUrl), "g"), r.resourceBase64); + // } + // } + + let contentHtml = inputContentHtml + .replace(/<source[^>]*>/g, '') // <picture> tags have a <source> which has a srcset field of image refs. instead of converting each, just use the default <img> of the picture + .replace(/noscript/g, 'div') + .replace(/<div class="mediaset"><\/div>/g, '') // when scripting isn't available (ie, rendering web pages here), <noscript> tags should become <div>'s. But for Brown CS, there's a layout problem if you leave the empty <mediaset> tag + .replace(/<link[^>]*>/g, '') // don't need to keep any linked style sheets because we've already processed all style sheets above + .replace(/srcset="([^ "]*)[^"]*"/g, 'src="$1"'); // instead of converting each item in the srcset to a data url, just convert the first one and use that + const urlsFoundInHtml = getImageUrlsFromFromHtml(contentHtml).filter(url => !url.startsWith('data:')); + return getMultipleResourcesAsBase64(webUrl, urlsFoundInHtml).then(fetchedResources => { for (let i = 0; i < fetchedResources.length; i += 1) { const r = fetchedResources[i]; if (r.resourceUrl) { @@ -243,9 +239,7 @@ const ForeignHtmlRenderer = function (styleSheets) { </svg>`; // convert SVG to data-uri - const dataUri = `data:image/svg+xml;base64,${window.btoa(unescape(encodeURIComponent(svg)))}`; - - resolve(dataUri); + return `data:image/svg+xml;base64,${window.btoa(unescape(encodeURIComponent(svg)))}`; }); }; @@ -256,18 +250,19 @@ const ForeignHtmlRenderer = function (styleSheets) { * * @return {Promise<Image>} */ - this.renderToImage = async function (webUrl, html, width, height, scroll, xoff) { - return new Promise(async (resolve, reject) => { + this.renderToImage = (webUrl, html, width, height, scroll, xoff) => + new Promise(resolve => { const img = new Image(); - console.log(`BUILDING SVG for: ${webUrl}`); - img.src = await buildSvgDataUri(webUrl, html, width, height, scroll, xoff); - img.onload = function () { console.log(`IMAGE SVG created: ${webUrl}`); resolve(img); }; + console.log(`BUILDING SVG for: ${webUrl}`); + buildSvgDataUri(webUrl, html, width, height, scroll, xoff).then(uri => { + img.src = uri; + return img; + }); }); - }; /** * @param {String} html @@ -276,20 +271,16 @@ const ForeignHtmlRenderer = function (styleSheets) { * * @return {Promise<Image>} */ - this.renderToCanvas = async function (webUrl, html, width, height, scroll, xoff, oversample) { - return new Promise(async (resolve, reject) => { - const img = await self.renderToImage(webUrl, html, width, height, scroll, xoff); - + this.renderToCanvas = (webUrl, html, width, height, scroll, xoff, oversample) => + self.renderToImage(webUrl, html, width, height, scroll, xoff).then(img => { const canvas = document.createElement('canvas'); canvas.width = img.width * oversample; canvas.height = img.height * oversample; const canvasCtx = canvas.getContext('2d'); canvasCtx.drawImage(img, 0, 0, img.width * oversample, img.height * oversample); - - resolve(canvas); + return canvas; }); - }; /** * @param {String} html @@ -298,12 +289,10 @@ const ForeignHtmlRenderer = function (styleSheets) { * * @return {Promise<String>} */ - this.renderToBase64Png = async function (webUrl, html, width, height, scroll, xoff, oversample) { - return new Promise(async (resolve, reject) => { - const canvas = await self.renderToCanvas(webUrl, html, width, height, scroll, xoff, oversample); - resolve(canvas.toDataURL('image/png')); - }); - }; + this.renderToBase64Png = (webUrl, html, width, height, scroll, xoff, oversample) => + self + .renderToCanvas(webUrl, html, width, height, scroll, xoff, oversample) // + .then(canvas => canvas.toDataURL('image/png')); }; export function CreateImage(webUrl, styleSheets, html, width, height, scroll, xoff = 0, oversample = 1) { @@ -379,11 +368,11 @@ const ClipboardUtils = new (function () { .then(result => { loadFile(result, callback); }) - .catch(error => { + .catch(() => { callback(null, 'Reading clipboard error.'); }); }) - .catch(error => { + .catch(() => { callback(null, 'Reading clipboard error.'); }); } else { diff --git a/src/client/views/nodes/audio/AudioWaveform.tsx b/src/client/views/nodes/audio/AudioWaveform.tsx index 7fd799952..2d1d3d7db 100644 --- a/src/client/views/nodes/audio/AudioWaveform.tsx +++ b/src/client/views/nodes/audio/AudioWaveform.tsx @@ -7,8 +7,8 @@ import { List } from '../../../../fields/List'; import { listSpec } from '../../../../fields/Schema'; import { Cast } from '../../../../fields/Types'; import { numberRange } from '../../../../Utils'; +import { Colors } from '../../global/globalEnums'; import { ObservableReactComponent } from '../../ObservableReactComponent'; -import { Colors } from './../../global/globalEnums'; import './AudioWaveform.scss'; import { WaveCanvas } from './WaveCanvas'; @@ -62,7 +62,7 @@ export class AudioWaveform extends ObservableReactComponent<AudioWaveformProps> 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; + audioBucketField = (start: number, end: number, zoomFactor: number) => this._props.fieldKey + '_audioBuckets//' + start.toFixed(2).replace('.', '_') + '/' + end.toFixed(2).replace('.', '_') + '/' + zoomFactor * 10; componentWillUnmount() { this._disposer?.(); @@ -72,7 +72,7 @@ export class AudioWaveform extends ObservableReactComponent<AudioWaveformProps> 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') { + 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)); @@ -109,7 +109,7 @@ export class AudioWaveform extends ObservableReactComponent<AudioWaveformProps> progressColor={Colors.MEDIUM_BLUE_ALT} progress={this._props.progress ?? 1} barWidth={200 / this.audioBuckets.length} - //gradientColors={this._props.gradientColors} + // gradientColors={this._props.gradientColors} peaks={this.audioBuckets} width={(this._props.PanelWidth ?? 0) * window.devicePixelRatio} height={this.waveHeight * window.devicePixelRatio} diff --git a/src/client/views/nodes/audio/WaveCanvas.tsx b/src/client/views/nodes/audio/WaveCanvas.tsx index d3f5669a2..eacda2d42 100644 --- a/src/client/views/nodes/audio/WaveCanvas.tsx +++ b/src/client/views/nodes/audio/WaveCanvas.tsx @@ -1,3 +1,4 @@ +/* eslint-disable react/require-default-props */ import React from 'react'; interface WaveCanvasProps { @@ -14,7 +15,7 @@ interface WaveCanvasProps { 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); + 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, @@ -47,6 +48,7 @@ export class WaveCanvas extends React.Component<WaveCanvasProps> { // A half-pixel offset makes lines crisp const $ = 0.5 / this.props.pixelRatio; + // eslint-disable-next-line no-bitwise const length = ~~(allPeaks.length / 2); // ~~ is Math.floor for positive numbers. const scale = width / length; @@ -55,14 +57,14 @@ export class WaveCanvas extends React.Component<WaveCanvasProps> { waveCanvasCtx.beginPath(); waveCanvasCtx.moveTo($, halfH); - for (var i = 0; i < length; i++) { - var h = Math.round((allPeaks[2 * i] / absmax) * halfH); + for (let i = 0; i < length; i++) { + const 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); + for (let i = length - 1; i >= 0; i--) { + const h = Math.round((allPeaks[2 * i + 1] / absmax) * halfH); waveCanvasCtx.lineTo(i * scale + $, halfH - h); } diff --git a/src/client/views/nodes/calendarBox/CalendarBox.tsx b/src/client/views/nodes/calendarBox/CalendarBox.tsx index 748c3322e..bd66941c3 100644 --- a/src/client/views/nodes/calendarBox/CalendarBox.tsx +++ b/src/client/views/nodes/calendarBox/CalendarBox.tsx @@ -1,12 +1,14 @@ -import { Calendar, EventClickArg, EventSourceInput } from '@fullcalendar/core'; +import { Calendar, EventSourceInput } from '@fullcalendar/core'; import dayGridPlugin from '@fullcalendar/daygrid'; import multiMonthPlugin from '@fullcalendar/multimonth'; import { makeObservable } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; -import { dateRangeStrToDates } from '../../../../Utils'; +import { dateRangeStrToDates } from '../../../../ClientUtils'; import { Doc } from '../../../../fields/Doc'; import { StrCast } from '../../../../fields/Types'; +import { DocumentType } from '../../../documents/DocumentTypes'; +import { Docs } from '../../../documents/Documents'; import { ViewBoxBaseComponent } from '../../DocComponent'; import { FieldView, FieldViewProps } from '../FieldView'; @@ -57,12 +59,13 @@ export class CalendarBox extends ViewBoxBaseComponent<FieldViewProps>() { docBackgroundColor(type: string): string { // TODO: Return a different color based on the event type + console.log(type); return 'blue'; } get calendarEvents(): EventSourceInput | undefined { if (this.childDocs.length === 0) return undefined; - return this.childDocs.map((doc, idx) => { + return this.childDocs.map(doc => { const docTitle = StrCast(doc.title); const docDateRange = StrCast(doc.date_range); const [startDate, endDate] = dateRangeStrToDates(docDateRange); @@ -85,7 +88,7 @@ export class CalendarBox extends ViewBoxBaseComponent<FieldViewProps>() { }); } - handleEventClick = (arg: EventClickArg) => { + handleEventClick = (/* arg: EventClickArg */) => { // TODO: open popover with event description, option to open CalendarManager and change event date, delete event, etc. }; @@ -113,8 +116,12 @@ export class CalendarBox extends ViewBoxBaseComponent<FieldViewProps>() { render() { return ( <div className="calendar-box-conatiner"> - <div id="calendar-box-v1"></div> + <div id="calendar-box-v1" /> </div> ); } } +Docs.Prototypes.TemplateMap.set(DocumentType.CALENDAR, { + layout: { view: CalendarBox, dataField: 'data' }, + options: { acl: '' }, +}); diff --git a/src/client/views/nodes/formattedText/DashDocCommentView.tsx b/src/client/views/nodes/formattedText/DashDocCommentView.tsx index a72ed1813..3ec49fa27 100644 --- a/src/client/views/nodes/formattedText/DashDocCommentView.tsx +++ b/src/client/views/nodes/formattedText/DashDocCommentView.tsx @@ -1,60 +1,11 @@ import { TextSelection } from 'prosemirror-state'; import * as ReactDOM from 'react-dom/client'; -import { Doc } from '../../../../fields/Doc'; -import { DocServer } from '../../../DocServer'; import * as React from 'react'; import { IReactionDisposer, computed, reaction } from 'mobx'; +import { Doc } from '../../../../fields/Doc'; +import { DocServer } from '../../../DocServer'; import { NumCast } from '../../../../fields/Types'; -// creates an inline comment in a note when '>>' is typed. -// the comment sits on the right side of the note and vertically aligns with its anchor in the text. -// the comment can be toggled on/off with the '<-' text anchor. -export class DashDocCommentView { - dom: HTMLDivElement; // container for label and value - root: any; - node: any; - - constructor(node: any, view: any, getPos: any) { - this.node = node; - this.dom = document.createElement('div'); - this.dom.style.width = node.attrs.width; - this.dom.style.height = node.attrs.height; - this.dom.style.fontWeight = 'bold'; - this.dom.style.position = 'relative'; - this.dom.style.display = 'inline-block'; - this.dom.onkeypress = function (e: any) { - e.stopPropagation(); - }; - this.dom.onkeydown = function (e: any) { - e.stopPropagation(); - }; - this.dom.onkeyup = function (e: any) { - e.stopPropagation(); - }; - this.dom.onmousedown = function (e: any) { - e.stopPropagation(); - }; - - this.root = ReactDOM.createRoot(this.dom); - this.root.render(<DashDocCommentViewInternal view={view} getPos={getPos} setHeight={this.setHeight} docId={node.attrs.docId} />); - (this as any).dom = this.dom; - } - - setHeight = (hgt: number) => { - !this.node.attrs.reflow && DocServer.GetRefField(this.node.attrs.docId).then(doc => doc instanceof Doc && (this.dom.style.height = hgt + '')); - }; - - destroy() { - this.root.unmount(); - } - deselectNode() { - this.dom.classList.remove('ProseMirror-selectednode'); - } - selectNode() { - this.dom.classList.add('ProseMirror-selectednode'); - } -} - interface IDashDocCommentViewInternal { docId: string; view: any; @@ -65,9 +16,6 @@ interface IDashDocCommentViewInternal { export class DashDocCommentViewInternal extends React.Component<IDashDocCommentViewInternal> { _reactionDisposer: IReactionDisposer | undefined; - @computed get _dashDoc() { - return DocServer.GetRefField(this.props.docId); - } constructor(props: any) { super(props); this.onPointerLeaveCollapsed = this.onPointerLeaveCollapsed.bind(this); @@ -77,58 +25,62 @@ export class DashDocCommentViewInternal extends React.Component<IDashDocCommentV } componentDidMount(): void { this._reactionDisposer?.(); - this._dashDoc.then( - doc => - doc instanceof Doc && - (this._reactionDisposer = reaction( + this._dashDoc.then(doc => { + if (doc instanceof Doc) { + this._reactionDisposer = reaction( () => NumCast((doc as Doc)._height), hgt => this.props.setHeight(hgt), - { - fireImmediately: true, - } - )) - ); + { fireImmediately: true } + ); + } + }); } componentWillUnmount(): void { this._reactionDisposer?.(); } - onPointerLeaveCollapsed(e: any) { + @computed get _dashDoc() { + return DocServer.GetRefField(this.props.docId); + } + + onPointerLeaveCollapsed = (e: any) => { this._dashDoc.then(async dashDoc => dashDoc instanceof Doc && Doc.linkFollowUnhighlight()); e.preventDefault(); e.stopPropagation(); - } + }; - onPointerEnterCollapsed(e: any) { + onPointerEnterCollapsed = (e: any) => { this._dashDoc.then(async dashDoc => dashDoc instanceof Doc && Doc.linkFollowHighlight(dashDoc, false)); e.preventDefault(); e.stopPropagation(); - } + }; - onPointerUpCollapsed(e: any) { + onPointerUpCollapsed = (e: any) => { const target = this.targetNode(); if (target) { const expand = target.hidden; - const tr = this.props.view.state.tr.setNodeMarkup(target.pos, undefined, { ...target.node.attrs, hidden: target.node.attrs.hidden ? false : true }); + const tr = this.props.view.state.tr.setNodeMarkup(target.pos, undefined, { ...target.node.attrs, hidden: !target.node.attrs.hidden }); this.props.view.dispatch(tr.setSelection(TextSelection.create(tr.doc, this.props.getPos() + (expand ? 2 : 1)))); // update the attrs setTimeout(() => { expand && this._dashDoc.then(async dashDoc => dashDoc instanceof Doc && Doc.linkFollowHighlight(dashDoc)); try { this.props.view.dispatch(this.props.view.state.tr.setSelection(TextSelection.create(this.props.view.state.tr.doc, this.props.getPos() + (expand ? 2 : 1)))); - } catch (e) {} + } catch (err) { + /* empty */ + } }, 0); } e.stopPropagation(); - } + }; - onPointerDownCollapsed(e: any) { + onPointerDownCollapsed = (e: any) => { e.stopPropagation(); - } + }; targetNode = () => { // search forward in the prosemirror doc for the attached dashDocNode that is the target of the comment anchor - const state = this.props.view.state; + const { state } = this.props.view; for (let i = this.props.getPos() + 1; i < state.doc.content.size; i++) { const m = state.doc.nodeAt(i); if (m && m.type === state.schema.nodes.dashDoc && m.attrs.docId === this.props.docId) { @@ -141,7 +93,9 @@ export class DashDocCommentViewInternal extends React.Component<IDashDocCommentV setTimeout(() => { try { this.props.view.dispatch(state.tr.setSelection(TextSelection.create(state.tr.doc, this.props.getPos() + 2))); - } catch (e) {} + } catch (err) { + /* empty */ + } }, 0); return undefined; }; @@ -154,7 +108,60 @@ export class DashDocCommentViewInternal extends React.Component<IDashDocCommentV onPointerLeave={this.onPointerLeaveCollapsed} onPointerEnter={this.onPointerEnterCollapsed} onPointerUp={this.onPointerUpCollapsed} - onPointerDown={this.onPointerDownCollapsed}></span> + onPointerDown={this.onPointerDownCollapsed} + /> ); } } + +// creates an inline comment in a note when '>>' is typed. +// the comment sits on the right side of the note and vertically aligns with its anchor in the text. +// the comment can be toggled on/off with the '<-' text anchor. +export class DashDocCommentView { + dom: HTMLDivElement; // container for label and value + root: any; + node: any; + + constructor(node: any, view: any, getPos: any) { + this.node = node; + this.dom = document.createElement('div'); + this.dom.style.width = node.attrs.width; + this.dom.style.height = node.attrs.height; + this.dom.style.fontWeight = 'bold'; + this.dom.style.position = 'relative'; + this.dom.style.display = 'inline-block'; + this.dom.onkeypress = function (e: any) { + e.stopPropagation(); + }; + this.dom.onkeydown = function (e: any) { + e.stopPropagation(); + }; + this.dom.onkeyup = function (e: any) { + e.stopPropagation(); + }; + this.dom.onmousedown = function (e: any) { + e.stopPropagation(); + }; + + this.root = ReactDOM.createRoot(this.dom); + this.root.render(<DashDocCommentViewInternal view={view} getPos={getPos} setHeight={this.setHeight} docId={node.attrs.docId} />); + (this as any).dom = this.dom; + } + + setHeight = (hgt: number) => { + !this.node.attrs.reflow && + DocServer.GetRefField(this.node.attrs.docId).then(doc => { + doc instanceof Doc && (this.dom.style.height = hgt + ''); + }); + }; + + destroy() { + this.root.unmount(); + } + deselectNode() { + this.dom.classList.remove('ProseMirror-selectednode'); + } + selectNode() { + this.dom.classList.add('ProseMirror-selectednode'); + } +} diff --git a/src/client/views/nodes/formattedText/DashDocView.tsx b/src/client/views/nodes/formattedText/DashDocView.tsx index 7335c9286..93371685d 100644 --- a/src/client/views/nodes/formattedText/DashDocView.tsx +++ b/src/client/views/nodes/formattedText/DashDocView.tsx @@ -1,77 +1,23 @@ +/* eslint-disable jsx-a11y/no-static-element-interactions */ import { action, computed, IReactionDisposer, makeObservable, observable, reaction } from 'mobx'; import { observer } from 'mobx-react'; import { NodeSelection } from 'prosemirror-state'; import * as React from 'react'; import * as ReactDOM from 'react-dom/client'; +import { ClientUtils, returnFalse } from '../../../../ClientUtils'; import { Doc } from '../../../../fields/Doc'; import { Height, Width } from '../../../../fields/DocSymbols'; import { NumCast } from '../../../../fields/Types'; -import { emptyFunction, returnFalse, Utils } from '../../../../Utils'; import { DocServer } from '../../../DocServer'; -import { Docs, DocUtils } from '../../../documents/Documents'; +import { Docs } from '../../../documents/Documents'; +import { DocUtils } from '../../../documents/DocUtils'; import { Transform } from '../../../util/Transform'; import { ObservableReactComponent } from '../../ObservableReactComponent'; import { DocumentView } from '../DocumentView'; -import { FocusViewOptions } from '../FieldView'; +import { FocusViewOptions } from '../FocusViewOptions'; import { FormattedTextBox } from './FormattedTextBox'; -var horizPadding = 3; // horizontal padding to container to allow cursor to show up on either side. -export class DashDocView { - dom: HTMLSpanElement; // container for label and value - root: any; - - constructor(node: any, view: any, getPos: any, tbox: FormattedTextBox) { - this.dom = document.createElement('span'); - this.dom.style.position = 'relative'; - this.dom.style.textIndent = '0'; - this.dom.style.width = (+node.attrs.width.toString().replace('px', '') + horizPadding).toString(); - this.dom.style.height = node.attrs.height; - this.dom.style.display = node.attrs.hidden ? 'none' : 'inline-block'; - (this.dom.style as any).float = node.attrs.float; - this.dom.onkeypress = function (e: any) { - e.stopPropagation(); - }; - this.dom.onkeydown = function (e: any) { - e.stopPropagation(); - }; - this.dom.onkeyup = function (e: any) { - e.stopPropagation(); - }; - this.dom.onmousedown = function (e: any) { - e.stopPropagation(); - }; - - this.root = ReactDOM.createRoot(this.dom); - this.root.render( - <DashDocViewInternal - docId={node.attrs.docId} - embedding={node.attrs.embedding} - width={node.attrs.width} - height={node.attrs.height} - hidden={node.attrs.hidden} - fieldKey={node.attrs.fieldKey} - tbox={tbox} - view={view} - node={node} - getPos={getPos} - /> - ); - } - destroy() { - setTimeout(() => { - try { - this.root.unmount(); - } catch {} - }); - } - deselectNode() { - this.dom.style.backgroundColor = ''; - } - selectNode() { - this.dom.style.backgroundColor = 'rgb(141, 182, 247)'; - } -} - +const horizPadding = 3; // horizontal padding to container to allow cursor to show up on either side. interface IDashDocViewInternal { docId: string; embedding: string; @@ -84,6 +30,7 @@ interface IDashDocViewInternal { node: any; getPos: any; } + @observer export class DashDocViewInternal extends ObservableReactComponent<IDashDocViewInternal> { _spanRef = React.createRef<HTMLDivElement>(); @@ -157,7 +104,7 @@ export class DashDocViewInternal extends ObservableReactComponent<IDashDocViewIn getDocTransform = () => { if (!this._spanRef.current) return Transform.Identity(); - const { scale, translateX, translateY } = Utils.GetScreenTransform(this._spanRef.current); + const { scale, translateX, translateY } = ClientUtils.GetScreenTransform(this._spanRef.current); return new Transform(-translateX, -translateY, 1).scale(1 / scale); }; outerFocus = (target: Doc, options: FocusViewOptions) => this._textBox.focus(target, options); // ideally, this would scroll to show the focus target @@ -226,3 +173,61 @@ export class DashDocViewInternal extends ObservableReactComponent<IDashDocViewIn ); } } + +export class DashDocView { + dom: HTMLSpanElement; // container for label and value + root: any; + + constructor(node: any, view: any, getPos: any, tbox: FormattedTextBox) { + this.dom = document.createElement('span'); + this.dom.style.position = 'relative'; + this.dom.style.textIndent = '0'; + this.dom.style.width = (+node.attrs.width.toString().replace('px', '') + horizPadding).toString(); + this.dom.style.height = node.attrs.height; + this.dom.style.display = node.attrs.hidden ? 'none' : 'inline-block'; + (this.dom.style as any).float = node.attrs.float; + this.dom.onkeypress = function (e: any) { + e.stopPropagation(); + }; + this.dom.onkeydown = function (e: any) { + e.stopPropagation(); + }; + this.dom.onkeyup = function (e: any) { + e.stopPropagation(); + }; + this.dom.onmousedown = function (e: any) { + e.stopPropagation(); + }; + + this.root = ReactDOM.createRoot(this.dom); + this.root.render( + <DashDocViewInternal + docId={node.attrs.docId} + embedding={node.attrs.embedding} + width={node.attrs.width} + height={node.attrs.height} + hidden={node.attrs.hidden} + fieldKey={node.attrs.fieldKey} + tbox={tbox} + view={view} + node={node} + getPos={getPos} + /> + ); + } + destroy() { + setTimeout(() => { + try { + this.root.unmount(); + } catch { + /* empty */ + } + }); + } + deselectNode() { + this.dom.style.backgroundColor = ''; + } + selectNode() { + this.dom.style.backgroundColor = 'rgb(141, 182, 247)'; + } +} diff --git a/src/client/views/nodes/formattedText/DashFieldView.tsx b/src/client/views/nodes/formattedText/DashFieldView.tsx index 17b8b53e7..9903d0e8a 100644 --- a/src/client/views/nodes/formattedText/DashFieldView.tsx +++ b/src/client/views/nodes/formattedText/DashFieldView.tsx @@ -1,15 +1,20 @@ +/* eslint-disable jsx-a11y/no-static-element-interactions */ +/* eslint-disable jsx-a11y/click-events-have-key-events */ +/* eslint-disable jsx-a11y/control-has-associated-label */ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { Tooltip } from '@mui/material'; -import { action, computed, IReactionDisposer, makeObservable, observable, reaction, runInAction, trace } from 'mobx'; +import { action, computed, IReactionDisposer, makeObservable, observable, reaction, runInAction } from 'mobx'; import { observer } from 'mobx-react'; +import { NodeSelection } from 'prosemirror-state'; import * as React from 'react'; import * as ReactDOM from 'react-dom/client'; +import { returnFalse, returnZero, setupMoveUpEvents } from '../../../../ClientUtils'; import { Doc, DocListCast, Field } from '../../../../fields/Doc'; import { List } from '../../../../fields/List'; import { listSpec } from '../../../../fields/Schema'; import { SchemaHeaderField } from '../../../../fields/SchemaHeaderField'; import { Cast, DocCast } from '../../../../fields/Types'; -import { emptyFunction, returnFalse, returnZero, setupMoveUpEvents } from '../../../../Utils'; +import { emptyFunction } from '../../../../Utils'; import { DocServer } from '../../../DocServer'; import { CollectionViewType } from '../../../documents/DocumentTypes'; import { Transform } from '../../../util/Transform'; @@ -18,96 +23,73 @@ import { AntimodeMenu, AntimodeMenuProps } from '../../AntimodeMenu'; import { SchemaTableCell } from '../../collections/collectionSchema/SchemaTableCell'; import { FilterPanel } from '../../FilterPanel'; import { ObservableReactComponent } from '../../ObservableReactComponent'; -import { OpenWhere } from '../DocumentView'; +import { OpenWhere } from '../OpenWhere'; import './DashFieldView.scss'; import { FormattedTextBox } from './FormattedTextBox'; -import { DocData } from '../../../../fields/DocSymbols'; -import { NodeSelection } from 'prosemirror-state'; -export class DashFieldView { - dom: HTMLDivElement; // container for label and value - root: any; - node: any; - tbox: FormattedTextBox; - getpos: any; - @observable _nodeSelected = false; - NodeSelected = () => this._nodeSelected; +@observer +export class DashFieldViewMenu extends AntimodeMenu<AntimodeMenuProps> { + // eslint-disable-next-line no-use-before-define + static Instance: DashFieldViewMenu; + static createFieldView: (e: React.MouseEvent) => void = emptyFunction; + static toggleFieldHide: () => void = emptyFunction; + static toggleValueHide: () => void = emptyFunction; + constructor(props: any) { + super(props); + DashFieldViewMenu.Instance = this; + } - unclickable = () => !this.tbox._props.rootSelected?.() && 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) { - makeObservable(this); - const self = this; - this.node = node; - this.tbox = tbox; - this.getpos = getPos; - this.dom = document.createElement('div'); - this.dom.style.width = node.attrs.width; - this.dom.style.height = node.attrs.height; - this.dom.style.position = 'relative'; - this.dom.style.display = 'inline-block'; - const tBox = this.tbox; - this.dom.onkeypress = function (e: KeyboardEvent) { - e.stopPropagation(); - }; - this.dom.onkeydown = function (e: KeyboardEvent) { - e.stopPropagation(); - if (e.key === 'Tab') { - e.preventDefault(); - const editor = tbox.EditorView; - if (editor) { - const state = editor.state; - for (var i = self.getpos() + 1; i < state.doc.content.size; i++) { - if (state.doc.nodeAt(i)?.type.name === state.schema.nodes.dashField.name) { - editor.dispatch(state.tr.setSelection(new NodeSelection(state.doc.resolve(i)))); - return; - } - } - // tBox.setFocus(state.selection.to); - } - } - }; - this.dom.onkeyup = function (e: any) { - e.stopPropagation(); - }; - this.dom.onmousedown = function (e: any) { - e.stopPropagation(); - }; + showFields = (e: React.MouseEvent) => { + DashFieldViewMenu.createFieldView(e); + DashFieldViewMenu.Instance.fadeOut(true); + }; + toggleFieldHide = () => { + DashFieldViewMenu.toggleFieldHide(); + DashFieldViewMenu.Instance.fadeOut(true); + }; + toggleValueHide = () => { + DashFieldViewMenu.toggleValueHide(); + DashFieldViewMenu.Instance.fadeOut(true); + }; - this.root = ReactDOM.createRoot(this.dom); - this.root.render( - <DashFieldViewInternal - node={node} - unclickable={this.unclickable} - getPos={getPos} - fieldKey={node.attrs.fieldKey} - docId={node.attrs.docId} - width={node.attrs.width} - height={node.attrs.height} - hideKey={node.attrs.hideKey} - hideValue={node.attrs.hideValue} - editable={node.attrs.editable} - nodeSelected={this.NodeSelected} - tbox={tbox} - /> + @observable _fieldKey = ''; + + @action + public show = (x: number, y: number, fieldKey: string) => { + this._fieldKey = fieldKey; + this.jumpTo(x, y, true); + const hideMenu = () => { + this.fadeOut(true); + document.removeEventListener('pointerdown', hideMenu, true); + }; + document.addEventListener('pointerdown', hideMenu, true); + }; + render() { + return this.getElement( + <> + <Tooltip key="trash" title={<div className="dash-tooltip">{`Show Pivot Viewer for '${this._fieldKey}'`}</div>}> + <button type="button" className="antimodeMenu-button" onPointerDown={this.showFields}> + <FontAwesomeIcon icon="eye" size="sm" /> + </button> + </Tooltip> + {this._fieldKey.startsWith('#') ? null : ( + <Tooltip key="key" title={<div className="dash-tooltip">Toggle view of field key</div>}> + <button type="button" className="antimodeMenu-button" onPointerDown={this.toggleFieldHide}> + <FontAwesomeIcon icon="bullseye" size="sm" /> + </button> + </Tooltip> + )} + {this._fieldKey.startsWith('#') ? null : ( + <Tooltip key="val" title={<div className="dash-tooltip">Toggle view of field value</div>}> + <button type="button" className="antimodeMenu-button" onPointerDown={this.toggleValueHide}> + <FontAwesomeIcon icon="hashtag" size="sm" /> + </button> + </Tooltip> + )} + </> ); } - destroy() { - setTimeout(() => { - try { - this.root.unmount(); - } catch {} - }); - } - deselectNode() { - runInAction(() => (this._nodeSelected = false)); - this.dom.classList.remove('ProseMirror-selectednode'); - } - selectNode() { - setTimeout(() => runInAction(() => (this._nodeSelected = true)), 100); - this.dom.classList.add('ProseMirror-selectednode'); - } } - interface IDashFieldViewInternal { fieldKey: string; docId: string; @@ -137,7 +119,9 @@ export class DashFieldViewInternal extends ObservableReactComponent<IDashFieldVi makeObservable(this); this._fieldKey = this._props.fieldKey; this._textBoxDoc = this._props.tbox.Document; - const setDoc = action((doc: Doc) => (this._dashDoc = doc)); + const setDoc = action((doc: Doc) => { + this._dashDoc = doc; + }); if (this._props.docId) { DocServer.GetRefField(this._props.docId).then(dashDoc => dashDoc instanceof Doc && setDoc(dashDoc)); @@ -171,7 +155,11 @@ export class DashFieldViewInternal extends ObservableReactComponent<IDashFieldVi // set the display of the field's value (checkbox for booleans, span of text for strings) @computed get fieldValueContent() { return !this._dashDoc ? null : ( - <div onClick={action(e => (this._expanded = !this._props.editable ? !this._expanded : true))} style={{ fontSize: 'smaller', width: !this._hideKey && this._expanded ? this.columnWidth() : undefined }}> + <div + onClick={action(() => { + this._expanded = !this._props.editable ? !this._expanded : true; + })} + style={{ fontSize: 'smaller', width: !this._hideKey && this._expanded ? this.columnWidth() : undefined }}> <SchemaTableCell Document={this._dashDoc} col={0} @@ -188,20 +176,20 @@ export class DashFieldViewInternal extends ObservableReactComponent<IDashFieldVi getFinfo={emptyFunction} setColumnValues={returnFalse} setSelectedColumnValues={returnFalse} - allowCRs={true} + allowCRs oneLine={!this._expanded && !this._props.nodeSelected()} finishEdit={this.finishEdit} transform={Transform.Identity} menuTarget={null} - autoFocus={true} + autoFocus rootSelected={this._props.tbox._props.rootSelected} /> </div> ); } - createPivotForField = (e: React.MouseEvent) => { - let container = this._props.tbox.DocumentView?.().containerViewPath?.().lastElement(); + createPivotForField = () => { + const container = this._props.tbox.DocumentView?.().containerViewPath?.().lastElement(); if (container) { const embedding = Doc.MakeEmbedding(container.Document); embedding._type_collection = CollectionViewType.Time; @@ -220,7 +208,7 @@ export class DashFieldViewInternal extends ObservableReactComponent<IDashFieldVi toggleFieldHide = undoable( action(() => { const editor = this._props.tbox.EditorView!; - editor.dispatch(editor.state.tr.setNodeMarkup(this._props.getPos(), this._props.node.type, { ...this._props.node.attrs, hideKey: this._props.node.attrs.hideValue ? false : !this._props.node.attrs.hideKey ? true : false })); + editor.dispatch(editor.state.tr.setNodeMarkup(this._props.getPos(), this._props.node.type, { ...this._props.node.attrs, hideKey: this._props.node.attrs.hideValue ? false : !this._props.node.attrs.hideKey })); }), 'hideKey' ); @@ -228,7 +216,7 @@ export class DashFieldViewInternal extends ObservableReactComponent<IDashFieldVi toggleValueHide = undoable( action(() => { const editor = this._props.tbox.EditorView!; - editor.dispatch(editor.state.tr.setNodeMarkup(this._props.getPos(), this._props.node.type, { ...this._props.node.attrs, hideValue: this._props.node.attrs.hideKey ? false : !this._props.node.attrs.hideValue ? true : false })); + editor.dispatch(editor.state.tr.setNodeMarkup(this._props.getPos(), this._props.node.type, { ...this._props.node.attrs, hideValue: this._props.node.attrs.hideKey ? false : !this._props.node.attrs.hideValue })); }), 'hideValue' ); @@ -244,11 +232,11 @@ export class DashFieldViewInternal extends ObservableReactComponent<IDashFieldVi // clicking on the label creates a pivot view collection of all documents // in the same collection. The pivot field is the fieldKey of this label onPointerDownLabelSpan = (e: React.PointerEvent) => { - setupMoveUpEvents(this, e, returnFalse, returnFalse, e => { + setupMoveUpEvents(this, e, returnFalse, returnFalse, moveEv => { DashFieldViewMenu.createFieldView = this.createPivotForField; DashFieldViewMenu.toggleFieldHide = this.toggleFieldHide; DashFieldViewMenu.toggleValueHide = this.toggleValueHide; - DashFieldViewMenu.Instance.show(e.clientX, e.clientY + 16, this._fieldKey); + DashFieldViewMenu.Instance.show(moveEv.clientX, moveEv.clientY + 16, this._fieldKey); const editor = this._props.tbox.EditorView!; setTimeout(() => editor.dispatch(editor.state.tr.setSelection(new NodeSelection(editor.state.doc.resolve(this._props.getPos())))), 100); }); @@ -278,7 +266,7 @@ export class DashFieldViewInternal extends ObservableReactComponent<IDashFieldVi }}> {this._hideKey ? null : ( <span className="dashFieldView-labelSpan" title="click to see related tags" onPointerDown={this.onPointerDownLabelSpan}> - {(Doc.AreProtosEqual(DocCast(this._textBoxDoc.rootDocument) ?? this._textBoxDoc, DocCast(this._dashDoc?.rootDocument) ?? this._dashDoc) ? '' : this._dashDoc?.title + ':') + this._fieldKey} + {(Doc.AreProtosEqual(DocCast(this._textBoxDoc.rootDocument) ?? this._textBoxDoc, DocCast(this._dashDoc?.rootDocument) ?? this._dashDoc) ? '' : (this._dashDoc?.title ?? '') + ':') + this._fieldKey} </span> )} {this._props.fieldKey.startsWith('#') || this._hideValue ? null : this.fieldValueContent} @@ -294,65 +282,92 @@ export class DashFieldViewInternal extends ObservableReactComponent<IDashFieldVi ); } } -@observer -export class DashFieldViewMenu extends AntimodeMenu<AntimodeMenuProps> { - static Instance: DashFieldViewMenu; - static createFieldView: (e: React.MouseEvent) => void = emptyFunction; - static toggleFieldHide: () => void = emptyFunction; - static toggleValueHide: () => void = emptyFunction; - constructor(props: any) { - super(props); - DashFieldViewMenu.Instance = this; - } - - showFields = (e: React.MouseEvent) => { - DashFieldViewMenu.createFieldView(e); - DashFieldViewMenu.Instance.fadeOut(true); - }; - toggleFieldHide = (e: React.MouseEvent) => { - DashFieldViewMenu.toggleFieldHide(); - DashFieldViewMenu.Instance.fadeOut(true); - }; - toggleValueHide = (e: React.MouseEvent) => { - DashFieldViewMenu.toggleValueHide(); - DashFieldViewMenu.Instance.fadeOut(true); - }; - - @observable _fieldKey = ''; +export class DashFieldView { + dom: HTMLDivElement; // container for label and value + root: any; + node: any; + tbox: FormattedTextBox; + getpos: any; + @observable _nodeSelected = false; + NodeSelected = () => this._nodeSelected; - @action - public show = (x: number, y: number, fieldKey: string) => { - this._fieldKey = fieldKey; - this.jumpTo(x, y, true); - const hideMenu = () => { - this.fadeOut(true); - document.removeEventListener('pointerdown', hideMenu, true); + unclickable = () => !this.tbox._props.rootSelected?.() && 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) { + makeObservable(this); + this.node = node; + this.tbox = tbox; + this.getpos = getPos; + this.dom = document.createElement('div'); + this.dom.style.width = node.attrs.width; + this.dom.style.height = node.attrs.height; + this.dom.style.position = 'relative'; + this.dom.style.display = 'inline-block'; + this.dom.onkeypress = function (e: KeyboardEvent) { + e.stopPropagation(); }; - document.addEventListener('pointerdown', hideMenu, true); - }; - render() { - return this.getElement( - <> - <Tooltip key="trash" title={<div className="dash-tooltip">{`Show Pivot Viewer for '${this._fieldKey}'`}</div>}> - <button className="antimodeMenu-button" onPointerDown={this.showFields}> - <FontAwesomeIcon icon="eye" size="sm" /> - </button> - </Tooltip> - {this._fieldKey.startsWith('#') ? null : ( - <Tooltip key="key" title={<div className="dash-tooltip">Toggle view of field key</div>}> - <button className="antimodeMenu-button" onPointerDown={this.toggleFieldHide}> - <FontAwesomeIcon icon="bullseye" size="sm" /> - </button> - </Tooltip> - )} - {this._fieldKey.startsWith('#') ? null : ( - <Tooltip key="val" title={<div className="dash-tooltip">Toggle view of field value</div>}> - <button className="antimodeMenu-button" onPointerDown={this.toggleValueHide}> - <FontAwesomeIcon icon="hashtag" size="sm" /> - </button> - </Tooltip> - )} - </> + this.dom.onkeydown = (e: KeyboardEvent) => { + e.stopPropagation(); + if (e.key === 'Tab') { + e.preventDefault(); + const editor = tbox.EditorView; + if (editor) { + const { state } = editor; + for (let i = this.getpos() + 1; i < state.doc.content.size; i++) { + if (state.doc.nodeAt(i)?.type.name === state.schema.nodes.dashField.name) { + editor.dispatch(state.tr.setSelection(new NodeSelection(state.doc.resolve(i)))); + return; + } + } + } + } + }; + this.dom.onkeyup = function (e: any) { + e.stopPropagation(); + }; + this.dom.onmousedown = function (e: any) { + e.stopPropagation(); + }; + + this.root = ReactDOM.createRoot(this.dom); + this.root.render( + <DashFieldViewInternal + node={node} + unclickable={this.unclickable} + getPos={getPos} + fieldKey={node.attrs.fieldKey} + docId={node.attrs.docId} + width={node.attrs.width} + height={node.attrs.height} + hideKey={node.attrs.hideKey} + hideValue={node.attrs.hideValue} + editable={node.attrs.editable} + nodeSelected={this.NodeSelected} + tbox={tbox} + /> ); } + destroy() { + setTimeout(() => { + try { + this.root.unmount(); + } catch { + /* empty */ + } + }); + } + deselectNode() { + runInAction(() => { + this._nodeSelected = false; + }); + this.dom.classList.remove('ProseMirror-selectednode'); + } + selectNode() { + setTimeout( + action(() => { + this._nodeSelected = true; + }), + 100 + ); + this.dom.classList.add('ProseMirror-selectednode'); + } } diff --git a/src/client/views/nodes/formattedText/EquationEditor.tsx b/src/client/views/nodes/formattedText/EquationEditor.tsx index b4102e08e..d9b1a2cf8 100644 --- a/src/client/views/nodes/formattedText/EquationEditor.tsx +++ b/src/client/views/nodes/formattedText/EquationEditor.tsx @@ -1,3 +1,4 @@ +/* eslint-disable react/require-default-props */ import React, { Component, createRef } from 'react'; // Import JQuery, required for the functioning of the equation editor @@ -5,11 +6,9 @@ import $ from 'jquery'; import './EquationEditor.scss'; -// eslint-disable-next-line @typescript-eslint/ban-ts-ignore // @ts-ignore window.jQuery = $; -// eslint-disable-next-line @typescript-eslint/ban-ts-ignore // @ts-ignore require('mathquill/build/mathquill'); diff --git a/src/client/views/nodes/formattedText/EquationView.tsx b/src/client/views/nodes/formattedText/EquationView.tsx index b90653acc..5167c8f2a 100644 --- a/src/client/views/nodes/formattedText/EquationView.tsx +++ b/src/client/views/nodes/formattedText/EquationView.tsx @@ -1,3 +1,4 @@ +/* eslint-disable jsx-a11y/no-static-element-interactions */ import { IReactionDisposer } from 'mobx'; import { observer } from 'mobx-react'; import { TextSelection } from 'prosemirror-state'; @@ -10,44 +11,6 @@ import EquationEditor from './EquationEditor'; import { FormattedTextBox } from './FormattedTextBox'; import { DocData } from '../../../../fields/DocSymbols'; -export class EquationView { - dom: HTMLDivElement; // container for label and value - root: any; - tbox: FormattedTextBox; - view: any; - constructor(node: any, view: any, getPos: any, tbox: FormattedTextBox) { - this.tbox = tbox; - this.view = view; - this.dom = document.createElement('div'); - this.dom.style.width = node.attrs.width; - this.dom.style.height = node.attrs.height; - this.dom.style.position = 'relative'; - this.dom.style.display = 'inline-block'; - this.dom.onmousedown = function (e: any) { - e.stopPropagation(); - }; - - this.root = ReactDOM.createRoot(this.dom); - this.root.render(<EquationViewInternal fieldKey={node.attrs.fieldKey} width={node.attrs.width} height={node.attrs.height} getPos={getPos} setEditor={this.setEditor} tbox={tbox} />); - } - _editor: EquationEditor | undefined; - setEditor = (editor?: EquationEditor) => (this._editor = editor); - destroy() { - this.root.unmount(); - } - setSelection() { - this._editor?.mathField.focus(); - } - selectNode() { - this.tbox._applyingChange = this.tbox.fieldKey; // setting focus will make prosemirror lose focus, which will cause it to change its selection to a text selection, which causes this view to get rebuilt but it's no longer node selected, so the equationview won't have focus - setTimeout(() => { - this._editor?.mathField.focus(); - setTimeout(() => (this.tbox._applyingChange = '')); - }); - } - deselectNode() {} -} - interface IEquationViewInternal { fieldKey: string; tbox: FormattedTextBox; @@ -70,12 +33,12 @@ export class EquationViewInternal extends React.Component<IEquationViewInternal> this._textBoxDoc = props.tbox.Document; } - componentWillUnmount() { - this._reactionDisposer?.(); - } componentDidMount() { this.props.setEditor(this._ref.current ?? undefined); } + componentWillUnmount() { + this._reactionDisposer?.(); + } render() { return ( @@ -100,12 +63,56 @@ export class EquationViewInternal extends React.Component<IEquationViewInternal> <EquationEditor ref={this._ref} value={StrCast(this._textBoxDoc[DocData][this._fieldKey])} - onChange={(str: any) => (this._textBoxDoc[DocData][this._fieldKey] = str)} + onChange={(str: any) => { + this._textBoxDoc[DocData][this._fieldKey] = str; + }} autoCommands="pi theta sqrt sum prod alpha beta gamma rho" autoOperatorNames="sin cos tan" - spaceBehavesLikeTab={true} + spaceBehavesLikeTab /> </div> ); } } + +export class EquationView { + dom: HTMLDivElement; // container for label and value + root: any; + tbox: FormattedTextBox; + view: any; + constructor(node: any, view: any, getPos: any, tbox: FormattedTextBox) { + this.tbox = tbox; + this.view = view; + this.dom = document.createElement('div'); + this.dom.style.width = node.attrs.width; + this.dom.style.height = node.attrs.height; + this.dom.style.position = 'relative'; + this.dom.style.display = 'inline-block'; + this.dom.onmousedown = function (e: any) { + e.stopPropagation(); + }; + + this.root = ReactDOM.createRoot(this.dom); + this.root.render(<EquationViewInternal fieldKey={node.attrs.fieldKey} width={node.attrs.width} height={node.attrs.height} getPos={getPos} setEditor={this.setEditor} tbox={tbox} />); + } + _editor: EquationEditor | undefined; + setEditor = (editor?: EquationEditor) => { + this._editor = editor; + }; + destroy() { + this.root.unmount(); + } + setSelection() { + this._editor?.mathField.focus(); + } + selectNode() { + this.tbox._applyingChange = this.tbox.fieldKey; // setting focus will make prosemirror lose focus, which will cause it to change its selection to a text selection, which causes this view to get rebuilt but it's no longer node selected, so the equationview won't have focus + setTimeout(() => { + this._editor?.mathField.focus(); + setTimeout(() => { + this.tbox._applyingChange = ''; + }); + }); + } + deselectNode() {} +} diff --git a/src/client/views/nodes/formattedText/FootnoteView.tsx b/src/client/views/nodes/formattedText/FootnoteView.tsx index b327e5137..4641da2e9 100644 --- a/src/client/views/nodes/formattedText/FootnoteView.tsx +++ b/src/client/views/nodes/formattedText/FootnoteView.tsx @@ -2,9 +2,9 @@ import { EditorView } from 'prosemirror-view'; import { EditorState } from 'prosemirror-state'; import { keymap } from 'prosemirror-keymap'; import { baseKeymap, toggleMark } from 'prosemirror-commands'; -import { schema } from './schema_rts'; import { redo, undo } from 'prosemirror-history'; import { StepMap } from 'prosemirror-transform'; +import { schema } from './schema_rts'; export class FootnoteView { innerView: any; @@ -100,8 +100,8 @@ export class FootnoteView { this.innerView.updateState(state); if (!tr.getMeta('fromOutside')) { - const outerTr = this.outerView.state.tr, - offsetMap = StepMap.offset(this.getPos() + 1); + const outerTr = this.outerView.state.tr; + const offsetMap = StepMap.offset(this.getPos() + 1); for (const transaction of transactions) { for (const step of transaction.steps) { outerTr.step(step.map(offsetMap)); @@ -115,7 +115,7 @@ export class FootnoteView { if (!node.sameMarkup(this.node)) return false; this.node = node; if (this.innerView) { - const state = this.innerView.state; + const { state } = this.innerView; const start = node.content.findDiffStart(state.doc.content); if (start !== null) { let { a: endA, b: endB } = node.content.findDiffEnd(state.doc.content); diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.scss b/src/client/views/nodes/formattedText/FormattedTextBox.scss index 38dd2e847..99b4a84fc 100644 --- a/src/client/views/nodes/formattedText/FormattedTextBox.scss +++ b/src/client/views/nodes/formattedText/FormattedTextBox.scss @@ -349,8 +349,9 @@ footnote::before { touch-action: none; span { font-family: inherit; - background-color: inherit; - display: contents; // fixes problem where extra space is added around <ol> lists when inside a prosemirror span + // background-color: inherit; // intended to allow texts to inherit background from list container, but this prevents css highlights e.,g highlight text from others + display: inline; // needs to be inline for search highlighting to appear + // display: contents; // BUT needs to be 'contents' to avoid Chrome bug where extra space is added above and <ol> lists when inside a prosemirror span } blockquote { diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx index 43010b2ed..321fdbb91 100644 --- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx +++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx @@ -1,3 +1,4 @@ +/* eslint-disable jsx-a11y/no-static-element-interactions */ import { IconProp } from '@fortawesome/fontawesome-svg-core'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { Tooltip } from '@mui/material'; @@ -12,8 +13,9 @@ import { EditorState, NodeSelection, Plugin, Selection, TextSelection, Transacti import { EditorView } from 'prosemirror-view'; import * as React from 'react'; import { BsMarkdownFill } from 'react-icons/bs'; +import { addStyleSheet, addStyleSheetRule, clearStyleSheetRules, ClientUtils, DivWidth, returnFalse, returnZero, setupMoveUpEvents, smoothScroll, StopEvent } from '../../../../ClientUtils'; import { DateField } from '../../../../fields/DateField'; -import { Doc, DocListCast, Field, Opt, StrListCast } from '../../../../fields/Doc'; +import { CreateLinkToActiveAudio, Doc, DocListCast, Field, FieldType, Opt, StrListCast } from '../../../../fields/Doc'; import { AclAdmin, AclAugment, AclEdit, AclSelfEdit, DocCss, DocData, ForceServerWrite, UpdatingFromServer } from '../../../../fields/DocSymbols'; import { Id } from '../../../../fields/FieldSymbols'; import { InkTool } from '../../../../fields/InkField'; @@ -23,37 +25,39 @@ import { RichTextField } from '../../../../fields/RichTextField'; import { ComputedField } from '../../../../fields/ScriptField'; import { BoolCast, Cast, DateCast, DocCast, FieldValue, NumCast, RTFCast, ScriptCast, StrCast } from '../../../../fields/Types'; import { GetEffectiveAcl, TraceMobx } from '../../../../fields/util'; -import { addStyleSheet, addStyleSheetRule, clearStyleSheetRules, DivWidth, emptyFunction, numberRange, returnFalse, returnZero, setupMoveUpEvents, smoothScroll, unimplementedFunction, Utils } from '../../../../Utils'; +import { emptyFunction, numberRange, unimplementedFunction, Utils } from '../../../../Utils'; import { gptAPICall, GPTCallType } from '../../../apis/gpt/GPT'; import { DocServer } from '../../../DocServer'; -import { Docs, DocUtils } from '../../../documents/Documents'; -import { CollectionViewType } from '../../../documents/DocumentTypes'; +import { Docs } from '../../../documents/Documents'; +import { CollectionViewType, DocumentType } from '../../../documents/DocumentTypes'; +import { DocUtils } from '../../../documents/DocUtils'; import { DictationManager } from '../../../util/DictationManager'; -import { DocumentManager } from '../../../util/DocumentManager'; -import { DragManager, dropActionType } from '../../../util/DragManager'; +import { DragManager } from '../../../util/DragManager'; +import { dropActionType } from '../../../util/DropActionTypes'; import { MakeTemplate } from '../../../util/DropConverter'; import { LinkManager } from '../../../util/LinkManager'; import { RTFMarkup } from '../../../util/RTFMarkup'; -import { SelectionManager } from '../../../util/SelectionManager'; import { SnappingManager } from '../../../util/SnappingManager'; import { undoable, undoBatch, UndoManager } from '../../../util/UndoManager'; -import { CollectionFreeFormView } from '../../collections/collectionFreeForm/CollectionFreeFormView'; import { CollectionStackingView } from '../../collections/CollectionStackingView'; import { CollectionTreeView } from '../../collections/CollectionTreeView'; import { ContextMenu } from '../../ContextMenu'; import { ContextMenuProps } from '../../ContextMenuItem'; -import { ViewBoxAnnotatableComponent, ViewBoxInterface } from '../../DocComponent'; +import { ViewBoxAnnotatableComponent } from '../../DocComponent'; import { Colors } from '../../global/globalEnums'; import { LightboxView } from '../../LightboxView'; import { AnchorMenu } from '../../pdf/AnchorMenu'; import { GPTPopup } from '../../pdf/GPTPopup/GPTPopup'; +import { PinDocView, PinProps } from '../../PinFuncs'; import { SidebarAnnos } from '../../SidebarAnnos'; -import { StyleProp } from '../../StyleProvider'; -import { media_state } from '../AudioBox'; -import { DocumentView, DocumentViewInternal, OpenWhere } from '../DocumentView'; -import { FieldView, FieldViewProps, FocusViewOptions } from '../FieldView'; +import { StyleProp } from '../../StyleProp'; +import { styleFromLayoutString } from '../../StyleProvider'; +import { mediaState } from '../AudioBox'; +import { DocumentView } from '../DocumentView'; +import { FieldView, FieldViewProps } from '../FieldView'; +import { FocusViewOptions } from '../FocusViewOptions'; import { LinkInfo } from '../LinkDocPreview'; -import { PinProps, PresBox } from '../trails'; +import { OpenWhere } from '../OpenWhere'; import { DashDocCommentView } from './DashDocCommentView'; import { DashDocView } from './DashDocView'; import { DashFieldView } from './DashFieldView'; @@ -69,16 +73,17 @@ import { schema } from './schema_rts'; import { SummaryView } from './SummaryView'; // import * as applyDevTools from 'prosemirror-dev-tools'; -interface FormattedTextBoxProps extends FieldViewProps { +export interface FormattedTextBoxProps extends FieldViewProps { onBlur?: () => void; // callback when text loses focus autoFocus?: boolean; // whether text should get input focus when created } @observer -export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextBoxProps>() implements ViewBoxInterface { +export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextBoxProps>() { public static LayoutString(fieldStr: string) { return FieldView.LayoutString(FormattedTextBox, fieldStr); } public static blankState = () => EditorState.create(FormattedTextBox.Instance.config); + // eslint-disable-next-line no-use-before-define public static Instance: FormattedTextBox; public static LiveTextUndo: UndoManager.Batch | undefined; static _globalHighlightsCache: string = ''; @@ -97,7 +102,6 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB private _inDrop = false; private _finishingLink = false; private _searchIndex = 0; - private _lastTimedMark: Mark | undefined = undefined; private _cachedLinks: Doc[] = []; private _undoTyping?: UndoManager.Batch; private _disposers: { [name: string]: IReactionDisposer } = {}; @@ -108,10 +112,6 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB private _keymap: any = undefined; private _rules: RichTextRules | undefined; private _forceUncollapse = true; // if the cursor doesn't move between clicks, then the selection will disappear for some reason. This flags the 2nd click as happening on a selection which allows bullet points to toggle - private _forceDownNode: Node | undefined; - private _downX = 0; - private _downY = 0; - private _downTime = 0; private _break = true; public ProseRef?: HTMLDivElement; public get EditorView() { @@ -152,10 +152,10 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB return this.titleHeight + NumCast(this.layoutDoc._layout_autoHeightMargins); } @computed get _recordingDictation() { - return this.dataDoc?.mediaState === media_state.Recording; + return this.dataDoc?.mediaState === mediaState.Recording; } set _recordingDictation(value) { - !this.dataDoc[`${this.fieldKey}_recordingSource`] && (this.dataDoc.mediaState = value ? media_state.Recording : undefined); + !this.dataDoc[`${this.fieldKey}_recordingSource`] && (this.dataDoc.mediaState = value ? mediaState.Recording : undefined); } @computed get config() { this._keymap = buildKeymap(schema, this._props); @@ -170,8 +170,8 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB keymap(baseKeymap), new Plugin({ props: { attributes: { class: 'ProseMirror-example-setup-style' } } }), new Plugin({ - view(editorView) { - return new FormattedTextBoxComment(editorView); + view(/* editorView */) { + return new FormattedTextBoxComment(); }, }), ], @@ -183,26 +183,8 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB private gptRes: string = ''; public static PasteOnLoad: ClipboardEvent | undefined; - private static SelectOnLoad: Doc | undefined; - public static SetSelectOnLoad(doc: Doc) { - FormattedTextBox.SelectOnLoad = doc; - } public static DontSelectInitialText = false; // whether initial text should be selected or not public static SelectOnLoadChar = ''; - public static IsFragment(html: string) { - return html.indexOf('data-pm-slice') !== -1; - } - public static GetHref(html: string): string { - const parser = new DOMParser(); - const parsedHtml = parser.parseFromString(html, 'text/html'); - if (parsedHtml.body.childNodes.length === 1 && parsedHtml.body.childNodes[0].childNodes.length === 1 && (parsedHtml.body.childNodes[0].childNodes[0] as any).href) { - return (parsedHtml.body.childNodes[0].childNodes[0] as any).href; - } - return ''; - } - public static GetDocFromUrl(url: string) { - return url.startsWith(document.location.origin) ? new URL(url).pathname.split('doc/').lastElement() : ''; // docId - } constructor(props: FormattedTextBoxProps) { super(props); @@ -217,13 +199,13 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB public RemoveLinkFromDoc(linkDoc?: Doc) { this.unhighlightSearchTerms(); const state = this._editorView?.state; - const a1 = linkDoc?.link_anchor_1 as Doc; - const a2 = linkDoc?.link_anchor_2 as Doc; + const a1 = DocCast(linkDoc?.link_anchor_1); + const a2 = DocCast(linkDoc?.link_anchor_2); if (state && a1 && a2 && this._editorView) { this.removeDocument(a1); this.removeDocument(a2); - var allFoundLinkAnchors: any[] = []; - state.doc.nodesBetween(0, state.doc.nodeSize - 2, (node: any, pos: number, parent: any) => { + let allFoundLinkAnchors: any[] = []; + state.doc.nodesBetween(0, state.doc.nodeSize - 2, (node: any /* , pos: number, parent: any */) => { const foundLinkAnchors = findLinkMark(node.marks)?.attrs.allAnchors.filter((a: any) => a.anchorId === a1[Id] || a.anchorId === a2[Id]) || []; allFoundLinkAnchors = foundLinkAnchors.length ? foundLinkAnchors : allFoundLinkAnchors; return true; @@ -246,14 +228,14 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB } getAnchor = (addAsAnnotation: boolean, pinProps?: PinProps) => { - const rootDoc = Doc.isTemplateDoc(this._props.docViewPath().lastElement()?.Document) ? this.Document : DocCast(this.Document.rootDocument, this.Document); + const rootDoc: Doc = Doc.isTemplateDoc(this._props.docViewPath().lastElement()?.Document) ? this.Document : DocCast(this.Document.rootDocument, this.Document); if (!pinProps && this._editorView?.state.selection.empty) return rootDoc; const anchor = Docs.Create.ConfigDocument({ title: StrCast(rootDoc.title), annotationOn: rootDoc }); this.addDocument(anchor); this._finishingLink = true; this.makeLinkAnchor(anchor, OpenWhere.addRight, undefined, 'Anchored Selection', false, addAsAnnotation); this._finishingLink = false; - PresBox.pinDocView(anchor, { pinDocLayout: pinProps?.pinDocLayout, pinData: { ...(pinProps?.pinData ?? {}), scrollable: true } }, this.Document); + PinDocView(anchor, { pinDocLayout: pinProps?.pinDocLayout, pinData: { ...(pinProps?.pinData ?? {}), scrollable: true } }, this.Document); return anchor; }; @@ -261,11 +243,11 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB setupAnchorMenu = () => { AnchorMenu.Instance.Status = 'marquee'; - AnchorMenu.Instance.OnClick = (e: PointerEvent) => { + AnchorMenu.Instance.OnClick = () => { !this.layoutDoc.layout_showSidebar && this.toggleSidebar(); setTimeout(() => this._sidebarRef.current?.anchorMenuClick(this.makeLinkAnchor(undefined, OpenWhere.addRight, undefined, 'Anchored Selection', true))); // give time for sidebarRef to be created }; - AnchorMenu.Instance.OnAudio = (e: PointerEvent) => { + AnchorMenu.Instance.OnAudio = () => { !this.layoutDoc.layout_showSidebar && this.toggleSidebar(); const anchor = this.makeLinkAnchor(undefined, OpenWhere.addRight, undefined, 'Anchored Selection', true, true); @@ -275,8 +257,9 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB anchor.followLinkAudio = true; let stopFunc: any; const targetData = target[DocData]; - targetData.mediaState = media_state.Recording; - DocumentViewInternal.recordAudioAnnotation(targetData, Doc.LayoutFieldKey(target), stop => (stopFunc = stop)); + targetData.mediaState = mediaState.Recording; + DictationManager.recordAudioAnnotation(targetData, Doc.LayoutFieldKey(target), stop => { stopFunc = stop }); // prettier-ignore + const reactionDisposer = reaction( () => target.mediaState, dictation => { @@ -286,12 +269,12 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB } } ); - target.title = ComputedField.MakeFunction(`self["text_audioAnnotations_text"].lastElement()`); + target.title = ComputedField.MakeFunction(`this.text_audioAnnotations_text.lastElement()`); } }); }; AnchorMenu.Instance.Highlight = undoable((color: string) => { - this._editorView?.state && RichTextMenu.Instance?.setHighlight(color); + this._editorView?.state && RichTextMenu.Instance?.setFontField(color, 'fontHighlight'); return undefined; }, 'highlght text'); AnchorMenu.Instance.onMakeAnchor = () => this.getAnchor(true); @@ -305,7 +288,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB e.stopPropagation(); const targetCreator = (annotationOn?: Doc) => { const target = DocUtils.GetNewTextDoc('Note linked to ' + this.Document.title, 0, 0, 100, 100, annotationOn); - FormattedTextBox.SetSelectOnLoad(target); + Doc.SetSelectOnLoad(target); return target; }; @@ -313,7 +296,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB }); const coordsB = this._editorView!.coordsAtPos(this._editorView!.state.selection.to); this._props.rootSelected?.() && AnchorMenu.Instance.jumpTo(coordsB.left, coordsB.bottom); - let ele: Opt<HTMLDivElement> = undefined; + let ele: Opt<HTMLDivElement>; try { const contents = window.getSelection()?.getRangeAt(0).cloneContents(); if (contents) { @@ -321,7 +304,9 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB ele.append(contents); } this._selectionHTML = ele?.innerHTML; - } catch (e) {} + } catch (e) { + /* empty */ + } }; leafText = (node: Node) => { @@ -330,13 +315,13 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB const fieldKey = StrCast(node.attrs.fieldKey); return ( (node.attrs.hideKey ? '' : fieldKey + ':') + // - (node.attrs.hideValue ? '' : Field.toJavascriptString(refDoc[fieldKey] as Field)) + (node.attrs.hideValue ? '' : Field.toJavascriptString(refDoc[fieldKey] as FieldType)) ); } return ''; }; dispatchTransaction = (tx: Transaction) => { - if (this._editorView && (this._editorView as any).docView) { + if (this._editorView) { const state = this._editorView.state.apply(tx); this._editorView.updateState(state); this.tryUpdateDoc(false); @@ -344,13 +329,12 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB }; tryUpdateDoc = (force: boolean) => { - if (this._editorView && (this._editorView as any).docView) { - const state = this._editorView.state; - const dataDoc = this.dataDoc; + if (this._editorView) { + const { state } = this._editorView; + const { dataDoc } = this; const newText = state.doc.textBetween(0, state.doc.content.size, ' \n', this.leafText); const newJson = JSON.stringify(state.toJSON()); const prevData = Cast(this.layoutDoc[this.fieldKey], RichTextField, null); // the actual text in the text box - const templateData = this.Document !== this.layoutDoc ? prevData : undefined; // the default text stored in a layout template const protoData = Cast(Cast(dataDoc.proto, Doc, null)?.[this.fieldKey], RichTextField, null); // the default text inherited from a prototype const layoutData = this.layoutDoc.isTemplateDoc ? Cast(this.layoutDoc[this.fieldKey], RichTextField, null) : undefined; // the default text inherited from a prototype const effectiveAcl = GetEffectiveAcl(dataDoc); @@ -359,7 +343,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB if ([AclEdit, AclAdmin, AclSelfEdit, AclAugment].includes(effectiveAcl)) { const accumTags = [] as string[]; - state.tr.doc.nodesBetween(0, state.doc.content.size, (node: any, pos: number, parent: any) => { + state.tr.doc.nodesBetween(0, state.doc.content.size, (node: any /* , pos: number, parent: any */) => { if (node.type === schema.nodes.dashField && node.attrs.fieldKey.startsWith('#')) { accumTags.push(node.attrs.fieldKey); } @@ -386,7 +370,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB } else { // if we've deleted all the text in a note driven by a template, then restore the template data dataDoc[this.fieldKey] = undefined; - this._editorView.updateState(EditorState.fromJSON(this.config, JSON.parse(((layoutData !== prevData ? layoutData : undefined) ?? protoData).Data))); + this._editorView.updateState(EditorState.fromJSON(this.config, JSON.parse(((layoutData !== prevData ? layoutData : undefined) ?? protoData)?.Data))); ScriptCast(this.layoutDoc.onTextChanged, null)?.script.run({ this: this.layoutDoc, text: newText }); unchanged = false; } @@ -414,37 +398,22 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB insertTime = () => { let linkTime; let linkAnchor; - let link; - LinkManager.Links(this.dataDoc).forEach((l, i) => { - const anchor = (l.link_anchor_1 as Doc).annotationOn ? (l.link_anchor_1 as Doc) : (l.link_anchor_2 as Doc).annotationOn ? (l.link_anchor_2 as Doc) : undefined; - if (anchor && (anchor.annotationOn as Doc).mediaState === media_state.Recording) { + Doc.Links(this.dataDoc).forEach(l => { + const anchor = DocCast(l.link_anchor_1)?.annotationOn ? DocCast(l.link_anchor_1) : DocCast(l.link_anchor_2)?.annotationOn ? DocCast(l.link_anchor_2) : undefined; + if (anchor && (anchor.annotationOn as Doc).mediaState === mediaState.Recording) { linkTime = NumCast(anchor._timecodeToShow /* audioStart */); linkAnchor = anchor; - link = l; } }); if (this._editorView && linkTime) { - const state = this._editorView.state; - const now = Date.now(); - let mark = schema.marks.user_mark.create({ userid: Doc.CurrentUserEmail, modified: Math.floor(now / 1000) }); - if (!this._break && state.selection.to !== state.selection.from) { - for (let i = state.selection.from; i <= state.selection.to; i++) { - const pos = state.doc.resolve(i); - const um = Array.from(pos.marks()).find(m => m.type === schema.marks.user_mark); - if (um) { - mark = um; - break; - } - } - } - - const path = (this._editorView.state.selection.$from as any).path; - if (linkAnchor && path[path.length - 3].type !== this._editorView.state.schema.nodes.code_block) { + const { state } = this._editorView; + const { path } = state.selection.$from as any; + if (linkAnchor && path[path.length - 3].type !== state.schema.nodes.code_block) { const time = linkTime + Date.now() / 1000 - this._recordingStart / 1000; this._break = false; - const from = state.selection.from; - const value = this._editorView.state.schema.nodes.audiotag.create({ timeCode: time, audioId: linkAnchor[Id] }); - const replaced = this._editorView.state.tr.insert(from - 1, value); + const { from } = state.selection; + const value = state.schema.nodes.audiotag.create({ timeCode: time, audioId: linkAnchor[Id] }); + const replaced = state.tr.insert(from - 1, value); this._editorView.dispatch(replaced.setSelection(new TextSelection(replaced.doc.resolve(from + 1)))); } } @@ -452,7 +421,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB autoLink = () => { const newAutoLinks = new Set<Doc>(); - const oldAutoLinks = LinkManager.Links(this.Document).filter( + const oldAutoLinks = Doc.Links(this.Document).filter( link => ((!Doc.isTemplateForField(this.Document) && (!Doc.isTemplateForField(DocCast(link.link_anchor_1)) || !Doc.AreProtosEqual(DocCast(link.link_anchor_1), this.Document)) && @@ -461,17 +430,17 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB link.link_relationship === LinkManager.AutoKeywords ); // prettier-ignore if (this._editorView?.state.doc.textContent) { - const f = this._editorView.state.selection.from; - - const t = this._editorView.state.selection.to; - var tr = this._editorView.state.tr as any; - const autoAnch = this._editorView.state.schema.marks.autoLinkAnchor; - tr = tr.removeMark(0, tr.doc.content.size, autoAnch); - Doc.MyPublishedDocs.filter(term => term.title).forEach(term => (tr = this.hyperlinkTerm(tr, term, newAutoLinks))); - tr = tr.setSelection(new TextSelection(tr.doc.resolve(f), tr.doc.resolve(t))); + let { tr } = this._editorView.state; + const { from, to } = this._editorView.state.selection; + const { autoLinkAnchor } = this._editorView.state.schema.marks; + tr = tr.removeMark(0, tr.doc.content.size, autoLinkAnchor); + Doc.MyPublishedDocs.filter(term => term.title).forEach(term => { + tr = this.hyperlinkTerm(tr, term, newAutoLinks); + }); + tr = tr.setSelection(new TextSelection(tr.doc.resolve(from), tr.doc.resolve(to))); this._editorView?.dispatch(tr); } - oldAutoLinks.filter(oldLink => !newAutoLinks.has(oldLink) && oldLink.link_anchor_2 !== this.Document).forEach(LinkManager.Instance.deleteLink); + oldAutoLinks.filter(oldLink => !newAutoLinks.has(oldLink) && oldLink.link_anchor_2 !== this.Document).forEach(doc => Doc.DeleteLink?.(doc)); }; updateTitle = () => { @@ -504,11 +473,12 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB * function of a freeform view that is driven by the text box's text. The include directive will copy the code of the published * document into the code being evaluated. */ - hyperlinkTerm = (tr: any, target: Doc, newAutoLinks: Set<Doc>) => { + hyperlinkTerm = (trIn: any, target: Doc, newAutoLinks: Set<Doc>) => { + let tr = trIn; const editorView = this._editorView; - if (editorView && (editorView as any).docView && !Doc.AreProtosEqual(target, this.Document)) { - const autoLinkTerm = Field.toString(target.title as Field).replace(/^@/, ''); - var alink: Doc | undefined; + if (editorView && !Doc.AreProtosEqual(target, this.Document)) { + const autoLinkTerm = Field.toString(target.title as FieldType).replace(/^@/, ''); + let alink: Doc | undefined; this.findInNode(editorView, editorView.state.doc, autoLinkTerm).forEach(sel => { if ( !sel.$anchor.pos || @@ -520,11 +490,11 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB ) { const splitter = editorView.state.schema.marks.splitter.create({ id: Utils.GenerateGuid() }); tr = tr.addMark(sel.from, sel.to, splitter); - tr.doc.nodesBetween(sel.from, sel.to, (node: any, pos: number, parent: any) => { + tr.doc.nodesBetween(sel.from, sel.to, (node: any, pos: number /* , parent: any */) => { if (node.firstChild === null && !node.marks.find((m: Mark) => m.type.name === schema.marks.noAutoLinkAnchor.name) && node.marks.find((m: Mark) => m.type.name === schema.marks.splitter.name)) { alink = alink ?? - (LinkManager.Links(this.Document).find( + (Doc.Links(this.Document).find( link => Doc.AreProtosEqual(Cast(link.link_anchor_1, Doc, null), this.Document) && // Doc.AreProtosEqual(Cast(link.link_anchor_2, Doc, null), target) @@ -551,12 +521,14 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB return true; }; highlightSearchTerms = (terms: string[], backward: boolean) => { - if (this._editorView && (this._editorView as any).docView && terms.some(t => t)) { - const mark = this._editorView.state.schema.mark(this._editorView.state.schema.marks.search_highlight); - const activeMark = this._editorView.state.schema.mark(this._editorView.state.schema.marks.search_highlight, { selected: true }); - const res = terms.filter(t => t).map(term => this.findInNode(this._editorView!, this._editorView!.state.doc, term)); - const length = res[0].length; - let tr = this._editorView.state.tr; + const { _editorView } = this; + if (_editorView && terms.some(t => t)) { + const { state } = _editorView; + let { tr } = state; + const mark = state.schema.mark(state.schema.marks.search_highlight); + const activeMark = state.schema.mark(state.schema.marks.search_highlight, { selected: true }); + const res = terms.filter(t => t).map(term => this.findInNode(_editorView, state.doc, term)); + const { length } = res[0]; const flattened: TextSelection[] = []; res.map(r => r.map(h => flattened.push(h))); this._searchIndex = ++this._searchIndex > flattened.length - 1 ? 0 : this._searchIndex; @@ -571,23 +543,27 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB } const lastSel = Math.min(flattened.length - 1, this._searchIndex); - flattened.forEach((h: TextSelection, ind: number) => (tr = tr.addMark(h.from, h.to, ind === lastSel ? activeMark : mark))); - flattened[lastSel] && this._editorView.dispatch(tr.setSelection(new TextSelection(tr.doc.resolve(flattened[lastSel].from), tr.doc.resolve(flattened[lastSel].to))).scrollIntoView()); + flattened.forEach((h: TextSelection, ind: number) => { + tr = tr.addMark(h.from, h.to, ind === lastSel ? activeMark : mark); + }); + flattened[lastSel] && _editorView.dispatch(tr.setSelection(new TextSelection(tr.doc.resolve(flattened[lastSel].from), tr.doc.resolve(flattened[lastSel].to))).scrollIntoView()); } }; unhighlightSearchTerms = () => { - if (window.screen.width < 600) null; - else if (this._editorView && (this._editorView as any).docView) { - const mark = this._editorView.state.schema.mark(this._editorView.state.schema.marks.search_highlight); - const activeMark = this._editorView.state.schema.mark(this._editorView.state.schema.marks.search_highlight, { selected: true }); - const end = this._editorView.state.doc.nodeSize - 2; - this._editorView.dispatch(this._editorView.state.tr.removeMark(0, end, mark).removeMark(0, end, activeMark)); + if (this._editorView) { + const { state } = this._editorView; + if (state) { + const mark = state.schema.mark(state.schema.marks.search_highlight); + const activeMark = state.schema.mark(state.schema.marks.search_highlight, { selected: true }); + const end = state.doc.nodeSize - 2; + this._editorView.dispatch(state.tr.removeMark(0, end, mark).removeMark(0, end, activeMark)); + } } }; adoptAnnotation = (start: number, end: number, mark: Mark) => { const view = this._editorView!; - const nmark = view.state.schema.marks.user_mark.create({ ...mark.attrs, userid: Doc.CurrentUserEmail }); + const nmark = view.state.schema.marks.user_mark.create({ ...mark.attrs, userid: ClientUtils.CurrentUserEmail() }); view.dispatch(view.state.tr.removeMark(start, end, nmark).addMark(start, end, nmark)); }; protected createDropTarget = (ele: HTMLDivElement) => { @@ -631,7 +607,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB float: 'unset', }); if (!de.embedKey && ![dropActionType.embed, dropActionType.copy].includes(dropAction ?? dropActionType.move)) { - added = dragData.removeDocument?.(draggedDoc) ? true : false; + added = !!dragData.removeDocument?.(draggedDoc); } else { added = true; } @@ -643,9 +619,9 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB this._inDrop = true; const pos = view.posAtCoords({ left: de.x, top: de.y })?.pos; pos && view.dispatch(view.state.tr.insert(pos, node)); - added = pos ? true : false; // pos will be null if you don't drop onto an actual text location - } catch (e) { - console.log('Drop failed', e); + added = !!pos; // pos will be null if you don't drop onto an actual text location + } catch (err) { + console.log('Drop failed', err); added = false; } finally { this._inDrop = false; @@ -677,29 +653,28 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB } offset += (context.content as any).content[i].nodeSize; } - return null; - } else { - return null; } + return null; } - //Recursively finds matches within a given node + // Recursively finds matches within a given node findInNode(pm: EditorView, node: Node, find: string) { let ret: TextSelection[] = []; if (node.isTextblock) { - let index = 0, - foundAt; + let index = 0; + let foundAt; const ep = this.getNodeEndpoints(pm.state.doc, node); const regexp = new RegExp(find, 'i'); if (regexp) { - var blockOffset = 0; - for (var i = 0; i < node.childCount; i++) { - var textContent = ''; + let blockOffset = 0; + for (let i = 0; i < node.childCount; i++) { + let textContent = ''; while (i < node.childCount && node.child(i).type === pm.state.schema.nodes.text) { textContent += node.child(i).textContent; i++; } + // eslint-disable-next-line no-cond-assign while (ep && (foundAt = textContent.slice(index).search(regexp)) > -1) { const sel = new TextSelection(pm.state.doc.resolve(ep.from + index + blockOffset + foundAt + 1), pm.state.doc.resolve(ep.from + index + blockOffset + foundAt + find.length + 1)); ret.push(sel); @@ -710,14 +685,18 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB } } } else { - node.content.forEach((child, i) => (ret = ret.concat(this.findInNode(pm, child, find)))); + node.content.forEach(child => { + ret = ret.concat(this.findInNode(pm, child, find)); + }); } return ret; } updateHighlights = (highlights: string[]) => { if (Array.from(highlights).join('') === FormattedTextBox._globalHighlightsCache) return; - setTimeout(() => (FormattedTextBox._globalHighlightsCache = Array.from(highlights).join(''))); + setTimeout(() => { + FormattedTextBox._globalHighlightsCache = Array.from(highlights).join(''); + }); clearStyleSheetRules(FormattedTextBox._userStyleSheet); if (!highlights.includes('Audio Tags')) { addStyleSheetRule(FormattedTextBox._userStyleSheet, 'audiotag', { display: 'none' }, ''); @@ -726,7 +705,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB addStyleSheetRule(FormattedTextBox._userStyleSheet, 'UM-remote', { background: 'yellow' }); } if (highlights.includes('My Text')) { - addStyleSheetRule(FormattedTextBox._userStyleSheet, 'UM-' + Doc.CurrentUserEmail.replace(/\./g, '').replace(/@/g, ''), { background: 'moccasin' }); + addStyleSheetRule(FormattedTextBox._userStyleSheet, 'UM-' + ClientUtils.CurrentUserEmail().replace(/\./g, '').replace(/@/g, ''), { background: 'moccasin' }); } if (highlights.includes('Todo Items')) { addStyleSheetRule(FormattedTextBox._userStyleSheet, 'UT-todo', { outline: 'black solid 1px' }); @@ -745,21 +724,22 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB addStyleSheetRule(FormattedTextBox._userStyleSheet, 'UT-ignore', { 'font-size': '1' }); } if (highlights.includes('By Recent Minute')) { - addStyleSheetRule(FormattedTextBox._userStyleSheet, 'UM-' + Doc.CurrentUserEmail.replace('.', '').replace('@', ''), { opacity: '0.1' }); + addStyleSheetRule(FormattedTextBox._userStyleSheet, 'UM-' + ClientUtils.CurrentUserEmail().replace('.', '').replace('@', ''), { opacity: '0.1' }); const min = Math.round(Date.now() / 1000 / 60); numberRange(10).map(i => addStyleSheetRule(FormattedTextBox._userStyleSheet, 'UM-min-' + (min - i), { opacity: ((10 - i - 1) / 10).toString() })); } if (highlights.includes('By Recent Hour')) { - addStyleSheetRule(FormattedTextBox._userStyleSheet, 'UM-' + Doc.CurrentUserEmail.replace('.', '').replace('@', ''), { opacity: '0.1' }); + addStyleSheetRule(FormattedTextBox._userStyleSheet, 'UM-' + ClientUtils.CurrentUserEmail().replace('.', '').replace('@', ''), { opacity: '0.1' }); const hr = Math.round(Date.now() / 1000 / 60 / 60); numberRange(10).map(i => addStyleSheetRule(FormattedTextBox._userStyleSheet, 'UM-hr-' + (hr - i), { opacity: ((10 - i - 1) / 10).toString() })); } + // eslint-disable-next-line operator-assignment this.layoutDoc[DocCss] = this.layoutDoc[DocCss] + 1; // css changes happen outside of react/mobx. so we need to set a flag that will notify anyone interested in layout changes triggered by css changes (eg., CollectionLinkView) }; @observable _showSidebar = false; @computed get SidebarShown() { - return this._showSidebar || this.layoutDoc._layout_showSidebar ? true : false; + return !!(this._showSidebar || this.layoutDoc._layout_showSidebar); } @action @@ -781,7 +761,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB this, e, this.sidebarMove, - (e, movement, isClick) => !isClick && batch.end(), + (moveEv, movement, isClick) => !isClick && batch.end(), () => { this.toggleSidebar(); batch.end(); @@ -805,7 +785,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB deleteAnnotation = (anchor: Doc) => { const batch = UndoManager.StartBatch('delete link'); - LinkManager.Instance.deleteLink(LinkManager.Links(anchor)[0]); + Doc.DeleteLink?.(Doc.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)); // AnchorMenu.Instance.fadeOut(true); @@ -817,7 +797,9 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB pinToPres = (anchor: Doc) => this._props.pinToPres(anchor, {}); @undoBatch - makeTargetToggle = (anchor: Doc) => (anchor.followLinkToggle = !anchor.followLinkToggle); + makeTargetToggle = (anchor: Doc) => { + anchor.followLinkToggle = !anchor.followLinkToggle; + }; @undoBatch showTargetTrail = (anchor: Doc) => { @@ -833,11 +815,10 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB specificContextMenu = (e: React.MouseEvent): void => { const cm = ContextMenu.Instance; - const editor = this._editorView!; - const pcords = editor.posAtCoords({ left: e.clientX, top: e.clientY }); let target = e.target as any; // hrefs are stored on the database of the <a> node that wraps the hyerlink <span> while (target && !target.dataset?.targethrefs) target = target.parentElement; - if (target && !(e.nativeEvent as any).dash) { + const editor = this._editorView; + if (editor && target && !(e.nativeEvent as any).dash) { const hrefs = (target.dataset?.targethrefs as string) ?.trim() .split(' ') @@ -847,14 +828,14 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB .replace(Doc.localServerPath(), '') .split('?')[0]; const deleteMarkups = undoBatch(() => { - const sel = editor.state.selection; - editor.dispatch(editor.state.tr.removeMark(sel.from, sel.to, editor.state.schema.marks.linkAnchor)); + const { selection } = editor.state; + editor.dispatch(editor.state.tr.removeMark(selection.from, selection.to, editor.state.schema.marks.linkAnchor)); }); e.persist(); anchorDoc && DocServer.GetRefField(anchorDoc).then( action(anchor => { - anchor && SelectionManager.SelectSchemaViewDoc(anchor as Doc); + anchor && DocumentView.SelectSchemaDoc(anchor as Doc); AnchorMenu.Instance.Status = 'annotation'; AnchorMenu.Instance.Delete = !anchor && editor.state.selection.empty ? returnFalse : !anchor ? deleteMarkups : () => this.deleteAnnotation(anchor as Doc); AnchorMenu.Instance.Pinned = false; @@ -884,7 +865,9 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB event: undoBatch(() => { this.dataDoc.layout_meta = Cast(Doc.UserDoc().emptyHeader, Doc, null)?.layout; this.Document.layout_fieldKey = 'layout_meta'; - setTimeout(() => (this.layoutDoc._header_height = this.layoutDoc._layout_autoHeightMargins = 50), 50); + setTimeout(() => { + this.layoutDoc._header_height = this.layoutDoc._layout_autoHeightMargins = 50; + }, 50); }), icon: 'eye', }); @@ -923,19 +906,26 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB appearanceItems.push({ description: !this.Document._layout_noSidebar ? 'Hide Sidebar Handle' : 'Show Sidebar Handle', - event: () => (this.layoutDoc._layout_noSidebar = !this.layoutDoc._layout_noSidebar), + event: () => { + this.layoutDoc._layout_noSidebar = !this.layoutDoc._layout_noSidebar; + }, icon: !this.Document._layout_noSidebar ? 'eye-slash' : 'eye', }); appearanceItems.push({ description: (this.Document._layout_enableAltContentUI ? 'Hide' : 'Show') + ' Alt Content UI', - event: () => (this.layoutDoc._layout_enableAltContentUI = !this.layoutDoc._layout_enableAltContentUI), + event: () => { + this.layoutDoc._layout_enableAltContentUI = !this.layoutDoc._layout_enableAltContentUI; + }, icon: !this.Document._layout_enableAltContentUI ? 'eye-slash' : 'eye', }); !Doc.noviceMode && appearanceItems.push({ description: 'Show Highlights...', noexpand: true, subitems: highlighting, icon: 'hand-point-right' }); !Doc.noviceMode && appearanceItems.push({ description: 'Broadcast Message', - event: () => DocServer.GetRefField('rtfProto').then(proto => proto instanceof Doc && (proto.BROADCAST_MESSAGE = Cast(this.dataDoc[this.fieldKey], RichTextField)?.Text)), + event: () => + DocServer.GetRefField('rtfProto').then(proto => { + proto instanceof Doc && (proto.BROADCAST_MESSAGE = Cast(this.dataDoc[this.fieldKey], RichTextField)?.Text); + }), icon: 'expand-arrows-alt', }); @@ -957,27 +947,36 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB const options = cm.findByDescription('Options...'); const optionItems = options && 'subitems' in options ? options.subitems : []; - optionItems.push({ description: `Toggle auto update from template`, event: () => (this.dataDoc[this.fieldKey + '_autoUpdate'] = !this.dataDoc[this.fieldKey + '_autoUpdate']), icon: 'star' }); + optionItems.push({ + description: `Toggle auto update from template`, + event: () => { + this.dataDoc[this.fieldKey + '_autoUpdate'] = !this.dataDoc[this.fieldKey + '_autoUpdate']; + }, + icon: 'star', + }); 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 && optionItems.push({ description: !this.Document._createDocOnCR ? 'Create New Doc on Carriage Return' : 'Allow Carriage Returns', - event: () => (this.layoutDoc._createDocOnCR = !this.layoutDoc._createDocOnCR), + event: () => { + this.layoutDoc._createDocOnCR = !this.layoutDoc._createDocOnCR; + }, icon: !this.Document._createDocOnCR ? 'grip-lines' : 'bars', }); !Doc.noviceMode && optionItems.push({ description: `${this.Document._layout_autoHeight ? 'Lock' : 'Auto'} Height`, - event: () => (this.layoutDoc._layout_autoHeight = !this.layoutDoc._layout_autoHeight), + event: () => { + this.layoutDoc._layout_autoHeight = !this.layoutDoc._layout_autoHeight; + }, icon: this.Document._layout_autoHeight ? 'lock' : 'unlock', }); !options && cm.addItem({ description: 'Options...', subitems: optionItems, icon: 'eye' }); const help = cm.findByDescription('Help...'); const helpItems = help && 'subitems' in help ? help.subitems : []; - helpItems.push({ description: `show markdown options`, event: RTFMarkup.Instance.open, icon: <BsMarkdownFill /> }); + helpItems.push({ description: `show markdown options`, event: () => RTFMarkup.Instance.setOpen(true), icon: <BsMarkdownFill /> }); !help && cm.addItem({ description: 'Help...', subitems: helpItems, icon: 'eye' }); - this._downX = this._downY = Number.NaN; }; animateRes = (resIndex: number, newText: string) => { @@ -990,7 +989,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB askGPT = action(async () => { try { - let res = await gptAPICall((this.dataDoc.text as RichTextField)?.Text, GPTCallType.COMPLETION); + const res = await gptAPICall((this.dataDoc.text as RichTextField)?.Text, GPTCallType.COMPLETION); if (!res) { this.animateRes(0, 'Something went wrong.'); } else if (this._editorView) { @@ -1016,10 +1015,10 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB breakupDictation = () => { if (this._editorView && this._recordingDictation) { - this.stopDictation(true); + this.stopDictation(/* true */); this._break = true; - const state = this._editorView.state; - const to = state.selection.to; + const { state } = this._editorView; + const { to } = state.selection; const updated = TextSelection.create(state.doc, to, to); this._editorView.dispatch(state.tr.setSelection(updated).insert(to, state.schema.nodes.paragraph.create({}))); if (this._recordingDictation) { @@ -1037,7 +1036,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB } }); }; - stopDictation = (abort: boolean) => DictationManager.Controls.stop(!abort); + stopDictation = (/* abort: boolean */) => DictationManager.Controls.stop(/* !abort */); setDictationContent = (value: string) => { if (this._editorView && this._recordingStart) { @@ -1046,7 +1045,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB const tanch = Docs.Create.ConfigDocument({ title: 'dictation anchor' }); return this.addDocument(tanch) ? tanch : undefined; }; - const link = DocUtils.MakeLinkToActiveAudio(textanchorFunc, false).lastElement(); + const link = CreateLinkToActiveAudio(textanchorFunc, false).lastElement(); if (link) { link[DocData].isDictation = true; const audioanchor = Cast(link.link_anchor_2, Doc, null); @@ -1065,7 +1064,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB } } } - const from = this._editorView.state.selection.from; + const { from } = this._editorView.state.selection; this._break = false; const tr = this._editorView.state.tr.insertText(value); this._editorView.dispatch(tr.setSelection(TextSelection.create(tr.doc, from, tr.doc.content.size)).scrollIntoView()); @@ -1074,23 +1073,24 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB // TODO: nda -- Look at how link anchors are added makeLinkAnchor(anchorDoc?: Doc, location?: string, targetHref?: string, title?: string, noPreview?: boolean, addAsAnnotation?: boolean) { - const state = this._editorView?.state; - if (state) { + const { _editorView } = this; + if (_editorView) { + const { state } = _editorView; let selectedText = ''; - const sel = state.selection; + const { selection } = state; const splitter = state.schema.marks.splitter.create({ id: Utils.GenerateGuid() }); - let tr = state.tr.addMark(sel.from, sel.to, splitter); - if (sel.from !== sel.to) { + let tr = state.tr.addMark(selection.from, selection.to, splitter); + if (selection.from !== selection.to) { const anchor = anchorDoc ?? Docs.Create.ConfigDocument({ // - title: 'text(' + this._editorView?.state.doc.textBetween(sel.from, sel.to) + ')', + title: 'text(' + state.doc.textBetween(selection.from, selection.to) + ')', annotationOn: this.dataDoc, }); const href = targetHref ?? Doc.localServerPath(anchor); if (anchor !== anchorDoc && addAsAnnotation) this.addDocument(anchor); - tr.doc.nodesBetween(sel.from, sel.to, (node: any, pos: number, parent: any) => { + tr.doc.nodesBetween(selection.from, selection.to, (node: any, pos: number /* , parent: any */) => { if (node.firstChild === null && node.marks.find((m: Mark) => m.type.name === schema.marks.splitter.name)) { const allAnchors = [{ href, title, anchorId: anchor[Id] }]; allAnchors.push(...(node.marks.find((m: Mark) => m.type.name === schema.marks.linkAnchor.name)?.attrs.allAnchors ?? [])); @@ -1100,7 +1100,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB } }); this.dataDoc[ForceServerWrite] = this.dataDoc[UpdatingFromServer] = true; // need to allow permissions for adding links to readonly/augment only documents - this._editorView!.dispatch(tr.removeMark(sel.from, sel.to, splitter)); + this._editorView!.dispatch(tr.removeMark(selection.from, selection.to, splitter)); this.dataDoc[UpdatingFromServer] = this.dataDoc[ForceServerWrite] = false; anchor.text = selectedText; anchor.text_html = this._selectionHTML ?? selectedText; @@ -1121,15 +1121,19 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB } setTimeout(() => this._sidebarRef?.current?.makeDocUnfiltered(doc)); } - return new Promise<Opt<DocumentView>>(res => DocumentManager.Instance.AddViewRenderedCb(doc, dv => res(dv))); + return new Promise<Opt<DocumentView>>(res => { + DocumentView.addViewRenderedCb(doc, dv => res(dv)); + }); }; focus = (textAnchor: Doc, options: FocusViewOptions) => { const focusSpeed = options.zoomTime ?? 500; const textAnchorId = textAnchor[Id]; + let start = 0; const findAnchorFrag = (frag: Fragment, editor: EditorView) => { const nodes: Node[] = []; let hadStart = start !== 0; frag.forEach((node, index) => { + // eslint-disable-next-line no-use-before-define const examinedNode = findAnchorNode(node, editor); if (examinedNode?.node && (examinedNode.node.textContent || examinedNode.node.type === this._editorView?.state.schema.nodes.dashDoc || examinedNode.node.type === this._editorView?.state.schema.nodes.audiotag)) { nodes.push(examinedNode.node); @@ -1161,7 +1165,6 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB return linkIndex !== -1 && marks[linkIndex].attrs.allAnchors.find((item: { href: string }) => textAnchorId === item.href.replace(/.*\/doc\//, '')) ? { node, start: 0 } : undefined; }; - let start = 0; this._didScroll = false; // assume we don't need to scroll. if we do, this will get set to true in handleScrollToSelextion when we dispatch the setSelection below if (this._editorView && textAnchorId) { const editor = this._editorView; @@ -1177,13 +1180,15 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB editor.dispatch(editor.state.tr.setSelection(new TextSelection(selection.$from, selection.$from)).scrollIntoView()); const escAnchorId = textAnchorId[0] >= '0' && textAnchorId[0] <= '9' ? `\\3${textAnchorId[0]} ${textAnchorId.substr(1)}` : textAnchorId; addStyleSheetRule(FormattedTextBox._highlightStyleSheet, `${escAnchorId}`, { background: 'yellow', transform: 'scale(3)', 'transform-origin': 'left bottom' }); - setTimeout(() => (this._focusSpeed = undefined), this._focusSpeed); + setTimeout(() => { + this._focusSpeed = undefined; + }, this._focusSpeed); 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); } + return undefined; }; // if the scroll height has changed and we're in layout_autoHeight mode, then we need to update the textHeight component of the doc. @@ -1199,11 +1204,11 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB } componentDidMount() { !this._props.dontSelectOnLoad && this._props.setContentViewBox?.(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._cachedLinks = Doc.Links(this.Document); this._disposers.breakupDictation = reaction(() => Doc.RecordingEvent, this.breakupDictation); this._disposers.layout_autoHeight = reaction( () => ({ autoHeight: this.layout_autoHeight, fontSize: this.fontSize, css: this.Document[DocCss] }), - (autoHeight, fontSize) => setTimeout(() => autoHeight && this.tryUpdateScrollHeight()) + autoHeight => setTimeout(() => autoHeight && this.tryUpdateScrollHeight()) ); this._disposers.highlights = reaction( () => Array.from(FormattedTextBox._globalHighlights).slice(), @@ -1212,21 +1217,21 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB ); this._disposers.width = reaction( () => this._props.PanelWidth(), - width => this.tryUpdateScrollHeight() + () => this.tryUpdateScrollHeight() ); this._disposers.scrollHeight = reaction( - () => ({ scrollHeight: this.scrollHeight, layout_autoHeight: this.layout_autoHeight, width: NumCast(this.layoutDoc._width) }), - ({ width, scrollHeight, layout_autoHeight }) => width && layout_autoHeight && this.resetNativeHeight(scrollHeight), + () => ({ scrollHeight: this.scrollHeight, layoutAutoHeight: this.layout_autoHeight, width: NumCast(this.layoutDoc._width) }), + ({ width, scrollHeight, layoutAutoHeight }) => width && layoutAutoHeight && this.resetNativeHeight(scrollHeight), { fireImmediately: true } ); this._disposers.componentHeights = reaction( // set the document height when one of the component heights changes and layout_autoHeight is on - () => ({ sidebarHeight: this.sidebarHeight, textHeight: this.textHeight, layout_autoHeight: this.layout_autoHeight, marginsHeight: this.layout_autoHeightMargins }), - ({ sidebarHeight, textHeight, layout_autoHeight, marginsHeight }) => { + () => ({ sidebarHeight: this.sidebarHeight, textHeight: this.textHeight, layoutAutoHeight: this.layout_autoHeight, marginsHeight: this.layout_autoHeightMargins }), + ({ sidebarHeight, textHeight, layoutAutoHeight, marginsHeight }) => { const newHeight = this.contentScaling * (marginsHeight + Math.max(sidebarHeight, textHeight)); if ( (!Array.from(FormattedTextBox._globalHighlights).includes('Bold Text') || this._props.isSelected()) && // - layout_autoHeight && + layoutAutoHeight && newHeight && newHeight !== this.layoutDoc.height && !this._props.dontRegisterView @@ -1237,7 +1242,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB { fireImmediately: !Array.from(FormattedTextBox._globalHighlights).includes('Bold Text') } ); this._disposers.links = reaction( - () => LinkManager.Links(this.dataDoc), // if a link is deleted, then remove all hyperlinks that reference it from the text's marks + () => Doc.Links(this.dataDoc), // if a link is deleted, then remove all hyperlinks that reference it from the text's marks newLinks => { this._cachedLinks.forEach(l => !newLinks.includes(l) && this.RemoveLinkFromDoc(l)); this._cachedLinks = newLinks; @@ -1274,14 +1279,15 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB this._disposers.search = reaction( () => Doc.IsSearchMatch(this.Document), search => (search ? this.highlightSearchTerms([Doc.SearchQuery()], search.searchMatch < 0) : this.unhighlightSearchTerms()), - { fireImmediately: Doc.IsSearchMatchUnmemoized(this.Document) ? true : false } + { fireImmediately: !!Doc.IsSearchMatchUnmemoized(this.Document) } ); this._disposers.selected = reaction( () => this._props.rootSelected?.(), action(selected => { - //selected && setTimeout(() => this.prepareForTyping()); + this.prepareForTyping(); if (FormattedTextBox._globalHighlights.has('Bold Text')) { + // eslint-disable-next-line operator-assignment this.layoutDoc[DocCss] = this.layoutDoc[DocCss] + 1; // css change happens outside of mobx/react, so this will notify anyone interested in the layout that it has changed } if (RichTextMenu.Instance?.view === this._editorView && !selected) { @@ -1299,7 +1305,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB this._disposers.record = reaction( () => this._recordingDictation, () => { - this.stopDictation(true); + this.stopDictation(/* true */); this._recordingDictation && this.recordDictation(); }, { fireImmediately: true } @@ -1322,10 +1328,10 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB } clipboardTextSerializer = (slice: Slice): string => { - let text = '', - separated = true; - const from = 0, - to = slice.content.size; + let text = ''; + let separated = true; + const from = 0; + const to = slice.content.size; slice.content.nodesBetween( from, to, @@ -1345,9 +1351,9 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB return text; }; - handlePaste = (view: EditorView, event: Event, slice: Slice): boolean => { + handlePaste = (view: EditorView, event: Event /* , slice: Slice */): boolean => { const pdfAnchorId = (event as ClipboardEvent).clipboardData?.getData('dash/pdfAnchor'); - return pdfAnchorId && this.addPdfReference(pdfAnchorId) ? true : false; + return !!(pdfAnchorId && this.addPdfReference(pdfAnchorId)); }; addPdfReference = (pdfAnchorId: string) => { @@ -1378,7 +1384,8 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB return false; }; - isActiveTab(el: Element | null | undefined) { + isActiveTab(elIn: Element | null | undefined) { + let el = elIn; while (el && el !== document.body) { if (getComputedStyle(el).display === 'none') return false; el = el.parentNode as any; @@ -1390,7 +1397,9 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB const self = this; return new Plugin({ view(newView) { - runInAction(() => self._props.rootSelected?.() && RichTextMenu.Instance && (RichTextMenu.Instance.view = newView)); + runInAction(() => { + self._props.rootSelected?.() && RichTextMenu.Instance && (RichTextMenu.Instance.view = newView); + }); return new RichTextMenuPlugin({ editorProps: this._props }); }, }); @@ -1415,7 +1424,9 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB const shift = Math.min(topOff ?? Number.MAX_VALUE, botOff ?? Number.MAX_VALUE); const scrollPos = scrollRef.scrollTop + shift * self.ScreenToLocalBoxXf().Scale; if (this._focusSpeed !== undefined) { - setTimeout(() => scrollPos && (this._scrollStopper = smoothScroll(this._focusSpeed || 0, scrollRef, scrollPos, 'ease', this._scrollStopper))); + setTimeout(() => { + scrollPos && (this._scrollStopper = smoothScroll(this._focusSpeed || 0, scrollRef, scrollPos, 'ease', this._scrollStopper)); + }); } else { scrollRef.scrollTo({ top: scrollPos }); } @@ -1425,25 +1436,12 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB }, dispatchTransaction: this.dispatchTransaction, nodeViews: { - dashComment(node: any, view: any, getPos: any) { - return new DashDocCommentView(node, view, getPos); - }, - dashDoc(node: any, view: any, getPos: any) { - return new DashDocView(node, view, getPos, self); - }, - dashField(node: any, view: any, getPos: any) { - return new DashFieldView(node, view, getPos, self); - }, - equation(node: any, view: any, getPos: any) { - return new EquationView(node, view, getPos, self); - }, - summary(node: any, view: any, getPos: any) { - return new SummaryView(node, view, getPos); - }, - //ordered_list(node: any, view: any, getPos: any) { return new OrderedListView(); }, - footnote(node: any, view: any, getPos: any) { - return new FootnoteView(node, view, getPos); - }, + dashComment(node: any, view: any, getPos: any) { return new DashDocCommentView(node, view, getPos); }, // prettier-ignore + dashDoc(node: any, view: any, getPos: any) { return new DashDocView(node, view, getPos, self); }, // prettier-ignore + dashField(node: any, view: any, getPos: any) { return new DashFieldView(node, view, getPos, self); }, // prettier-ignore + equation(node: any, view: any, getPos: any) { return new EquationView(node, view, getPos, self); }, // prettier-ignore + summary(node: any, view: any, getPos: any) { return new SummaryView(node, view, getPos); }, // prettier-ignore + footnote(node: any, view: any, getPos: any) { return new FootnoteView(node, view, getPos); }, // prettier-ignore }, clipboardTextSerializer: this.clipboardTextSerializer, handlePaste: this.handlePaste, @@ -1451,7 +1449,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB const { state, dispatch } = this._editorView; if (!rtfField) { const dataDoc = Doc.IsDelegateField(DocCast(this.layoutDoc.proto), this.fieldKey) ? DocCast(this.layoutDoc.proto) : this.dataDoc; - const startupText = Field.toString(dataDoc[fieldKey] as Field); + const startupText = Field.toString(dataDoc[fieldKey] as FieldType); if (startupText) { dispatch(state.tr.insertText(startupText)); } @@ -1465,17 +1463,17 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB (this._editorView as any).TextView = this; } - const selectOnLoad = Doc.AreProtosEqual(this._props.TemplateDataDocument ?? this.Document, FormattedTextBox.SelectOnLoad) && (!LightboxView.LightboxDoc || LightboxView.Contains(this.DocumentView?.())); + const selectOnLoad = Doc.AreProtosEqual(this._props.TemplateDataDocument ?? this.Document, Doc.SelectOnLoad) && (!LightboxView.LightboxDoc || LightboxView.Contains(this.DocumentView?.())); const selLoadChar = FormattedTextBox.SelectOnLoadChar; if (selectOnLoad) { - FormattedTextBox.SelectOnLoad = undefined; + Doc.SetSelectOnLoad(undefined); FormattedTextBox.SelectOnLoadChar = ''; } if (this._editorView && selectOnLoad && !this._props.dontRegisterView && !this._props.dontSelectOnLoad && this.isActiveTab(this.ProseRef)) { this._props.select(false); if (selLoadChar) { const $from = this._editorView.state.selection.anchor ? this._editorView.state.doc.resolve(this._editorView.state.selection.anchor - 1) : undefined; - const mark = schema.marks.user_mark.create({ userid: Doc.CurrentUserEmail, modified: Math.floor(Date.now() / 1000) }); + const mark = schema.marks.user_mark.create({ userid: ClientUtils.CurrentUserEmail(), modified: Math.floor(Date.now() / 1000) }); const curMarks = this._editorView.state.storedMarks ?? $from?.marksAcross(this._editorView.state.selection.$head) ?? []; const storedMarks = [...curMarks.filter(m => m.type !== mark.type), mark]; const tr1 = this._editorView.state.tr.setStoredMarks(storedMarks); @@ -1483,8 +1481,20 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB const tr = tr2.setStoredMarks(storedMarks); this._editorView.dispatch(tr.setSelection(new TextSelection(tr.doc.resolve(tr.doc.content.size)))); - } else if (curText && !FormattedTextBox.DontSelectInitialText) { - selectAll(this._editorView.state, this._editorView?.dispatch); + this.tryUpdateDoc(true); // calling select() above will make isContentActive() true only after a render .. which means the selectAll() above won't write to the Document and the incomingValue will overwrite the selection with the non-updated data + } else if (!FormattedTextBox.DontSelectInitialText) { + const mark = schema.marks.user_mark.create({ userid: ClientUtils.CurrentUserEmail(), modified: Math.floor(Date.now() / 1000) }); + selectAll(this._editorView.state, (tx: Transaction) => { + this._editorView?.dispatch(tx.deleteSelection().addStoredMark(mark)); + }); + this.tryUpdateDoc(true); // calling select() above will make isContentActive() true only after a render .. which means the selectAll() above won't write to the Document and the incomingValue will overwrite the selection with the non-updated data + } else { + const $from = this._editorView.state.selection.anchor ? this._editorView.state.doc.resolve(this._editorView.state.selection.anchor - 1) : undefined; + const mark = schema.marks.user_mark.create({ userid: ClientUtils.CurrentUserEmail(), modified: Math.floor(Date.now() / 1000) }); + const curMarks = this._editorView.state.storedMarks ?? $from?.marksAcross(this._editorView.state.selection.$head) ?? []; + const storedMarks = [...curMarks.filter(m => m.type !== mark.type), mark]; + const { tr } = this._editorView.state; + this._editorView.dispatch(tr.setSelection(new TextSelection(tr.doc.resolve(tr.doc.content.size))).setStoredMarks(storedMarks)); this.tryUpdateDoc(true); // calling select() above will make isContentActive() true only after a render .. which means the selectAll() above won't write to the Document and the incomingValue will overwrite the selection with the non-updated data } } @@ -1503,17 +1513,14 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB // add user mark for any first character that was typed since the user mark that gets set in KeyPress won't have been called yet. prepareForTyping = () => { - if (!this._editorView) return; - const docDefaultMarks = [ - ...(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().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)); + if (this._editorView) { + const { text, paragraph } = schema.nodes; + const selNode = this._editorView.state.selection.$anchor.node(); + if (this._editorView.state.selection.from === 1 && this._editorView.state.selection.empty && [undefined, text, paragraph].includes(selNode?.type)) { + const docDefaultMarks = [schema.marks.user_mark.create({ userid: ClientUtils.CurrentUserEmail(), modified: Math.floor(Date.now() / 1000) })]; + this._editorView.state.selection.empty && this._editorView.state.selection.from === 1 && this._editorView?.dispatch(this._editorView?.state.tr.setStoredMarks(docDefaultMarks).removeStoredMark(schema.marks.pFontColor)); + } + } }; componentWillUnmount() { @@ -1532,8 +1539,9 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB onPointerDown = (e: React.PointerEvent): void => { if ((e.nativeEvent as any).handledByInnerReactInstance) { - return; //e.stopPropagation(); - } else (e.nativeEvent as any).handledByInnerReactInstance = true; + return; // e.stopPropagation(); + } + (e.nativeEvent as any).handledByInnerReactInstance = true; if (this.Document.forceActive) e.stopPropagation(); this.tryUpdateScrollHeight(); // if a doc a fitWidth doc is being viewed in different embedContainer (eg freeform & lightbox), then it will have conflicting heights. so when the doc is clicked on, we want to make sure it has the appropriate height for the selected view. @@ -1546,7 +1554,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB // const timecode = NumCast(anchor.timecodeToShow, 0); const audiodoc = anchor.annotationOn as Doc; const func = () => { - const docView = DocumentManager.Instance.getDocumentView(audiodoc); + const docView = DocumentView.getDocumentView(audiodoc); if (!docView) { this._props.addDocTab(audiodoc, OpenWhere.addBottom); setTimeout(func); @@ -1559,9 +1567,6 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB if (this._recordingDictation && !e.ctrlKey && e.button === 0) { this.breakupDictation(); } - this._downX = e.clientX; - this._downY = e.clientY; - this._downTime = Date.now(); FormattedTextBoxComment.textBox = this; if (e.button === 0 && this._props.rootSelected?.() && !e.altKey && !e.ctrlKey && !e.metaKey) { if (e.clientX < this.ProseRef!.getBoundingClientRect().right) { @@ -1575,17 +1580,15 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB e.preventDefault(); } }; - onSelectEnd = (e: PointerEvent) => { - document.removeEventListener('pointerup', this.onSelectEnd); - }; + onSelectEnd = (): void => document.removeEventListener('pointerup', this.onSelectEnd); onPointerUp = (e: React.PointerEvent): void => { const state = this.EditorView?.state; if (state && this.ProseRef?.children[0].className.includes('-focused') && this._props.isContentActive() && !e.button) { if (!state.selection.empty && !(state.selection instanceof NodeSelection)) this.setupAnchorMenu(); - let target = e.target as any; // hrefs are stored on the dataset of the <a> node that wraps the hyerlink <span> - for (let target = e.target as any; target && !target.dataset?.targethrefs; target = target.parentElement); - while (target && !target.dataset?.targethrefs) target = target.parentElement; - FormattedTextBoxComment.update(this, this.EditorView!, undefined, target?.dataset?.targethrefs, target?.dataset.linkdoc, target?.dataset.nopreview === 'true'); + let clickTarget = e.target as any; // hrefs are stored on the dataset of the <a> node that wraps the hyerlink <span> + for (let { target } = e as any; target && !target.dataset?.targethrefs; target = target.parentElement); + while (clickTarget && !clickTarget.dataset?.targethrefs) clickTarget = clickTarget.parentElement; + FormattedTextBoxComment.update(this, this.EditorView!, undefined, clickTarget?.dataset?.targethrefs, clickTarget?.dataset.linkdoc, clickTarget?.dataset.nopreview === 'true'); } }; @action @@ -1613,7 +1616,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB }; @action onFocused = (e: React.FocusEvent): void => { - //applyDevTools.applyDevTools(this._editorView); + // applyDevTools.applyDevTools(this._editorView); this.ProseRef?.children[0] === e.nativeEvent.target && this._editorView && RichTextMenu.Instance?.updateMenu(this._editorView, undefined, this._props, this.layoutDoc); e.stopPropagation(); }; @@ -1624,10 +1627,6 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB e.stopPropagation(); return; } - if (Math.abs(e.clientX - this._downX) > 4 || Math.abs(e.clientY - this._downY) > 4) { - this._forceDownNode = undefined; - return; - } if (!this._forceUncollapse || (this._editorView!.root as any).getSelection().isCollapsed) { // this is a hack to allow the cursor to be placed at the end of a document when the document ends in an inline dash comment. Apparently Chrome on Windows has a bug/feature which breaks this when clicking after the end of the text. const pcords = this._editorView!.posAtCoords({ left: e.clientX, top: e.clientY }); @@ -1651,13 +1650,12 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB 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); + this.hitBulletTargets(e.clientX, e.clientY, !this._editorView?.state.selection.empty || this._forceUncollapse, false, e.shiftKey); } this._forceUncollapse = !(this._editorView!.root as any).getSelection().isCollapsed; - this._forceDownNode = (this._editorView!.state.selection as NodeSelection)?.node; }; // this hackiness handles clicking on the list item bullets to do expand/collapse. the bullets are ::before pseudo elements so there's no real way to hit test against them. - hitBulletTargets(x: number, y: number, collapse: boolean, highlightOnly: boolean, downNode: Node | undefined = undefined, selectOrderedList: boolean = false) { + hitBulletTargets(x: number, y: number, collapse: boolean, highlightOnly: boolean, selectOrderedList: boolean = false) { this._forceUncollapse = false; clearStyleSheetRules(FormattedTextBox._bulletStyleSheet); const clickPos = this._editorView!.posAtCoords({ left: x, top: y }); @@ -1710,9 +1708,9 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB if (!(this.EditorView?.state.selection instanceof NodeSelection)) { this.autoLink(); if (this._editorView?.state.tr) { - const tr = stordMarks?.reduce((tr, m) => { - tr.addStoredMark(m); - return tr; + const tr = stordMarks?.reduce((tr2, m) => { + tr2.addStoredMark(m); + return tr2; }, this._editorView.state.tr); tr && this._editorView.dispatch(tr); } @@ -1727,6 +1725,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB const match = RTFCast(this.Document[this.fieldKey])?.Text.match(/^(@[a-zA-Z][a-zA-Z_0-9 -]*[a-zA-Z_0-9-]+)/); if (match) { this.dataDoc.title_custom = true; + // eslint-disable-next-line prefer-destructuring this.dataDoc.title = match[1]; // this triggers the collectionDockingView to publish this Doc this.EditorView?.dispatch(this.EditorView?.state.tr.deleteRange(0, match[1].length + 1)); } @@ -1736,33 +1735,31 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB FormattedTextBox.LiveTextUndo?.end(); FormattedTextBox.LiveTextUndo = undefined; - const state = this._editorView!.state; // if the text box blurs and none of its contents are focused(), then pass the blur along setTimeout(() => !this.ProseRef?.contains(document.activeElement) && this._props.onBlur?.()); }; onKeyDown = (e: React.KeyboardEvent) => { + const { _editorView } = this; + if (!_editorView) return; if ((e.altKey || e.ctrlKey) && e.key === 't') { - e.preventDefault(); - e.stopPropagation(); this._props.setTitleFocus?.(); + StopEvent(e); return; } - const state = this._editorView!.state; + const { state } = _editorView; if (!state.selection.empty && e.key === '%') { this._rules!.EnteringStyle = true; - e.preventDefault(); - e.stopPropagation(); + StopEvent(e); return; } if (state.selection.empty || !this._rules!.EnteringStyle) { this._rules!.EnteringStyle = false; } - let stopPropagation = true; - for (var i = state.selection.from; i <= state.selection.to; i++) { + for (let i = state.selection.from; i <= state.selection.to; i++) { const node = state.doc.resolve(i); - if (state.doc.content.size - 1 > i && node?.marks?.().some(mark => mark.type === schema.marks.user_mark && mark.attrs.userid !== Doc.CurrentUserEmail) && [AclAugment, AclSelfEdit].includes(GetEffectiveAcl(this.Document))) { + if (state.doc.content.size - 1 > i && node?.marks?.().some(mark => mark.type === schema.marks.user_mark && mark.attrs.userid !== ClientUtils.CurrentUserEmail()) && [AclAugment, AclSelfEdit].includes(GetEffectiveAcl(this.Document))) { e.preventDefault(); } } @@ -1770,27 +1767,27 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB case 'Escape': this._editorView!.dispatch(state.tr.setSelection(TextSelection.create(state.doc, state.selection.from, state.selection.from))); (document.activeElement as any).blur?.(); - SelectionManager.DeselectAll(); + DocumentView.DeselectAll(); RichTextMenu.Instance?.updateMenu(undefined, undefined, undefined, undefined); return; case 'Enter': this.insertTime(); + // eslint-disable-next-line no-fallthrough case 'Tab': e.preventDefault(); break; - case 'c': - this._editorView?.state.selection.empty && (stopPropagation = false); + case 'Space': + case 'Backspace': break; default: - if (this._lastTimedMark?.attrs.userid === Doc.CurrentUserEmail) break; - case ' ': - if (e.code !== 'Space' && e.code !== 'Backspace') { - [AclEdit, AclAugment, AclAdmin].includes(GetEffectiveAcl(this.Document)) && - this._editorView!.dispatch(this._editorView!.state.tr.removeStoredMark(schema.marks.user_mark).addStoredMark(schema.marks.user_mark.create({ userid: Doc.CurrentUserEmail, modified: Math.floor(Date.now() / 1000) }))); + if ([AclEdit, AclAugment, AclAdmin].includes(GetEffectiveAcl(this.Document))) { + const modified = Math.floor(Date.now() / 1000); + const mark = state.selection.$to.marks().find(m => m.type === schema.marks.user_mark && m.attrs.modified === modified); + _editorView.dispatch(state.tr.removeStoredMark(schema.marks.user_mark).addStoredMark(mark ?? schema.marks.user_mark.create({ userid: ClientUtils.CurrentUserEmail(), modified }))); } break; } - if (stopPropagation) e.stopPropagation(); + e.stopPropagation(); this.startUndoTypingBatch(); }; ondrop = (e: React.DragEvent) => { @@ -1810,6 +1807,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB const margins = 2 * NumCast(this.layoutDoc._yMargin, this._props.yPadding || 0); const children = this.ProseRef?.children.length ? Array.from(this.ProseRef.children[0].children) : undefined; if (children && !SnappingManager.IsDragging) { + // eslint-disable-next-line no-use-before-define const getChildrenHeights = (kids: Element[] | undefined) => kids?.reduce((p, child) => p + toHgt(child), margins) ?? 0; const toNum = (val: string) => Number(val.replace('px', '')); const toHgt = (node: Element): number => { @@ -1821,7 +1819,9 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB const scrollHeight = this.ProseRef && proseHeight; if (this._props.setHeight && !this._props.suppressSetHeight && 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); + const setScrollHeight = () => { + this.dataDoc[this.fieldKey + '_scrollHeight'] = scrollHeight; + }; if (this.Document === this.layoutDoc || this.layoutDoc.resolvedDataDoc) { setScrollHeight(); @@ -1839,7 +1839,9 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB }; 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); + 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(); sidebarScreenToLocal = () => this._props @@ -1857,10 +1859,12 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB e, returnFalse, emptyFunction, - action(e => (this._recordingDictation = !this._recordingDictation)) + action(() => { + this._recordingDictation = !this._recordingDictation; + }) ) }> - <FontAwesomeIcon className="formattedTextBox-audioFont" style={{ color: 'red' }} icon={'microphone'} size="sm" /> + <FontAwesomeIcon className="formattedTextBox-audioFont" style={{ color: 'red' }} icon="microphone" size="sm" /> </div> ); } @@ -1885,15 +1889,16 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB } @computed get sidebarCollection() { const renderComponent = (tag: string) => { - const ComponentTag: any = tag === CollectionViewType.Freeform ? CollectionFreeFormView : tag === CollectionViewType.Tree ? CollectionTreeView : tag === 'translation' ? FormattedTextBox : CollectionStackingView; + const ComponentTag: any = tag === CollectionViewType.Tree ? CollectionTreeView : tag === 'translation' ? FormattedTextBox : CollectionStackingView; return ComponentTag === CollectionStackingView ? ( <SidebarAnnos ref={this._sidebarRef} + // eslint-disable-next-line react/jsx-props-no-spreading {...this._props} Document={this.Document} layoutDoc={this.layoutDoc} dataDoc={this.dataDoc} - usePanelWidth={true} + usePanelWidth nativeWidth={NumCast(this.layoutDoc._nativeWidth)} showSidebar={this.SidebarShown} whenChildContentsActiveChanged={this.whenChildContentsActiveChanged} @@ -1906,8 +1911,9 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB setHeight={this.setSidebarHeight} /> ) : ( - <div onPointerDown={e => setupMoveUpEvents(this, e, returnFalse, emptyFunction, () => SelectionManager.SelectView(this.DocumentView?.()!, false), true)}> + <div onPointerDown={e => setupMoveUpEvents(this, e, returnFalse, emptyFunction, () => DocumentView.SelectView(this.DocumentView?.()!, false), true)}> <ComponentTag + // eslint-disable-next-line react/jsx-props-no-spreading {...this._props} ref={this._sidebarTagRef as any} setContentView={emptyFunction} @@ -1930,8 +1936,8 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB renderDepth={this._props.renderDepth + 1} setHeight={this.setSidebarHeight} fitContentsToBox={this.fitContentsToBox} - noSidebar={true} - treeViewHideTitle={true} + noSidebar + treeViewHideTitle fieldKey={this.layoutDoc[this.SidebarKey + '_type_collection'] === 'translation' ? `${this.fieldKey}_translation` : `${this.fieldKey}_sidebar`} /> </div> @@ -1969,7 +1975,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB }> <div className="formattedTextBox-alternateButton" - onPointerDown={e => setupMoveUpEvents(e.target, e, returnFalse, emptyFunction, e => this.cycleAlternateText())} + onPointerDown={e => setupMoveUpEvents(e.target, e, returnFalse, emptyFunction, () => this.cycleAlternateText())} style={{ display: this._props.isContentActive() && !SnappingManager.IsDragging ? 'flex' : 'none', background: usePath === undefined ? 'white' : usePath === 'alternate' ? 'black' : 'gray', @@ -1990,23 +1996,18 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB @observable _isHovering = false; onPassiveWheel = (e: WheelEvent) => { if (e.clientX > this.ProseRef!.getBoundingClientRect().right) { - if (this.dataDoc[this.SidebarKey + '_type_collection'] === CollectionViewType.Freeform) { - // if the scrolled freeform is a child of the sidebar component, we need to let the event go through - // so react can let the freeform view handle it. We prevent default to stop any containing views from scrolling - e.preventDefault(); - } return; } // 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._header_height}px' > - const height = Number(styleFromLayoutString.height?.replace('px', '')); + const styleFromLayout = styleFromLayoutString(this.Document, this._props, scale); // this converts any expressions in the format string to style props. e.g., <FormattedTextBox height='{this._header_height}px' > + const height = Number(styleFromLayout.height?.replace('px', '')); // prevent default if selected || child is active but this doc isn't scrollable if ( - !Number.isNaN(height) && - (this._scrollRef?.scrollHeight ?? 0) <= Math.ceil((height ? height : this._props.PanelHeight()) / scale) && // + !isNaN(height) && + (this._scrollRef?.scrollHeight ?? 0) <= Math.ceil((height || this._props.PanelHeight()) / scale) && // (this._props.rootSelected?.() || this.isAnyChildContentActive()) ) { e.preventDefault(); @@ -2016,7 +2017,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB }; _oldWheel: any; @computed get fontColor() { - return this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.Color); + return this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.FontColor); } @computed get fontSize() { return this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.FontSize); @@ -2029,20 +2030,20 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB } render() { TraceMobx(); - const scale = this._props.NativeDimScaling?.() || 1; // * NumCast(this.layoutDoc._freeform_scale, 1); + const scale = this._props.NativeDimScaling?.() || 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._header_height}px' > - return styleFromLayoutString?.height === '0px' ? null : ( + const styleFromLayout = styleFromLayoutString(this.Document, this._props, scale); // this converts any expressions in the format string to style props. e.g., <FormattedTextBox height='{this._header_height}px' > + return styleFromLayout?.height === '0px' ? null : ( <div className="formattedTextBox" onPointerEnter={action(() => { this._isHovering = true; this.layoutDoc[`_${this._props.fieldKey}_usePath`] && (this.Document.isHovering = true); })} - onPointerLeave={action(() => (this.Document.isHovering = this._isHovering = false))} + onPointerLeave={action(() => { this.Document.isHovering = this._isHovering = false; })} // prettier-ignore ref={r => { this._oldWheel?.removeEventListener('wheel', this.onPassiveWheel); this._oldWheel = r; @@ -2062,7 +2063,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB fontSize: this.fontSize, fontFamily: this.fontFamily, fontWeight: this.fontWeight, - ...styleFromLayoutString, + ...styleFromLayout, }}> <div className="formattedTextBox-cont" @@ -2071,7 +2072,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB cursor: this._props.isContentActive() ? 'text' : undefined, height: this._props.height ? 'max-content' : undefined, overflow: this.layout_autoHeight ? 'hidden' : undefined, - pointerEvents: Doc.ActiveTool === InkTool.None && !this._props.onBrowseClickScript?.() ? undefined : 'none', + pointerEvents: Doc.ActiveTool === InkTool.None && !SnappingManager.ExploreMode ? undefined : 'none', }} onContextMenu={this.specificContextMenu} onKeyDown={this.onKeyDown} @@ -2084,7 +2085,9 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB onDoubleClick={this.onDoubleClick}> <div className="formattedTextBox-outer" - ref={r => (this._scrollRef = r)} + ref={r => { + this._scrollRef = r; + }} style={{ width: this.noSidebar ? '100%' : `calc(100% - ${this.layout_sidebarWidthPercent})`, overflow: this.layoutDoc._createDocOnCR ? 'hidden' : this.layoutDoc._layout_autoHeight ? 'visible' : undefined, @@ -2112,3 +2115,18 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB ); } } + +Docs.Prototypes.TemplateMap.set(DocumentType.RTF, { + layout: { view: FormattedTextBox, dataField: 'text' }, + options: { + acl: '', + _height: 35, + _xMargin: 10, + _yMargin: 10, + _layout_nativeDimEditable: true, + _layout_reflowVertical: true, + _layout_reflowHorizontal: true, + defaultDoubleClick: 'ignore', + systemIcon: 'BsFileEarmarkTextFill', + }, +}); diff --git a/src/client/views/nodes/formattedText/FormattedTextBoxComment.tsx b/src/client/views/nodes/formattedText/FormattedTextBoxComment.tsx index ce17af6ca..01c46edeb 100644 --- a/src/client/views/nodes/formattedText/FormattedTextBoxComment.tsx +++ b/src/client/views/nodes/formattedText/FormattedTextBoxComment.tsx @@ -1,15 +1,16 @@ import { Mark, ResolvedPos } from 'prosemirror-model'; -import { EditorState, NodeSelection } from 'prosemirror-state'; +import { EditorState } from 'prosemirror-state'; import { EditorView } from 'prosemirror-view'; +import { ClientUtils } from '../../../../ClientUtils'; import { Doc } from '../../../../fields/Doc'; import { DocServer } from '../../../DocServer'; -import { LinkDocPreview, LinkInfo } from '../LinkDocPreview'; +import { LinkInfo } from '../LinkDocPreview'; import { FormattedTextBox } from './FormattedTextBox'; import './FormattedTextBoxComment.scss'; import { schema } from './schema_rts'; export function findOtherUserMark(marks: readonly Mark[]): Mark | undefined { - return marks.find(m => m.attrs.userid && m.attrs.userid !== Doc.CurrentUserEmail); + return marks.find(m => m.attrs.userid && m.attrs.userid !== ClientUtils.CurrentUserEmail()); } export function findUserMark(marks: readonly Mark[]): Mark | undefined { return marks.find(m => m.attrs.userid); @@ -18,20 +19,22 @@ export function findLinkMark(marks: readonly Mark[]): Mark | undefined { return marks.find(m => m.type === schema.marks.autoLinkAnchor || m.type === schema.marks.linkAnchor); } export function findStartOfMark(rpos: ResolvedPos, view: EditorView, finder: (marks: readonly Mark[]) => Mark | undefined) { - let before = 0, - nbef = rpos.nodeBefore; + let before = 0; + let nbef = rpos.nodeBefore; while (nbef && finder(nbef.marks)) { before += nbef.nodeSize; + // eslint-disable-next-line no-param-reassign rpos = view.state.doc.resolve(rpos.pos - nbef.nodeSize); rpos && (nbef = rpos.nodeBefore); } return before; } export function findEndOfMark(rpos: ResolvedPos, view: EditorView, finder: (marks: readonly Mark[]) => Mark | undefined) { - let after = 0, - naft = rpos.nodeAfter; + let after = 0; + let naft = rpos.nodeAfter; while (naft && finder(naft.marks)) { after += naft.nodeSize; + // eslint-disable-next-line no-param-reassign rpos = view.state.doc.resolve(rpos.pos + naft.nodeSize); rpos && (naft = rpos.nodeAfter); } @@ -49,7 +52,7 @@ export class FormattedTextBoxComment { static userMark: Mark; static textBox: FormattedTextBox | undefined; - constructor(view: any) { + constructor() { if (!FormattedTextBoxComment.tooltip) { const tooltip = (FormattedTextBoxComment.tooltip = document.createElement('div')); const tooltipText = (FormattedTextBoxComment.tooltipText = document.createElement('div')); @@ -79,10 +82,10 @@ export class FormattedTextBoxComment { } static showCommentbox(view: EditorView, nbef: number) { - const state = view.state; + const { state } = view; // These are in screen coordinates - const start = view.coordsAtPos(state.selection.from - nbef), - end = view.coordsAtPos(state.selection.from - nbef); + const start = view.coordsAtPos(state.selection.from - nbef); + const end = view.coordsAtPos(state.selection.from - nbef); // The box in which the tooltip is positioned, to use as base const box = (document.getElementsByClassName('mainView-container') as any)[0].getBoundingClientRect(); // Find a center-ish x position from the selection endpoints (when crossing lines, end may be more to the left) @@ -109,14 +112,16 @@ export class FormattedTextBoxComment { } static setupPreview(view: EditorView, textBox: FormattedTextBox, hrefs?: string[], linkDoc?: string, noPreview?: boolean) { - const state = view.state; + const { state } = view; // this section checks to see if the insertion point is over text entered by a different user. If so, it sets ths comment text to indicate the user and the modification date if (state.selection.$from) { const nbef = findStartOfMark(state.selection.$from, view, findOtherUserMark); const naft = findEndOfMark(state.selection.$from, view, findOtherUserMark); const noselection = state.selection.$from === state.selection.$to; let child: any = null; - state.doc.nodesBetween(state.selection.from, state.selection.to, (node: any, pos: number, parent: any) => !child && node.marks.length && (child = node)); + state.doc.nodesBetween(state.selection.from, state.selection.to, (node: any /* , pos: number, parent: any */) => { + !child && node.marks.length && (child = node); + }); const mark = child && findOtherUserMark(child.marks); if (mark && child && (nbef || naft) && (!mark.attrs.opened || noselection)) { FormattedTextBoxComment.saveMarkRegion(textBox, state.selection.$from.pos - nbef, state.selection.$from.pos + naft, mark); @@ -131,7 +136,7 @@ export class FormattedTextBoxComment { if (state.selection.$from && hrefs?.length) { const nbef = findStartOfMark(state.selection.$from, view, findLinkMark); const naft = findEndOfMark(state.selection.$from, view, findLinkMark) || nbef; - //nbef && + // nbef && naft && LinkInfo.SetLinkInfo({ DocumentView: textBox.DocumentView, diff --git a/src/client/views/nodes/formattedText/OrderedListView.tsx b/src/client/views/nodes/formattedText/OrderedListView.tsx index c3595e59b..dbc60f7bf 100644 --- a/src/client/views/nodes/formattedText/OrderedListView.tsx +++ b/src/client/views/nodes/formattedText/OrderedListView.tsx @@ -1,8 +1,7 @@ export class OrderedListView { - - update(node: any) { - // if attr's of an ordered_list (e.g., bulletStyle) change, + update() { + // if attr's of an ordered_list (e.g., bulletStyle) change, // return false forces the dom node to be recreated which is necessary for the bullet labels to update - return false; + return false; } -}
\ No newline at end of file +} diff --git a/src/client/views/nodes/formattedText/ParagraphNodeSpec.ts b/src/client/views/nodes/formattedText/ParagraphNodeSpec.ts index 30da91710..8799964b3 100644 --- a/src/client/views/nodes/formattedText/ParagraphNodeSpec.ts +++ b/src/client/views/nodes/formattedText/ParagraphNodeSpec.ts @@ -1,18 +1,18 @@ +import { Node, DOMOutputSpec } from 'prosemirror-model'; import clamp from '../../../util/clamp'; import convertToCSSPTValue from '../../../util/convertToCSSPTValue'; import toCSSLineSpacing from '../../../util/toCSSLineSpacing'; -import { Node, DOMOutputSpec } from 'prosemirror-model'; -//import type { NodeSpec } from './Types'; +// import type { NodeSpec } from './Types'; type NodeSpec = { - attrs?: { [key: string]: any }, - content?: string, - draggable?: boolean, - group?: string, - inline?: boolean, - name?: string, - parseDOM?: Array<any>, - toDOM?: (node: any) => DOMOutputSpec, + attrs?: { [key: string]: any }; + content?: string; + draggable?: boolean; + group?: string; + inline?: boolean; + name?: string; + parseDOM?: Array<any>; + toDOM?: (node: any) => DOMOutputSpec; }; // This assumes that every 36pt maps to one indent level. @@ -25,41 +25,18 @@ export const EMPTY_CSS_VALUE = new Set(['', '0%', '0pt', '0px']); const ALIGN_PATTERN = /(left|right|center|justify)/; -// https://github.com/ProseMirror/prosemirror-schema-basic/blob/master/src/schema-basic.js -// :: NodeSpec A plain paragraph textblock. Represented in the DOM -// as a `<p>` element. -export const ParagraphNodeSpec: NodeSpec = { - attrs: { - align: { default: null }, - color: { default: null }, - id: { default: null }, - indent: { default: null }, - inset: { default: null }, - lineSpacing: { default: null }, - // TODO: Add UI to let user edit / clear padding. - paddingBottom: { default: null }, - // TODO: Add UI to let user edit / clear padding. - paddingTop: { default: null }, - }, - content: 'inline*', - group: 'block', - parseDOM: [{ tag: 'p', getAttrs }], - toDOM, -}; +function convertMarginLeftToIndentValue(marginLeft: string): number { + const ptValue = convertToCSSPTValue(marginLeft); + return clamp(MIN_INDENT_LEVEL, Math.floor(ptValue / INDENT_MARGIN_PT_SIZE), MAX_INDENT_LEVEL); +} function getAttrs(dom: HTMLElement): Object { - const { - lineHeight, - textAlign, - marginLeft, - paddingTop, - paddingBottom, - } = dom.style; + const { lineHeight, textAlign, marginLeft, paddingTop, paddingBottom } = dom.style; let align = dom.getAttribute('align') || textAlign || ''; - align = ALIGN_PATTERN.test(align) ? align : ""; + align = ALIGN_PATTERN.test(align) ? align : ''; - let indent = parseInt(dom.getAttribute(ATTRIBUTE_INDENT) || "", 10); + let indent = parseInt(dom.getAttribute(ATTRIBUTE_INDENT) || '', 10); if (!indent && marginLeft) { indent = convertMarginLeftToIndentValue(marginLeft); @@ -74,15 +51,7 @@ function getAttrs(dom: HTMLElement): Object { } function toDOM(node: Node): DOMOutputSpec { - const { - align, - indent, - inset, - lineSpacing, - paddingTop, - paddingBottom, - id, - } = node.attrs; + const { align, indent, inset, lineSpacing, paddingTop, paddingBottom, id } = node.attrs; const attrs: { [key: string]: any } | null = {}; let style = ''; @@ -128,16 +97,29 @@ function toDOM(node: Node): DOMOutputSpec { return ['p', attrs, 0]; } +// https://github.com/ProseMirror/prosemirror-schema-basic/blob/master/src/schema-basic.js +// :: NodeSpec A plain paragraph textblock. Represented in the DOM +// as a `<p>` element. +export const ParagraphNodeSpec: NodeSpec = { + attrs: { + align: { default: null }, + color: { default: null }, + id: { default: null }, + indent: { default: null }, + inset: { default: null }, + lineSpacing: { default: null }, + // TODO: Add UI to let user edit / clear padding. + paddingBottom: { default: null }, + // TODO: Add UI to let user edit / clear padding. + paddingTop: { default: null }, + }, + content: 'inline*', + group: 'block', + parseDOM: [{ tag: 'p', getAttrs }], + toDOM, +}; + export const toParagraphDOM = toDOM; export const getParagraphNodeAttrs = getAttrs; -export function convertMarginLeftToIndentValue(marginLeft: string): number { - const ptValue = convertToCSSPTValue(marginLeft); - return clamp( - MIN_INDENT_LEVEL, - Math.floor(ptValue / INDENT_MARGIN_PT_SIZE), - MAX_INDENT_LEVEL - ); -} - -export default ParagraphNodeSpec;
\ No newline at end of file +export default ParagraphNodeSpec; diff --git a/src/client/views/nodes/formattedText/ProsemirrorExampleTransfer.ts b/src/client/views/nodes/formattedText/ProsemirrorExampleTransfer.ts index 03c902580..7a8b72be0 100644 --- a/src/client/views/nodes/formattedText/ProsemirrorExampleTransfer.ts +++ b/src/client/views/nodes/formattedText/ProsemirrorExampleTransfer.ts @@ -1,29 +1,29 @@ import { chainCommands, deleteSelection, exitCode, joinBackward, joinDown, joinUp, lift, newlineInCode, selectNodeBackward, setBlockType, splitBlockKeepMarks, toggleMark, wrapIn } from 'prosemirror-commands'; import { redo, undo } from 'prosemirror-history'; import { Schema } from 'prosemirror-model'; -import { splitListItem, wrapInList, sinkListItem, liftListItem } from 'prosemirror-schema-list'; +import { liftListItem, sinkListItem, splitListItem, wrapInList } from 'prosemirror-schema-list'; import { EditorState, NodeSelection, TextSelection, Transaction } from 'prosemirror-state'; import { liftTarget } from 'prosemirror-transform'; -import { AclAdmin, AclAugment, AclEdit } from '../../../../fields/DocSymbols'; -import { GetEffectiveAcl } from '../../../../fields/util'; +import { EditorView } from 'prosemirror-view'; +import { ClientUtils } from '../../../../ClientUtils'; import { Utils } from '../../../../Utils'; +import { AclAdmin, AclAugment, AclEdit, DocData } from '../../../../fields/DocSymbols'; +import { GetEffectiveAcl } from '../../../../fields/util'; import { Docs } from '../../../documents/Documents'; import { RTFMarkup } from '../../../util/RTFMarkup'; -import { SelectionManager } from '../../../util/SelectionManager'; -import { OpenWhere } from '../DocumentView'; -import { Doc } from '../../../../fields/Doc'; -import { EditorView } from 'prosemirror-view'; +import { DocumentView } from '../DocumentView'; +import { OpenWhere } from '../OpenWhere'; const mac = typeof navigator !== 'undefined' ? /Mac/.test(navigator.platform) : false; export type KeyMap = { [key: string]: any }; -export let updateBullets = (tx2: Transaction, schema: Schema, assignedMapStyle?: string, from?: number, to?: number) => { +export const updateBullets = (tx2: Transaction, schema: Schema, assignedMapStyle?: string, from?: number, to?: number) => { let mapStyle = assignedMapStyle; - tx2.doc.descendants((node: any, offset: any, index: any) => { + tx2.doc.descendants((node: any, offset: any /* , index: any */) => { if ((from === undefined || to === undefined || (from <= offset + node.nodeSize && to >= offset)) && (node.type === schema.nodes.ordered_list || node.type === schema.nodes.list_item)) { - const path = (tx2.doc.resolve(offset) as any).path; - let depth = Array.from(path).reduce((p: number, c: any) => p + (c.hasOwnProperty('type') && c.type === schema.nodes.ordered_list ? 1 : 0), 0); + const { path } = tx2.doc.resolve(offset) as any; + let depth = Array.from(path).reduce((p: number, c: any) => p + (c.type === schema.nodes.ordered_list ? 1 : 0), 0); if (node.type === schema.nodes.ordered_list) { if (depth === 0 && !assignedMapStyle) mapStyle = node.attrs.mapStyle; depth++; @@ -34,38 +34,44 @@ export let updateBullets = (tx2: Transaction, schema: Schema, assignedMapStyle?: return tx2; }; -export function buildKeymap<S extends Schema<any>>(schema: S, props: any, mapKeys?: KeyMap): KeyMap { +export function buildKeymap<S extends Schema<any>>(schema: S, props: any): KeyMap { const keys: { [key: string]: any } = {}; function bind(key: string, cmd: any) { - if (mapKeys) { - const mapped = mapKeys[key]; - if (mapped === false) return; - if (mapped) key = mapped; - } keys[key] = cmd; } + function onKey(): boolean | undefined { + // bcz: this is pretty hacky -- prosemirror doesn't send us the keyboard event, but the 'event' variable is in scope.. so we access it anyway + // eslint-disable-next-line no-restricted-globals + return props.onKey?.(event, props); + } + const canEdit = (state: any) => { - switch (GetEffectiveAcl(props.TemplateDataDocument)) { + const permissions = GetEffectiveAcl(props.TemplateDataDocument ?? props.Document[DocData]); + switch (permissions) { case AclAugment: - const prevNode = state.selection.$cursor.nodeBefore; - const prevUser = !prevNode ? Doc.CurrentUserEmail : prevNode.marks[prevNode.marks.length - 1].attrs.userid; - if (prevUser != Doc.CurrentUserEmail) { - return false; + { + const prevNode = state.selection.$cursor.nodeBefore; + const prevUser = !prevNode ? ClientUtils.CurrentUserEmail() : prevNode.marks.lastElement()?.attrs.userid; + if (prevUser !== ClientUtils.CurrentUserEmail()) { + return false; + } } + break; + default: } return true; }; const toggleEditableMark = (mark: any) => (state: EditorState, dispatch: (tx: Transaction) => void) => canEdit(state) && toggleMark(mark)(state, dispatch); - //History commands + // History commands bind('Mod-z', undo); bind('Shift-Mod-z', redo); !mac && bind('Mod-y', redo); - //Commands to modify Mark + // Commands to modify Mark bind('Mod-b', toggleEditableMark(schema.marks.strong)); bind('Mod-B', toggleEditableMark(schema.marks.strong)); @@ -77,15 +83,15 @@ export function buildKeymap<S extends Schema<any>>(schema: S, props: any, mapKey bind('Mod-u', toggleEditableMark(schema.marks.underline)); bind('Mod-U', toggleEditableMark(schema.marks.underline)); - //Commands for lists + // Commands for lists bind('Ctrl-i', (state: EditorState, dispatch: (tx: Transaction) => void) => canEdit(state) && wrapInList(schema.nodes.ordered_list)(state as any, dispatch as any)); - bind('Ctrl-Tab', () => (props.onKey?.(event, props) ? true : true)); - bind('Alt-Tab', () => (props.onKey?.(event, props) ? true : true)); - bind('Meta-Tab', () => (props.onKey?.(event, props) ? true : true)); - bind('Meta-Enter', () => (props.onKey?.(event, props) ? true : true)); + bind('Ctrl-Tab', () => onKey() || true); + bind('Alt-Tab', () => onKey() || true); + bind('Meta-Tab', () => onKey() || true); + bind('Meta-Enter', () => onKey() || true); bind('Tab', (state: EditorState, dispatch: (tx: Transaction) => void) => { - if (props.onKey?.(event, props)) return true; + if (onKey()) return true; if (!canEdit(state)) return true; const ref = state.selection; const range = ref.$from.blockRange(ref.$to); @@ -103,8 +109,8 @@ export function buildKeymap<S extends Schema<any>>(schema: S, props: any, mapKey if ( !wrapInList(schema.nodes.ordered_list)(newstate.state as any, (tx2: Transaction) => { const tx25 = updateBullets(tx2, schema); - const ol_node = tx25.doc.nodeAt(range!.start)!; - const tx3 = tx25.setNodeMarkup(range!.start, ol_node.type, ol_node.attrs, marks); + const olNode = tx25.doc.nodeAt(range!.start)!; + const tx3 = tx25.setNodeMarkup(range!.start, olNode.type, olNode.attrs, marks); // when promoting to a list, assume list will format things so don't copy the stored marks. marks && tx3.ensureMarks([...marks]); marks && tx3.setStoredMarks([...marks]); @@ -115,10 +121,11 @@ export function buildKeymap<S extends Schema<any>>(schema: S, props: any, mapKey console.log('bullet promote fail'); } } + return undefined; }); bind('Shift-Tab', (state: EditorState, dispatch: (tx: Transaction) => void) => { - if (props.onKey?.(event, props)) return true; + if (onKey()) return true; if (!canEdit(state)) return true; const marks = state.storedMarks || (state.selection.$to.parentOffset && state.selection.$from.marks()); @@ -132,15 +139,16 @@ export function buildKeymap<S extends Schema<any>>(schema: S, props: any, mapKey ) { console.log('bullet demote fail'); } + return undefined; }); - //Command to create a new Tab with a PDF of all the command shortcuts - bind('Mod-/', (state: EditorState, dispatch: (tx: Transaction) => void) => { - const newDoc = Docs.Create.PdfDocument(Utils.prepend('/assets/cheat-sheet.pdf'), { _width: 300, _height: 300 }); + // Command to create a new Tab with a PDF of all the command shortcuts + bind('Mod-/', () => { + const newDoc = Docs.Create.PdfDocument(ClientUtils.prepend('/assets/cheat-sheet.pdf'), { _width: 300, _height: 300 }); props.addDocTab(newDoc, OpenWhere.addRight); }); - //Commands to modify BlockType + // Commands to modify BlockType bind('Ctrl->', (state: EditorState, dispatch: (tx: Transaction) => void) => canEdit(state && wrapIn(schema.nodes.blockquote)(state as any, dispatch as any))); bind('Alt-\\', (state: EditorState, dispatch: (tx: Transaction) => void) => canEdit(state) && setBlockType(schema.nodes.paragraph)(state as any, dispatch as any)); bind('Shift-Ctrl-\\', (state: EditorState, dispatch: (tx: Transaction) => void) => canEdit(state) && setBlockType(schema.nodes.code_block)(state as any, dispatch as any)); @@ -156,25 +164,25 @@ export function buildKeymap<S extends Schema<any>>(schema: S, props: any, mapKey bind('Shift-Ctrl-' + i, (state: EditorState, dispatch: (tx: Transaction) => void) => canEdit(state) && setBlockType(schema.nodes.heading, { level: i })(state as any, dispatch as any)); } - //Command to create a horizontal break line + // Command to create a horizontal break line const hr = schema.nodes.horizontal_rule; bind('Mod-_', (state: EditorState, dispatch: (tx: Transaction) => void) => canEdit(state) && dispatch(state.tr.replaceSelectionWith(hr.create()).scrollIntoView())); - //Command to unselect all + // Command to unselect all bind('Escape', (state: EditorState, dispatch: (tx: Transaction) => void) => { dispatch(state.tr.setSelection(TextSelection.create(state.doc, state.selection.from, state.selection.from))); (document.activeElement as any).blur?.(); - SelectionManager.DeselectAll(); + DocumentView.DeselectAll(); }); - bind('Alt-Enter', () => (props.onKey?.(event, props) ? true : true)); - bind('Ctrl-Enter', () => (props.onKey?.(event, props) ? true : true)); + bind('Alt-Enter', () => onKey() || true); + bind('Ctrl-Enter', () => onKey() || true); bind('Cmd-a', (state: EditorState, dispatch: (tx: Transaction) => void) => { dispatch(state.tr.setSelection(new TextSelection(state.doc.resolve(1), state.doc.resolve(state.doc.content.size - 1)))); return true; }); - bind('Cmd-?', (state: EditorState, dispatch: (tx: Transaction) => void) => { - RTFMarkup.Instance.open(); + bind('Cmd-?', () => { + RTFMarkup.Instance.setOpen(true); return true; }); bind('Cmd-e', (state: EditorState, dispatch: (tx: Transaction) => void) => { @@ -189,14 +197,14 @@ export function buildKeymap<S extends Schema<any>>(schema: S, props: any, mapKey }); bind('Cmd-]', (state: EditorState, dispatch: (tx: Transaction) => void) => { const resolved = state.doc.resolve(state.selection.from) as any; - const tr = state.tr; + const { tr } = state; if (resolved?.parent.type.name === 'paragraph') { tr.setNodeMarkup(resolved.path[resolved.path.length - 4], schema.nodes.paragraph, { ...resolved.parent.attrs, align: 'right' }, resolved.parent.marks); } else { const node = resolved.nodeAfter; const sm = state.storedMarks || undefined; if (node) { - tr.replaceRangeWith(state.selection.from, state.selection.from, schema.nodes.paragraph.create({ align: 'right' })).setStoredMarks([...node.marks, ...(sm ? sm : [])]); + tr.replaceRangeWith(state.selection.from, state.selection.from, schema.nodes.paragraph.create({ align: 'right' })).setStoredMarks([...node.marks, ...(sm || [])]); } } dispatch(tr); @@ -204,14 +212,14 @@ export function buildKeymap<S extends Schema<any>>(schema: S, props: any, mapKey }); bind('Cmd-\\', (state: EditorState, dispatch: (tx: Transaction) => void) => { const resolved = state.doc.resolve(state.selection.from) as any; - const tr = state.tr; + const { tr } = state; if (resolved?.parent.type.name === 'paragraph') { tr.setNodeMarkup(resolved.path[resolved.path.length - 4], schema.nodes.paragraph, { ...resolved.parent.attrs, align: 'center' }, resolved.parent.marks); } else { const node = resolved.nodeAfter; const sm = state.storedMarks || undefined; if (node) { - tr.replaceRangeWith(state.selection.from, state.selection.from, schema.nodes.paragraph.create({ align: 'center' })).setStoredMarks([...node.marks, ...(sm ? sm : [])]); + tr.replaceRangeWith(state.selection.from, state.selection.from, schema.nodes.paragraph.create({ align: 'center' })).setStoredMarks([...node.marks, ...(sm || [])]); } } dispatch(tr); @@ -219,14 +227,14 @@ export function buildKeymap<S extends Schema<any>>(schema: S, props: any, mapKey }); bind('Cmd-[', (state: EditorState, dispatch: (tx: Transaction) => void) => { const resolved = state.doc.resolve(state.selection.from) as any; - const tr = state.tr; + const { tr } = state; if (resolved?.parent.type.name === 'paragraph') { tr.setNodeMarkup(resolved.path[resolved.path.length - 4], schema.nodes.paragraph, { ...resolved.parent.attrs, align: 'left' }, resolved.parent.marks); } else { const node = resolved.nodeAfter; const sm = state.storedMarks || undefined; if (node) { - tr.replaceRangeWith(state.selection.from, state.selection.from, schema.nodes.paragraph.create({ align: 'left' })).setStoredMarks([...node.marks, ...(sm ? sm : [])]); + tr.replaceRangeWith(state.selection.from, state.selection.from, schema.nodes.paragraph.create({ align: 'left' })).setStoredMarks([...node.marks, ...(sm || [])]); } } dispatch(tr); @@ -236,7 +244,7 @@ export function buildKeymap<S extends Schema<any>>(schema: S, props: any, mapKey bind('Cmd-f', (state: EditorState, dispatch: (tx: Transaction) => void) => { const content = state.tr.selection.empty ? undefined : state.tr.selection.content().content.textBetween(0, state.tr.selection.content().size + 1); const newNode = schema.nodes.footnote.create({}, content ? state.schema.text(content) : undefined); - const tr = state.tr; + const { tr } = state; tr.replaceSelectionWith(newNode); // replace insertion with a footnote. dispatch( tr.setSelection( @@ -258,7 +266,7 @@ export function buildKeymap<S extends Schema<any>>(schema: S, props: any, mapKey // backspace = chainCommands(deleteSelection, joinBackward, selectNodeBackward); const backspace = (state: EditorState, dispatch: (tx: Transaction) => void, view: EditorView) => { - if (props.onKey?.(event, props)) return true; + if (onKey()) return true; if (!canEdit(state)) return true; if ( @@ -271,7 +279,7 @@ export function buildKeymap<S extends Schema<any>>(schema: S, props: any, mapKey dispatch(updateBullets(tx, schema)); if (view.state.selection.$anchor.node(-1)?.type === schema.nodes.list_item) { // gets rid of an extra paragraph when joining two list items together. - joinBackward(view.state, (tx: Transaction) => view.dispatch(tx)); + joinBackward(view.state, (tx2: Transaction) => view.dispatch(tx2)); } }) ) { @@ -288,11 +296,11 @@ export function buildKeymap<S extends Schema<any>>(schema: S, props: any, mapKey }; bind('Backspace', backspace); - //newlineInCode, createParagraphNear, liftEmptyBlock, splitBlock - //command to break line + // newlineInCode, createParagraphNear, liftEmptyBlock, splitBlock + // command to break line const enter = (state: EditorState, dispatch: (tx: Transaction) => void, view: EditorView, once = true) => { - if (props.onKey?.(event, props)) return true; + if (onKey()) return true; if (!canEdit(state)) return true; const trange = state.selection.$from.blockRange(state.selection.$to); @@ -337,7 +345,7 @@ export function buildKeymap<S extends Schema<any>>(schema: S, props: any, mapKey !splitBlockKeepMarks(state, (tx3: Transaction) => { const tonode = tx3.selection.$to.node(); if (tx3.selection.to && tx3.doc.nodeAt(tx3.selection.to - 1)) { - const tx4 = tx3.setNodeMarkup(tx3.selection.to - 1, tonode.type, fromattrs, tonode.marks); + const tx4 = tx3.setNodeMarkup(tx3.selection.to - 1, tonode.type, fromattrs, tonode.marks).setStoredMarks(marks || []); dispatch(tx4); } @@ -356,9 +364,10 @@ export function buildKeymap<S extends Schema<any>>(schema: S, props: any, mapKey }; bind('Enter', enter); - //Command to create a blank space - bind('Space', (state: EditorState, dispatch: (tx: Transaction) => void) => { - if (props.TemplateDataDocument && GetEffectiveAcl(props.TemplateDataDocument) != AclEdit && GetEffectiveAcl(props.TemplateDataDocument) != AclAugment && GetEffectiveAcl(props.TemplateDataDocument) != AclAdmin) return true; + // Command to create a blank space + bind('Space', () => { + const editDoc = props.TemplateDataDocument ?? props.Document[DocData]; + if (editDoc && ![AclAdmin, AclAugment, AclEdit].includes(GetEffectiveAcl(editDoc))) return true; return false; }); diff --git a/src/client/views/nodes/formattedText/RichTextMenu.tsx b/src/client/views/nodes/formattedText/RichTextMenu.tsx index cecf106a3..a612f3c65 100644 --- a/src/client/views/nodes/formattedText/RichTextMenu.tsx +++ b/src/client/views/nodes/formattedText/RichTextMenu.tsx @@ -3,30 +3,30 @@ import { Tooltip } from '@mui/material'; import { action, computed, IReactionDisposer, makeObservable, observable, reaction } from 'mobx'; import { observer } from 'mobx-react'; import { lift, wrapIn } from 'prosemirror-commands'; -import { Mark, MarkType, Node as ProsNode, ResolvedPos } from 'prosemirror-model'; +import { Mark, MarkType } from 'prosemirror-model'; import { wrapInList } from 'prosemirror-schema-list'; import { EditorState, NodeSelection, TextSelection } from 'prosemirror-state'; import { EditorView } from 'prosemirror-view'; import * as React from 'react'; import { Doc } from '../../../../fields/Doc'; import { BoolCast, Cast, StrCast } from '../../../../fields/Types'; -import { numberRange } from '../../../../Utils'; import { DocServer } from '../../../DocServer'; -import { LinkManager } from '../../../util/LinkManager'; -import { SelectionManager } from '../../../util/SelectionManager'; import { undoBatch, UndoManager } from '../../../util/UndoManager'; import { AntimodeMenu, AntimodeMenuProps } from '../../AntimodeMenu'; import { ObservableReactComponent } from '../../ObservableReactComponent'; +import { DocumentView } from '../DocumentView'; import { EquationBox } from '../EquationBox'; import { FieldViewProps } from '../FieldView'; import { FormattedTextBox } from './FormattedTextBox'; import { updateBullets } from './ProsemirrorExampleTransfer'; import './RichTextMenu.scss'; import { schema } from './schema_rts'; + const { toggleMark } = require('prosemirror-commands'); @observer export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> { + // eslint-disable-next-line no-use-before-define static _instance: { menu: RichTextMenu | undefined } = observable({ menu: undefined }); static get Instance() { return RichTextMenu._instance?.menu; @@ -115,8 +115,8 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> { _disposer: IReactionDisposer | undefined; componentDidMount() { this._disposer = reaction( - () => SelectionManager.Views.slice(), - views => this.updateMenu(undefined, undefined, undefined, undefined) + () => DocumentView.Selected().slice(), + () => this.updateMenu(undefined, undefined, undefined, undefined) ); } componentWillUnmount() { @@ -139,18 +139,19 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> { this.setActiveMarkButtons(this.getActiveMarksOnSelection()); const active = this.getActiveFontStylesOnSelection(); - const activeFamilies = active.activeFamilies; - const activeSizes = active.activeSizes; - const activeColors = active.activeColors; - const activeHighlights = active.activeHighlights; - const refDoc = SelectionManager.Views.lastElement()?.layoutDoc ?? Doc.UserDoc(); - const refField = (pfx => (pfx ? pfx + '_' : ''))(SelectionManager.Views.lastElement()?.LayoutFieldKey); + const { activeFamilies } = active; + const { activeSizes } = active; + const { activeColors } = active; + const { activeHighlights } = active; + const refDoc = DocumentView.Selected().lastElement()?.layoutDoc ?? Doc.UserDoc(); + const refField = (pfx => (pfx ? pfx + '_' : ''))(DocumentView.Selected().lastElement()?.LayoutFieldKey); + const refVal = (field: string, dflt: string) => StrCast(refDoc[refField + field], StrCast(Doc.UserDoc()[field], dflt)); this._activeListType = this.getActiveListStyle(); this._activeAlignment = this.getActiveAlignment(); - this._activeFontFamily = !activeFamilies.length ? StrCast(this.TextView?.Document._text_fontFamily, StrCast(refDoc[refField + 'fontFamily'], 'Arial')) : activeFamilies.length === 1 ? String(activeFamilies[0]) : 'various'; - this._activeFontSize = !activeSizes.length ? StrCast(this.TextView?.Document.fontSize, StrCast(refDoc[refField + 'fontSize'], '10px')) : activeSizes[0]; - this._activeFontColor = !activeColors.length ? StrCast(this.TextView?.Document.fontColor, StrCast(refDoc[refField + 'fontColor'], 'black')) : activeColors.length > 0 ? String(activeColors[0]) : '...'; + this._activeFontFamily = !activeFamilies.length ? StrCast(this.TextView?.Document._text_fontFamily, refVal('fontFamily', 'Arial')) : activeFamilies.length === 1 ? String(activeFamilies[0]) : 'various'; + this._activeFontSize = !activeSizes.length ? StrCast(this.TextView?.Document.fontSize, refVal('fontSize', '10px')) : activeSizes[0]; + this._activeFontColor = !activeColors.length ? StrCast(this.TextView?.Document.fontColor, refVal('fontColor', 'black')) : activeColors.length > 0 ? String(activeColors[0]) : '...'; this._activeHighlightColor = !activeHighlights.length ? '' : activeHighlights.length > 0 ? String(activeHighlights[0]) : '...'; // update link in current selection @@ -159,12 +160,7 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> { setMark = (mark: Mark, state: EditorState, dispatch: any, dontToggle: boolean = false) => { if (mark) { - const liFirst = numberRange(state.selection.$from.depth + 1).find(i => state.selection.$from.node(i)?.type === state.schema.nodes.list_item); - const liTo = numberRange(state.selection.$to.depth + 1).find(i => state.selection.$to.node(i)?.type === state.schema.nodes.list_item); - const olFirst = numberRange(state.selection.$from.depth + 1).find(i => state.selection.$from.node(i)?.type === state.schema.nodes.ordered_list); - const nodeOl = (liFirst && liTo && state.selection.$from.node(liFirst) !== state.selection.$to.node(liTo) && olFirst) || (!liFirst && !liTo && olFirst); - const fromRange = numberRange(state.selection.from).reverse(); - const newPos = nodeOl ? fromRange.find(i => state.doc.nodeAt(i)?.type === state.schema.nodes.ordered_list) ?? state.selection.from : state.selection.from; + const newPos = state.selection.$anchor.node()?.type === schema.nodes.ordered_list ? state.selection.from : state.selection.from; const node = (state.selection as NodeSelection).node ?? (newPos >= 0 ? state.doc.nodeAt(newPos) : undefined); if (node?.type === schema.nodes.ordered_list || node?.type === schema.nodes.list_item) { const hasMark = node.marks.some(m => m.type === mark.type); @@ -172,25 +168,23 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> { const addAnyway = node.marks.filter(m => m.type === mark.type && Object.keys(m.attrs).some(akey => m.attrs[akey] !== mark.attrs[akey])); const markup = state.tr.setNodeMarkup(newPos, node.type, node.attrs, hasMark && !addAnyway ? otherMarks : [...otherMarks, mark]); dispatch(updateBullets(markup, state.schema)); - } else { - const state = this.view?.state; - const tr = this.view?.state.tr; - if (tr && state) { - if (dontToggle) { - tr.addMark(state.selection.from, state.selection.to, mark); - dispatch(tr.setSelection(new TextSelection(tr.doc.resolve(state.selection.from), tr.doc.resolve(state.selection.to)))); // bcz: need to redo the selection because ctrl-a selections disappear otherwise - } else { - toggleMark(mark.type, mark.attrs)(state, dispatch); - } + } else if (state) { + const { tr } = state; + if (dontToggle) { + tr.addMark(state.selection.from, state.selection.to, mark); + dispatch(tr.setSelection(new TextSelection(tr.doc.resolve(state.selection.from), tr.doc.resolve(state.selection.to)))); // bcz: need to redo the selection because ctrl-a selections disappear otherwise + } else { + toggleMark(mark.type, mark.attrs)(state, dispatch); } } + this.updateMenu(this.view, undefined, undefined, this.layoutDoc); } }; // finds font sizes and families in selection getActiveAlignment() { if (this.view && this.TextView?._props.rootSelected?.()) { - const path = (this.view.state.selection.$from as any).path; + const { path } = this.view.state.selection.$from as any; for (let i = path.length - 3; i < path.length && i >= 0; i -= 3) { if (path[i]?.type === this.view.state.schema.nodes.paragraph || path[i]?.type === this.view.state.schema.nodes.heading) { return path[i].attrs.align || 'left'; @@ -222,27 +216,28 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> { const activeColors = new Set<string>(); const activeHighlights = new Set<string>(); if (this.view && this.TextView?._props.rootSelected?.()) { - const state = this.view.state; + const { state } = this.view; const pos = this.view.state.selection.$from; - var marks: Mark[] = [...(state.storedMarks ?? [])]; + let marks: Mark[] = [...(state.storedMarks ?? [])]; if (state.storedMarks !== null) { + /* empty */ } else if (state.selection.empty) { for (let i = 0; i <= pos.depth; i++) { marks = [...Array.from(pos.node(i).marks), ...this.view.state.selection.$anchor.marks(), ...marks]; } } else { - state.doc.nodesBetween(state.selection.from, state.selection.to, (node, pos, parent, index) => { + state.doc.nodesBetween(state.selection.from, state.selection.to, (node /* , pos, parent, index */) => { node.marks?.filter(mark => !mark.isInSet(marks)).map(mark => marks.push(mark)); }); } marks.forEach(m => { - m.type === state.schema.marks.pFontFamily && activeFamilies.add(m.attrs.family); - m.type === state.schema.marks.pFontColor && activeColors.add(m.attrs.color); + m.type === state.schema.marks.pFontFamily && activeFamilies.add(m.attrs.fontFamily); + m.type === state.schema.marks.pFontColor && activeColors.add(m.attrs.fontColor); m.type === state.schema.marks.pFontSize && activeSizes.add(m.attrs.fontSize); - m.type === state.schema.marks.marker && activeHighlights.add(String(m.attrs.highlight)); + m.type === state.schema.marks.pFontHighlight && activeHighlights.add(String(m.attrs.fontHighlight)); }); - } else if (SelectionManager.Views.some(dv => dv.ComponentView instanceof EquationBox)) { - SelectionManager.Views.forEach(dv => StrCast(dv.Document._text_fontSize) && activeSizes.add(StrCast(dv.Document._text_fontSize))); + } else if (DocumentView.Selected().some(dv => dv.ComponentView instanceof EquationBox)) { + DocumentView.Selected().forEach(dv => StrCast(dv.Document._text_fontSize) && activeSizes.add(StrCast(dv.Document._text_fontSize))); } return { activeFamilies: Array.from(activeFamilies), activeSizes: Array.from(activeSizes), activeColors: Array.from(activeColors), activeHighlights: Array.from(activeHighlights) }; } @@ -254,26 +249,27 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> { return found; } - //finds all active marks on selection in given group + // finds all active marks on selection in given group getActiveMarksOnSelection() { if (!this.view || !this.TextView?._props.rootSelected?.()) return [] as MarkType[]; - const state = this.view.state; - var marks: Mark[] = [...(state.storedMarks ?? [])]; + const { state } = this.view; + let marks: Mark[] = [...(state.storedMarks ?? [])]; const pos = this.view.state.selection.$from; if (state.storedMarks !== null) { + /* empty */ } else if (state.selection.empty) { for (let i = 0; i <= pos.depth; i++) { marks = [...Array.from(pos.node(i).marks), ...this.view.state.selection.$anchor.marks(), ...marks]; } } else { - state.doc.nodesBetween(state.selection.from, state.selection.to, (node, pos, parent, index) => { + state.doc.nodesBetween(state.selection.from, state.selection.to, (node /* , pos, parent, index */) => { node.marks?.filter(mark => !mark.isInSet(marks)).map(mark => marks.push(mark)); }); } const markGroup = [schema.marks.noAutoLinkAnchor, schema.marks.strong, schema.marks.em, schema.marks.underline, schema.marks.strikethrough, schema.marks.superscript, schema.marks.subscript]; - return markGroup.filter(mark_type => { - const mark = state.schema.mark(mark_type); + return markGroup.filter(markType => { + const mark = state.schema.mark(markType); return mark.isInSet(marks); }); } @@ -291,7 +287,6 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> { this._superscriptActive = false; activeMarks.forEach(mark => { - // prettier-ignore switch (mark.name) { case 'noAutoLinkAnchor': this._noLinkActive = true; break; case 'strong': this._boldActive = true; break; @@ -300,7 +295,8 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> { case 'strikethrough': this._strikethroughActive = true; break; case 'subscript': this._subscriptActive = true; break; case 'superscript': this._superscriptActive = true; break; - } + default: + } // prettier-ignore }); } @@ -353,53 +349,25 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> { } }; - setFontSize = (fontSize: string) => { + setFontField = (value: string, fontField: 'fontSize' | 'fontFamily' | 'fontColor' | 'fontHighlight') => { if (this.view) { - if (this.view.state.selection.from === 1 && this.view.state.selection.empty && (!this.view.state.doc.nodeAt(1) || !this.view.state.doc.nodeAt(1)?.marks.some(m => m.type.name === fontSize))) { - this.TextView.dataDoc[this.TextView.fieldKey + '_fontSize'] = fontSize; - this.view.focus(); - } else { - const fmark = this.view.state.schema.marks.pFontSize.create({ fontSize }); - this.setMark(fmark, this.view.state, (tx: any) => this.view!.dispatch(tx.addStoredMark(fmark)), true); + const { text, paragraph } = this.view.state.schema.nodes; + const selNode = this.view.state.selection.$anchor.node(); + if (this.view.state.selection.from === 1 && this.view.state.selection.empty && [undefined, text, paragraph].includes(selNode?.type)) { + this.TextView.dataDoc[this.TextView.fieldKey + `_${fontField}`] = value; this.view.focus(); } - } else if (SelectionManager.Views.length) { - SelectionManager.Views.forEach(dv => (dv.layoutDoc[dv.LayoutFieldKey + '_fontSize'] = fontSize)); - } else Doc.UserDoc().fontSize = fontSize; - this.updateMenu(this.view, undefined, this.props, this.layoutDoc); - }; - - setFontFamily = (family: string) => { - if (this.view) { - const fmark = this.view.state.schema.marks.pFontFamily.create({ family }); + const attrs: { [key: string]: string } = {}; + attrs[fontField] = value; + const fmark = this.view?.state.schema.marks['pF' + fontField.substring(1)].create(attrs); this.setMark(fmark, this.view.state, (tx: any) => this.view!.dispatch(tx.addStoredMark(fmark)), true); this.view.focus(); - } else if (SelectionManager.Views.length) { - SelectionManager.Views.forEach(dv => (dv.layoutDoc[dv.LayoutFieldKey + '_fontFamily'] = family)); - } else Doc.UserDoc().fontFamily = family; - this.updateMenu(this.view, undefined, this.props, this.layoutDoc); + } else { + Doc.UserDoc()[fontField] = value; + this.updateMenu(this.view, undefined, this.props, this.layoutDoc); + } }; - setHighlight(color: string) { - if (this.view) { - const highlightMark = this.view.state.schema.mark(this.view.state.schema.marks.marker, { highlight: color }); - 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); - } - - setColor(color: string) { - if (this.view) { - const colorMark = this.view.state.schema.mark(this.view.state.schema.marks.pFontColor, { color }); - this.setMark(colorMark, this.view.state, (tx: any) => this.view!.dispatch(tx.addStoredMark(colorMark)), true); - this.view.focus(); - } else if (SelectionManager.Views.length) { - SelectionManager.Views.forEach(dv => (dv.layoutDoc[dv.LayoutFieldKey + '_fontColor'] = color)); - } else Doc.UserDoc().fontColor = color; - this.updateMenu(this.view, undefined, this.props, this.layoutDoc); - } - // TODO: remove doesn't work // remove all node type and apply the passed-in one to the selected text changeListType = (mapStyle: string) => { @@ -407,7 +375,7 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> { const newMapStyle = active === mapStyle ? '' : mapStyle; if (!this.view || newMapStyle === '') return; - let inList = this.view.state.selection.$anchor.node(1).type === schema.nodes.ordered_list; + const inList = this.view.state.selection.$anchor.node(1).type === schema.nodes.ordered_list; const marks = this.view.state.storedMarks || (this.view.state.selection.$to.parentOffset && this.view.state.selection.$from.marks()); if (inList) { const tx2 = updateBullets(this.view.state.tr, schema, newMapStyle, this.view.state.doc.resolve(this.view.state.selection.$anchor.before(1) + 1).pos, this.view.state.doc.resolve(this.view.state.selection.$anchor.after(1)).pos); @@ -428,7 +396,7 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> { insertSummarizer(state: EditorState, dispatch: any) { if (state.selection.empty) return false; const mark = state.schema.marks.summarize.create(); - const tr = state.tr; + const { tr } = state; tr.addMark(state.selection.from, state.selection.to, mark); const content = tr.selection.content(); const newNode = state.schema.nodes.summary.create({ visibility: false, text: content, textslice: content.toJSON() }); @@ -436,13 +404,13 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> { return true; } - vcenterToggle = (view: EditorView, dispatch: any) => { + vcenterToggle = () => { this.layoutDoc && (this.layoutDoc._layout_centered = !this.layoutDoc._layout_centered); }; align = (view: EditorView, dispatch: any, alignment: 'left' | 'right' | 'center') => { if (this.TextView?._props.rootSelected?.()) { - var tr = view.state.tr; - view.state.doc.nodesBetween(view.state.selection.from, view.state.selection.to, (node, pos, parent, index) => { + let { tr } = view.state; + view.state.doc.nodesBetween(view.state.selection.from, view.state.selection.to, (node, pos) => { if ([schema.nodes.paragraph, schema.nodes.heading].includes(node.type)) { tr = tr.setNodeMarkup(pos, node.type, { ...node.attrs, align: alignment }, node.marks); return false; @@ -455,56 +423,14 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> { } }; - insetParagraph(state: EditorState, dispatch: any) { - var tr = state.tr; - state.doc.nodesBetween(state.selection.from, state.selection.to, (node, pos, parent, index) => { - if (node.type === schema.nodes.paragraph || node.type === schema.nodes.heading) { - const inset = (node.attrs.inset ? Number(node.attrs.inset) : 0) + 10; - tr = tr.setNodeMarkup(pos, node.type, { ...node.attrs, inset }, node.marks); - return false; - } - return true; - }); - dispatch?.(tr); - return true; - } - outsetParagraph(state: EditorState, dispatch: any) { - var tr = state.tr; - state.doc.nodesBetween(state.selection.from, state.selection.to, (node, pos, parent, index) => { - if (node.type === schema.nodes.paragraph || node.type === schema.nodes.heading) { - const inset = Math.max(0, (node.attrs.inset ? Number(node.attrs.inset) : 0) - 10); - tr = tr.setNodeMarkup(pos, node.type, { ...node.attrs, inset }, node.marks); - return false; - } - return true; - }); - dispatch?.(tr); - return true; - } - - indentParagraph(state: EditorState, dispatch: any) { - var tr = state.tr; - const heading = false; - state.doc.nodesBetween(state.selection.from, state.selection.to, (node, pos, parent, index) => { - if (node.type === schema.nodes.paragraph || node.type === schema.nodes.heading) { - const nodeval = node.attrs.indent ? Number(node.attrs.indent) : undefined; - const indent = !nodeval ? 25 : nodeval < 0 ? 0 : nodeval + 25; - tr = tr.setNodeMarkup(pos, node.type, { ...node.attrs, indent }, node.marks); - return false; - } - return true; - }); - !heading && dispatch?.(tr); - return true; - } - - hangingIndentParagraph(state: EditorState, dispatch: any) { - var tr = state.tr; - state.doc.nodesBetween(state.selection.from, state.selection.to, (node, pos, parent, index) => { + paragraphSetup(state: EditorState, dispatch: any, field: 'inset' | 'indent', value?: 0 | 10 | -10) { + let { tr } = state; + state.doc.nodesBetween(state.selection.from, state.selection.to, (node, pos) => { if (node.type === schema.nodes.paragraph || node.type === schema.nodes.heading) { - const nodeval = node.attrs.indent ? Number(node.attrs.indent) : undefined; - const indent = !nodeval ? -25 : nodeval > 0 ? 0 : nodeval - 10; - tr = tr.setNodeMarkup(pos, node.type, { ...node.attrs, indent }, node.marks); + const newValue = !value ? + (node.attrs[field] ? 0 : node.attrs[field] + 10) : + Math.max(0, value); // prettier-ignore + tr = tr.setNodeMarkup(pos, node.type, { ...node.attrs, ...(field === 'inset' ? { inset: newValue } : { indent: newValue }) }, node.marks); return false; } return true; @@ -514,7 +440,7 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> { } insertBlockquote(state: EditorState, dispatch: any) { - const path = (state.selection.$from as any).path; + const { path } = state.selection.$from as any; if (path.length > 6 && path[path.length - 6].type === schema.nodes.blockquote) { lift(state, dispatch); } else { @@ -547,13 +473,13 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> { } @action - fillBrush(state: EditorState, dispatch: any) { + fillBrush() { if (!this.view) return; if (!Array.from(this.brushMarks.keys()).length) { - const selected_marks = this.getMarksInSelection(this.view.state); - if (selected_marks.size >= 0) { - this.brushMarks = selected_marks; + const selectedMarks = this.getMarksInSelection(this.view.state); + if (selectedMarks.size >= 0) { + this.brushMarks = selectedMarks; } } else { const { from, to, $from } = this.view.state.selection; @@ -597,9 +523,12 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> { const button = ( <Tooltip title={<div className="dash-tooltip">set hyperlink</div>} placement="bottom"> - <button className="antimodeMenu-button color-preview-button"> - <FontAwesomeIcon icon="link" size="lg" /> - </button> + { + // eslint-disable-next-line jsx-a11y/control-has-associated-label + <button type="button" className="antimodeMenu-button color-preview-button"> + <FontAwesomeIcon icon="link" size="lg" /> + </button> + } </Tooltip> ); @@ -607,21 +536,22 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> { <div className="dropdown link-menu"> <p>Linked to:</p> <input value={link} ref={this._linkToRef} placeholder="Enter URL" onChange={onLinkChange} /> - <button className="make-button" onPointerDown={e => this.makeLinkToURL(link, 'add:right')}> + <button type="button" className="make-button" onPointerDown={() => this.makeLinkToURL(link)}> Apply hyperlink </button> <div className="divider" /> - <button className="remove-button" onPointerDown={e => this.deleteLink()}> + <button type="button" className="remove-button" onPointerDown={() => this.deleteLink()}> Remove link </button> </div> ); - return <ButtonDropdown view={this.view} key={'link button'} button={button} dropdownContent={dropdownContent} openDropdownOnButton={true} link={true} />; + // eslint-disable-next-line no-use-before-define + return <ButtonDropdown view={this.view} key="link button" button={button} dropdownContent={dropdownContent} openDropdownOnButton link />; } async getTextLinkTargetTitle() { - if (!this.view) return; + if (!this.view) return undefined; const node = this.view.state.selection.$from.nodeAfter; const link = node && node.marks.find(m => m.type.name === 'link'); @@ -633,15 +563,15 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> { if (linkclicked) { const linkDoc = await DocServer.GetRefField(linkclicked); if (linkDoc instanceof Doc) { - const link_anchor_1 = await Cast(linkDoc.link_anchor_1, Doc); - const link_anchor_2 = await Cast(linkDoc.link_anchor_2, Doc); - const currentDoc = SelectionManager.Docs.lastElement(); - if (currentDoc && link_anchor_1 && link_anchor_2) { - if (Doc.AreProtosEqual(currentDoc, link_anchor_1)) { - return StrCast(link_anchor_2.title); + const linkAnchor1 = await Cast(linkDoc.link_anchor_1, Doc); + const linkAnchor2 = await Cast(linkDoc.link_anchor_2, Doc); + const currentDoc = DocumentView.Selected().lastElement().Document; + if (currentDoc && linkAnchor1 && linkAnchor2) { + if (Doc.AreProtosEqual(currentDoc, linkAnchor1)) { + return StrCast(linkAnchor2.title); } - if (Doc.AreProtosEqual(currentDoc, link_anchor_2)) { - return StrCast(link_anchor_1.title); + if (Doc.AreProtosEqual(currentDoc, linkAnchor2)) { + return StrCast(linkAnchor1.title); } } } @@ -653,11 +583,12 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> { return link.attrs.title; } } + return undefined; } // TODO: should check for valid URL @undoBatch - makeLinkToURL = (target: string, lcoation: string) => { + makeLinkToURL = (target: string) => { ((this.view as any)?.TextView as FormattedTextBox).makeLinkAnchor(undefined, 'onRadd:rightight', target, target); }; @@ -673,7 +604,7 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> { .filter((aref: any) => aref?.href.indexOf(Doc.localServerPath()) === 0) .forEach((aref: any) => { const anchorId = aref.href.replace(Doc.localServerPath(), '').split('?')[0]; - anchorId && DocServer.GetRefField(anchorId).then(linkDoc => LinkManager.Instance.deleteLink(linkDoc as Doc)); + anchorId && DocServer.GetRefField(anchorId).then(linkDoc => Doc.DeleteLink?.(linkDoc as Doc)); }); } } @@ -736,7 +667,11 @@ export class ButtonDropdown extends ObservableReactComponent<ButtonDropdownProps render() { return ( - <div className="button-dropdown-wrapper" ref={node => (this.ref = node)}> + <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} @@ -747,9 +682,12 @@ export class ButtonDropdown extends ObservableReactComponent<ButtonDropdownProps ) : ( <> {this._props.button} - <button className="dropdown-button antimodeMenu-button" key="antimodebutton" onPointerDown={this.onDropdownClick}> - <FontAwesomeIcon icon="caret-down" size="sm" /> - </button> + { + // eslint-disable-next-line jsx-a11y/control-has-associated-label + <button type="button" className="dropdown-button antimodeMenu-button" key="antimodebutton" onPointerDown={this.onDropdownClick}> + <FontAwesomeIcon icon="caret-down" size="sm" /> + </button> + } </> )} {this.showDropdown ? this._props.dropdownContent : null} @@ -762,10 +700,11 @@ interface RichTextMenuPluginProps { editorProps: any; } export class RichTextMenuPlugin extends React.Component<RichTextMenuPluginProps> { - render() { - return null; - } + // eslint-disable-next-line react/no-unused-class-component-methods update(view: EditorView, lastState: EditorState | undefined) { RichTextMenu.Instance?.updateMenu(view, lastState, this.props.editorProps, (view as any).TextView?.layoutDoc); } + render() { + return null; + } } diff --git a/src/client/views/nodes/formattedText/RichTextRules.ts b/src/client/views/nodes/formattedText/RichTextRules.ts index 42665830f..bf11dfe62 100644 --- a/src/client/views/nodes/formattedText/RichTextRules.ts +++ b/src/client/views/nodes/formattedText/RichTextRules.ts @@ -1,17 +1,17 @@ import { ellipsis, emDash, InputRule, smartQuotes, textblockTypeInputRule } from 'prosemirror-inputrules'; import { NodeSelection, TextSelection } from 'prosemirror-state'; +import { ClientUtils } from '../../../../ClientUtils'; import { Doc, DocListCast, FieldResult, StrListCast } from '../../../../fields/Doc'; import { DocData } from '../../../../fields/DocSymbols'; import { Id } from '../../../../fields/FieldSymbols'; import { List } from '../../../../fields/List'; import { NumCast, StrCast } from '../../../../fields/Types'; import { Utils } from '../../../../Utils'; -import { DocServer } from '../../../DocServer'; -import { Docs, DocUtils } from '../../../documents/Documents'; +import { Docs } from '../../../documents/Documents'; import { CollectionViewType } from '../../../documents/DocumentTypes'; +import { DocUtils } from '../../../documents/DocUtils'; import { CollectionView } from '../../collections/CollectionView'; import { ContextMenu } from '../../ContextMenu'; -import { KeyValueBox } from '../KeyValueBox'; import { FormattedTextBox } from './FormattedTextBox'; import { wrappingInputRule } from './prosemirrorPatches'; import { RichTextMenu } from './RichTextMenu'; @@ -48,13 +48,9 @@ export class RichTextRules { /^A\.\s$/, schema.nodes.ordered_list, // match => { - () => { - return { mapStyle: 'multi', bulletStyle: 1 }; - // return ({ order: +match[1] }) - }, - (match: any, node: any) => { - return node.childCount + node.attrs.order === +match[1]; - }, + () => ({ mapStyle: 'multi', bulletStyle: 1 }), + // return ({ order: +match[1] }) + (match: any, node: any) => node.childCount + node.attrs.order === +match[1], ((type: any) => ({ type: type, attrs: { mapStyle: 'multi', bulletStyle: 1 } })) as any ), @@ -70,7 +66,7 @@ export class RichTextRules { // ``` create code block new InputRule(/^```$/, (state, match, start, end) => { - let $start = state.doc.resolve(start); + const $start = state.doc.resolve(start); if (!$start.node(-1).canReplaceWith($start.index(-1), $start.indexAfter(-1), schema.nodes.code_block)) return null; // this enables text with code blocks to be used as a 'paint' function via a styleprovider button that is added to Docs that have an onPaint script @@ -86,13 +82,13 @@ export class RichTextRules { }), // %<font-size> set the font size - new InputRule(new RegExp(/%([0-9]+)\s$/), (state, match, start, end) => { + new InputRule(/%([0-9]+)\s$/, (state, match, start, end) => { const size = Number(match[1]); return state.tr.deleteRange(start, end).addStoredMark(schema.marks.pFontSize.create({ fontSize: size })); }), - //Create annotation to a field on the text document - new InputRule(new RegExp(/>::$/), (state, match, start, end) => { + // Create annotation to a field on the text document + new InputRule(/>::$/, (state, match, start, end) => { const creator = (doc: Doc) => { const textDoc = this.Document[DocData]; const numInlines = NumCast(textDoc.inlineTextCount); @@ -107,7 +103,7 @@ export class RichTextRules { .insert(start, newNode) .replaceRangeWith(start + 1, end + 2, dashDoc) .insertText(' ', start + 2) - .setStoredMarks([...node.marks, ...(sm ? sm : [])]) + .setStoredMarks([...node.marks, ...(sm || [])]) : this.TextBox.EditorView.state.tr ); }; @@ -117,8 +113,8 @@ export class RichTextRules { return null; }), - //Create annotation to a field on the text document - new InputRule(new RegExp(/>>$/), (state, match, start, end) => { + // Create annotation to a field on the text document + new InputRule(/>>$/, (state, match, start, end) => { const textDoc = this.Document[DocData]; const numInlines = NumCast(textDoc.inlineTextCount); textDoc.inlineTextCount = numInlines + 1; @@ -150,13 +146,13 @@ export class RichTextRules { .insert(start, newNode) .replaceRangeWith(start + 1, end + 1, dashDoc) .insertText(' ', start + 2) - .setStoredMarks([...node.marks, ...(sm ? sm : [])]) + .setStoredMarks([...node.marks, ...(sm || [])]) : state.tr; return replaced; }), // set the First-line indent node type for the selection's paragraph (assumes % was used to initiate an EnteringStyle mode) - new InputRule(new RegExp(/(%d|d)$/), (state, match, start, end) => { + new InputRule(/(%d|d)$/, (state, match, start, end) => { if (!match[0].startsWith('%') && !this.EnteringStyle) return null; const pos = state.doc.resolve(start) as any; for (let depth = pos.path.length / 3 - 1; depth >= 0; depth--) { @@ -171,7 +167,7 @@ export class RichTextRules { }), // set the Hanging indent node type for the current selection's paragraph (assumes % was used to initiate an EnteringStyle mode) - new InputRule(new RegExp(/(%h|h)$/), (state, match, start, end) => { + new InputRule(/(%h|h)$/, (state, match, start, end) => { if (!match[0].startsWith('%') && !this.EnteringStyle) return null; const pos = state.doc.resolve(start) as any; for (let depth = pos.path.length / 3 - 1; depth >= 0; depth--) { @@ -186,11 +182,11 @@ export class RichTextRules { }), // set the Quoted indent node type for the current selection's paragraph (assumes % was used to initiate an EnteringStyle mode) - new InputRule(new RegExp(/(%q|q)$/), (state, match, start, end) => { + new InputRule(/(%q|q)$/, (state, match, start, end) => { if (!match[0].startsWith('%') && !this.EnteringStyle) return null; const pos = state.doc.resolve(start) as any; if (state.selection instanceof NodeSelection && state.selection.node.type === schema.nodes.ordered_list) { - const node = state.selection.node; + const { node } = state.selection; return state.tr.setNodeMarkup(pos.pos, node.type, { ...node.attrs, indent: node.attrs.indent === 30 ? undefined : 30 }); } for (let depth = pos.path.length / 3 - 1; depth >= 0; depth--) { @@ -205,46 +201,43 @@ export class RichTextRules { }), // center justify text - new InputRule(new RegExp(/%\^/), (state, match, start, end) => { + new InputRule(/%\^/, (state, match, start, end) => { const resolved = state.doc.resolve(start) as any; if (resolved?.parent.type.name === 'paragraph') { return state.tr.deleteRange(start, end).setNodeMarkup(resolved.path[resolved.path.length - 4], schema.nodes.paragraph, { ...resolved.parent.attrs, align: 'center' }, resolved.parent.marks); - } else { - const node = resolved.nodeAfter; - const sm = state.storedMarks || undefined; - const replaced = node ? state.tr.replaceRangeWith(start, end, schema.nodes.paragraph.create({ align: 'center' })).setStoredMarks([...node.marks, ...(sm ? sm : [])]) : state.tr; - return replaced.setSelection(new TextSelection(replaced.doc.resolve(end - 2))); } + const node = resolved.nodeAfter; + const sm = state.storedMarks || undefined; + const replaced = node ? state.tr.replaceRangeWith(start, end, schema.nodes.paragraph.create({ align: 'center' })).setStoredMarks([...node.marks, ...(sm || [])]) : state.tr; + return replaced.setSelection(new TextSelection(replaced.doc.resolve(end - 2))); }), // left justify text - new InputRule(new RegExp(/%\[/), (state, match, start, end) => { + new InputRule(/%\[/, (state, match, start, end) => { const resolved = state.doc.resolve(start) as any; if (resolved?.parent.type.name === 'paragraph') { return state.tr.deleteRange(start, end).setNodeMarkup(resolved.path[resolved.path.length - 4], schema.nodes.paragraph, { ...resolved.parent.attrs, align: 'left' }, resolved.parent.marks); - } else { - const node = resolved.nodeAfter; - const sm = state.storedMarks || undefined; - const replaced = node ? state.tr.replaceRangeWith(start, end, schema.nodes.paragraph.create({ align: 'left' })).setStoredMarks([...node.marks, ...(sm ? sm : [])]) : state.tr; - return replaced.setSelection(new TextSelection(replaced.doc.resolve(end - 2))); } + const node = resolved.nodeAfter; + const sm = state.storedMarks || undefined; + const replaced = node ? state.tr.replaceRangeWith(start, end, schema.nodes.paragraph.create({ align: 'left' })).setStoredMarks([...node.marks, ...(sm || [])]) : state.tr; + return replaced.setSelection(new TextSelection(replaced.doc.resolve(end - 2))); }), // right justify text - new InputRule(new RegExp(/%\]/), (state, match, start, end) => { + new InputRule(/%\]/, (state, match, start, end) => { const resolved = state.doc.resolve(start) as any; if (resolved?.parent.type.name === 'paragraph') { return state.tr.deleteRange(start, end).setNodeMarkup(resolved.path[resolved.path.length - 4], schema.nodes.paragraph, { ...resolved.parent.attrs, align: 'right' }, resolved.parent.marks); - } else { - const node = resolved.nodeAfter; - const sm = state.storedMarks || undefined; - const replaced = node ? state.tr.replaceRangeWith(start, end, schema.nodes.paragraph.create({ align: 'right' })).setStoredMarks([...node.marks, ...(sm ? sm : [])]) : state.tr; - return replaced.setSelection(new TextSelection(replaced.doc.resolve(end - 2))); } + const node = resolved.nodeAfter; + const sm = state.storedMarks || undefined; + const replaced = node ? state.tr.replaceRangeWith(start, end, schema.nodes.paragraph.create({ align: 'right' })).setStoredMarks([...node.marks, ...(sm || [])]) : state.tr; + return replaced.setSelection(new TextSelection(replaced.doc.resolve(end - 2))); }), // activate a style by name using prefix '%<color name>' - new InputRule(new RegExp(/%[a-zA-Z_]+$/), (state, match, start, end) => { + new InputRule(/%[a-zA-Z_]+$/, (state, match, start, end) => { const color = match[0].substring(1, match[0].length); const marks = RichTextMenu.Instance?._brushMap.get(color); @@ -259,7 +252,7 @@ export class RichTextRules { } if (marks) { const tr = state.tr.deleteRange(start, end); - return marks ? Array.from(marks).reduce((tr, m) => tr.addStoredMark(m), tr) : tr; + return marks ? Array.from(marks).reduce((tr2, m) => tr2.addStoredMark(m), tr) : tr; } const isValidColor = (strColor: string) => { @@ -269,33 +262,33 @@ export class RichTextRules { }; if (isValidColor(color)) { - return state.tr.deleteRange(start, end).addStoredMark(schema.marks.pFontColor.create({ color: color })); + return state.tr.deleteRange(start, end).addStoredMark(schema.marks.pFontColor.create({ fontColor: color })); } return null; }), // toggle alternate text UI %/ - new InputRule(new RegExp(/%\//), (state, match, start, end) => { + new InputRule(/%\//, (state, match, start, end) => { setTimeout(() => this.TextBox.cycleAlternateText(true)); return state.tr.deleteRange(start, end); }), // stop using active style - new InputRule(new RegExp(/%%$/), (state, match, start, end) => { + new InputRule(/%%$/, (state, match, start, end) => { const tr = state.tr.deleteRange(start, end); const marks = state.tr.selection.$anchor.nodeBefore?.marks; return marks ? Array.from(marks) .filter(m => m.type !== state.schema.marks.user_mark) - .reduce((tr, m) => tr.removeStoredMark(m), tr) + .reduce((tr2, m) => tr2.removeStoredMark(m), tr) : tr; }), // create a hyperlink to a titled document // @(<doctitle>) - new InputRule(new RegExp(/@\(([a-zA-Z_@\.\? \-0-9]+)\)/), (state, match, start, end) => { + new InputRule(/@\(([a-zA-Z_@.? \-0-9]+)\)/, (state, match, start, end) => { const docTitle = match[1]; const prefixLength = '@('.length; if (docTitle) { @@ -315,11 +308,11 @@ export class RichTextRules { teditor.dispatch(tstate.tr.setSelection(new TextSelection(tstate.doc.resolve(selection)))); } }; - const getTitledDoc = (docTitle: string) => { - if (!DocServer.FindDocByTitle(docTitle)) { - Docs.Create.TextDocument('', { title: docTitle, _width: 400, _layout_fitWidth: true, _layout_autoHeight: true }); + const getTitledDoc = (title: string) => { + if (!Doc.FindDocByTitle(title)) { + Docs.Create.TextDocument('', { title: title, _width: 400, _layout_fitWidth: true, _layout_autoHeight: true }); } - const titledDoc = DocServer.FindDocByTitle(docTitle); + const titledDoc = Doc.FindDocByTitle(title); return titledDoc ? Doc.BestEmbedding(titledDoc) : titledDoc; }; const target = getTitledDoc(docTitle); @@ -335,22 +328,31 @@ export class RichTextRules { // [@{this,doctitle,}.fieldKey{:,=,:=,=:=}value] // [@{this,doctitle,}.fieldKey] new InputRule( - new RegExp(/\[(@|@this\.|@[a-zA-Z_\? \-0-9]+\.)([a-zA-Z_\?\-0-9]+)((:|=|:=|=:=)([a-zA-Z,_\(\)\.@\?\+\-\*\/\ 0-9\(\)]*))?\]/), + /\[(@|@this\.|@[a-zA-Z_? \-0-9]+\.)([a-zA-Z_?\-0-9]+)((:|=|:=|=:=)([a-zA-Z,_().@?+\-*/ 0-9()]*))?\]/, (state, match, start, end) => { const docTitle = match[1].substring(1).replace(/\.$/, ''); const fieldKey = match[2]; const assign = match[4] === ':' ? (match[4] = '') : match[4]; const value = match[5]; const dataDoc = value === undefined ? !fieldKey.startsWith('_') : !assign?.startsWith('='); - const getTitledDoc = (docTitle: string) => DocServer.FindDocByTitle(docTitle); + const getTitledDoc = (title: string) => Doc.FindDocByTitle(title); // if the value has commas assume its an array (unless it's part of a chat gpt call indicated by '((' ) if (value?.includes(',') && !value.startsWith('((')) { const values = value.split(','); const strs = values.some(v => !v.match(/^[-]?[0-9.]$/)); this.Document[DocData][fieldKey] = strs ? new List<string>(values) : new List<number>(values.map(v => Number(v))); } else if (value) { - KeyValueBox.SetField(this.Document, fieldKey, assign + value, Doc.IsDataProto(this.Document) ? true : undefined, assign.includes(":=") ? undefined: - (gptval: FieldResult) => (dataDoc ? this.Document[DocData]:this.Document)[fieldKey] = gptval as string ); // prettier-ignore + Doc.SetField( + this.Document, + fieldKey, + assign + value, + Doc.IsDataProto(this.Document) ? true : undefined, + assign.includes(':=') + ? undefined + : (gptval: FieldResult) => { + (dataDoc ? this.Document[DocData] : this.Document)[fieldKey] = gptval as string; + } + ); if (fieldKey === this.TextBox.fieldKey) return this.TextBox.EditorView!.state.tr; } const target = docTitle ? getTitledDoc(docTitle) : undefined; @@ -361,9 +363,9 @@ export class RichTextRules { ), // pass the contents between '((' and '))' to chatGPT and append the result - new InputRule(new RegExp(/(^|[^=])(\(\(.*\)\))$/), (state, match, start, end) => { - var count = 0; // ignore first return value which will be the notation that chat is pending a result - KeyValueBox.SetField(this.Document, '', match[2], false, (gptval: FieldResult) => { + new InputRule(/(^|[^=])(\(\(.*\)\))$/, (state, match, start, end) => { + let count = 0; // ignore first return value which will be the notation that chat is pending a result + Doc.SetField(this.Document, '', match[2], false, (gptval: FieldResult) => { if (count) { const tr = this.TextBox.EditorView?.state.tr.insertText(' ' + (gptval as string)); tr && this.TextBox.EditorView?.dispatch(tr.setSelection(new TextSelection(tr.doc.resolve(end + 2), tr.doc.resolve(end + 2 + (gptval as string).length)))); @@ -376,7 +378,7 @@ export class RichTextRules { // create a text display of a metadata field on this or another document, or create a hyperlink portal to another document // @(wiki:title) - new InputRule(new RegExp(/@\(wiki:([a-zA-Z_@:\.\?\-0-9 ]+)\)$/), (state, match, start, end) => { + new InputRule(/@\(wiki:([a-zA-Z_@:.?\-0-9 ]+)\)$/, (state, match, start, end) => { const title = match[1].trim().replace(/ /g, '_'); this.TextBox.EditorView?.dispatch(state.tr.setSelection(new TextSelection(state.doc.resolve(start), state.doc.resolve(end)))); @@ -392,7 +394,7 @@ export class RichTextRules { // create an inline equation node // %eq - new InputRule(new RegExp(/%eq/), (state, match, start, end) => { + new InputRule(/%eq/, (state, match, start, end) => { const fieldKey = 'math' + Utils.GenerateGuid(); this.TextBox.dataDoc[fieldKey] = 'y='; const tr = state.tr.setSelection(new TextSelection(state.tr.doc.resolve(end - 3), state.tr.doc.resolve(end))).replaceSelectionWith(schema.nodes.equation.create({ fieldKey })); @@ -400,10 +402,10 @@ export class RichTextRules { }), // create an inline view of a tag stored under the '#' field - new InputRule(new RegExp(/#([a-zA-Z_\-]+[a-zA-Z_\-0-9]*)\s$/), (state, match, start, end) => { + new InputRule(/#([a-zA-Z_-]+[a-zA-Z_\-0-9]*)\s$/, (state, match, start, end) => { const tag = match[1]; if (!tag) return state.tr; - //this.Document[DocData]['#' + tag] = '#' + tag; + // this.Document[DocData]['#' + tag] = '#' + tag; const tags = StrListCast(this.Document[DocData].tags); if (!tags.includes(tag)) { tags.push(tag); @@ -417,29 +419,25 @@ export class RichTextRules { }), // # heading - textblockTypeInputRule(new RegExp(/^(#{1,6})\s$/), schema.nodes.heading, match => { - return { level: match[1].length }; - }), + textblockTypeInputRule(/^(#{1,6})\s$/, schema.nodes.heading, match => ({ level: match[1].length })), // set the Todo user-tag on the current selection (assumes % was used to initiate an EnteringStyle mode) - new InputRule(new RegExp(/[ti!x]$/), (state, match, start, end) => { + new InputRule(/[ti!x]$/, (state, match, start, end) => { if (state.selection.to === state.selection.from || !this.EnteringStyle) return null; const tag = match[0] === 't' ? 'todo' : match[0] === 'i' ? 'ignore' : match[0] === 'x' ? 'disagree' : match[0] === '!' ? 'important' : '??'; const node = (state.doc.resolve(start) as any).nodeAfter; if (node?.marks.findIndex((m: any) => m.type === schema.marks.user_tag) !== -1) return state.tr.removeMark(start, end, schema.marks.user_tag); - if (node?.marks.findIndex((m: any) => m.type === schema.marks.user_mark) !== -1) { - } return node ? state.tr .removeMark(start, end, schema.marks.user_mark) - .addMark(start, end, schema.marks.user_mark.create({ userid: Doc.CurrentUserEmail, modified: Math.floor(Date.now() / 1000) })) - .addMark(start, end, schema.marks.user_tag.create({ userid: Doc.CurrentUserEmail, tag: tag, modified: Math.round(Date.now() / 1000 / 60) })) + .addMark(start, end, schema.marks.user_mark.create({ userid: ClientUtils.CurrentUserEmail(), modified: Math.floor(Date.now() / 1000) })) + .addMark(start, end, schema.marks.user_tag.create({ userid: ClientUtils.CurrentUserEmail(), tag: tag, modified: Math.round(Date.now() / 1000 / 60) })) : state.tr; }), - new InputRule(new RegExp(/%\(/), (state, match, start, end) => { + new InputRule(/%\(/, (state, match, start, end) => { const node = (state.doc.resolve(start) as any).nodeAfter; const sm = state.storedMarks?.slice() || []; const mark = state.schema.marks.summarizeInclusive.create(); @@ -452,9 +450,7 @@ export class RichTextRules { return replaced.setSelection(new TextSelection(replaced.doc.resolve(end))).setStoredMarks([...node.marks, ...sm]); }), - new InputRule(new RegExp(/%\)/), (state, match, start, end) => { - return state.tr.deleteRange(start, end).removeStoredMark(state.schema.marks.summarizeInclusive.create()); - }), + new InputRule(/%\)/, (state, match, start, end) => state.tr.deleteRange(start, end).removeStoredMark(state.schema.marks.summarizeInclusive.create())), ], }; } diff --git a/src/client/views/nodes/formattedText/SummaryView.tsx b/src/client/views/nodes/formattedText/SummaryView.tsx index 7ec296ed2..238267f6e 100644 --- a/src/client/views/nodes/formattedText/SummaryView.tsx +++ b/src/client/views/nodes/formattedText/SummaryView.tsx @@ -3,6 +3,15 @@ import { Fragment, Node, Slice } from 'prosemirror-model'; import * as ReactDOM from 'react-dom/client'; import * as React from 'react'; +interface ISummaryView {} +// currently nothing needs to be rendered for the internal view of a summary. +// eslint-disable-next-line react/prefer-stateless-function +export class SummaryViewInternal extends React.Component<ISummaryView> { + render() { + return null; + } +} + // an elidable textblock that collapses when its '<-' is clicked and expands when its '...' anchor is clicked. // this node actively edits prosemirror (as opposed to just changing how things are rendered) and thus doesn't // really need a react view. However, it would be cleaner to figure out how to do this just as a react rendering @@ -12,11 +21,10 @@ export class SummaryView { root: any; constructor(node: any, view: any, getPos: any) { - const self = this; this.dom = document.createElement('span'); this.dom.className = this.className(node.attrs.visibility); - this.dom.onpointerdown = function (e: any) { - self.onPointerDown(e, node, view, getPos); + this.dom.onpointerdown = (e: any) => { + this.onPointerDown(e, node, view, getPos); }; this.dom.onkeypress = function (e: any) { e.stopPropagation(); @@ -32,8 +40,8 @@ export class SummaryView { }; const js = node.toJSON; - node.toJSON = function () { - return js.apply(this, arguments); + node.toJSON = function (...args: any[]) { + return js.apply(this, args); }; this.root = ReactDOM.createRoot(this.dom); @@ -54,7 +62,8 @@ export class SummaryView { const visited = new Set(); for (let i: number = start + 1; i < view.state.doc.nodeSize - 1; i++) { let skip = false; - view.state.doc.nodesBetween(start, i, (node: Node, pos: number, parent: Node, index: number) => { + // eslint-disable-next-line no-loop-func + view.state.doc.nodesBetween(start, i, (node: Node /* , pos: number, parent: Node, index: number */) => { if (node.isLeaf && !visited.has(node) && !skip) { if (node.marks.find((m: any) => m.type === mtype || m.type === mtypeInc)) { visited.add(node); @@ -87,11 +96,3 @@ export class SummaryView { this.dom.className = this.className(visible); }; } - -interface ISummaryView {} -// currently nothing needs to be rendered for the internal view of a summary. -export class SummaryViewInternal extends React.Component<ISummaryView> { - render() { - return <> </>; - } -} diff --git a/src/client/views/nodes/formattedText/marks_rts.ts b/src/client/views/nodes/formattedText/marks_rts.ts index ccf7de4a1..6e1f325cf 100644 --- a/src/client/views/nodes/formattedText/marks_rts.ts +++ b/src/client/views/nodes/formattedText/marks_rts.ts @@ -1,6 +1,5 @@ -import * as React from 'react'; -import { DOMOutputSpec, Fragment, MarkSpec, Node, NodeSpec, Schema, Slice } from 'prosemirror-model'; -import { Doc } from '../../../../fields/Doc'; +import { DOMOutputSpec, MarkSpec } from 'prosemirror-model'; +import { ClientUtils } from '../../../../ClientUtils'; import { Utils } from '../../../../Utils'; const emDOM: DOMOutputSpec = ['em', 0]; @@ -13,7 +12,7 @@ export const marks: { [index: string]: MarkSpec } = { attrs: { id: { default: '' }, }, - toDOM(node: any) { + toDOM() { return ['div', { className: 'dummy' }, 0]; }, }, @@ -45,7 +44,7 @@ export const marks: { [index: string]: MarkSpec } = { toDOM(node: any) { const targethrefs = node.attrs.allAnchors.reduce((p: string, item: { href: string; title: string; anchorId: string }) => (p ? p + ' ' + item.href : item.href), ''); const anchorids = node.attrs.allAnchors.reduce((p: string, item: { href: string; title: string; anchorId: string }) => (p ? p + ' ' + item.anchorId : item.anchorId), ''); - return ['a', { id: Utils.GenerateGuid(), class: anchorids, 'data-targethrefs': targethrefs, /*'data-noPreview': 'true', */ 'data-linkdoc': node.attrs.linkDoc, title: node.attrs.title, style: `background: lightBlue` }, 0]; + return ['a', { id: Utils.GenerateGuid(), class: anchorids, 'data-targethrefs': targethrefs, /* 'data-noPreview': 'true', */ 'data-linkdoc': node.attrs.linkDoc, title: node.attrs.title, style: `background: lightBlue` }, 0]; }, }, noAutoLinkAnchor: { @@ -61,7 +60,7 @@ export const marks: { [index: string]: MarkSpec } = { }, }, ], - toDOM(node: any) { + toDOM() { return ['span', { 'data-noAutoLink': 'true' }, 0]; }, }, @@ -128,29 +127,29 @@ export const marks: { [index: string]: MarkSpec } = { /* FONTS */ pFontFamily: { - attrs: { family: { default: '' } }, + attrs: { fontFamily: { default: '' } }, parseDOM: [ { tag: 'span', getAttrs(dom: any) { const cstyle = getComputedStyle(dom); if (cstyle.font) { - if (cstyle.font.indexOf('Times New Roman') !== -1) return { family: 'Times New Roman' }; - if (cstyle.font.indexOf('Arial') !== -1) return { family: 'Arial' }; - if (cstyle.font.indexOf('Georgia') !== -1) return { family: 'Georgia' }; - if (cstyle.font.indexOf('Comic Sans') !== -1) return { family: 'Comic Sans MS' }; - if (cstyle.font.indexOf('Tahoma') !== -1) return { family: 'Tahoma' }; - if (cstyle.font.indexOf('Crimson') !== -1) return { family: 'Crimson Text' }; + if (cstyle.font.indexOf('Times New Roman') !== -1) return { fontFamily: 'Times New Roman' }; + if (cstyle.font.indexOf('Arial') !== -1) return { fontFamily: 'Arial' }; + if (cstyle.font.indexOf('Georgia') !== -1) return { fontFamily: 'Georgia' }; + if (cstyle.font.indexOf('Comic Sans') !== -1) return { fontFamily: 'Comic Sans MS' }; + if (cstyle.font.indexOf('Tahoma') !== -1) return { fontFamily: 'Tahoma' }; + if (cstyle.font.indexOf('Crimson') !== -1) return { fontFamily: 'Crimson Text' }; } - return { family: '' }; + return { fontFamily: '' }; }, }, ], - toDOM: node => (node.attrs.family ? ['span', { style: `font-family: "${node.attrs.family}";` }] : ['span', 0]), + toDOM: node => (node.attrs.fontFamily ? ['span', { style: `font-family: "${node.attrs.fontFamily}";` }] : ['span', 0]), }, // :: MarkSpec Coloring on text. Has `color` attribute that defined the color of the marked text. pFontColor: { - attrs: { color: { default: '' } }, + attrs: { fontColor: { default: '' } }, inclusive: true, parseDOM: [ { @@ -160,24 +159,24 @@ export const marks: { [index: string]: MarkSpec } = { }, }, ], - toDOM: node => (node.attrs.color ? ['span', { style: 'color:' + node.attrs.color }] : ['span', 0]), + toDOM: node => (node.attrs.fontColor ? ['span', { style: 'color:' + node.attrs.fontColor }] : ['span', 0]), }, - marker: { + pFontHighlight: { attrs: { - highlight: { default: 'transparent' }, + fontHighlight: { default: 'transparent' }, }, inclusive: true, parseDOM: [ { tag: 'span', getAttrs(dom: any) { - return { highlight: dom.getAttribute('backgroundColor') }; + return { fontHighlight: dom.getAttribute('background-color') }; }, }, ], toDOM(node: any) { - return node.attrs.highlight ? ['span', { style: 'background-color:' + node.attrs.highlight }] : ['span', { style: 'background-color: transparent' }]; + return node.attrs.fontHighlight ? ['span', { style: 'background-color:' + node.attrs.fontHighlight }] : ['span', { style: 'background-color: transparent' }]; }, }, @@ -336,7 +335,7 @@ export const marks: { [index: string]: MarkSpec } = { const min = Math.round(node.attrs.modified / 60); const hr = Math.round(min / 60); const day = Math.round(hr / 60 / 24); - const remote = node.attrs.userid !== Doc.CurrentUserEmail ? ' UM-remote' : ''; + const remote = node.attrs.userid !== ClientUtils.CurrentUserEmail() ? ' UM-remote' : ''; return ['span', { class: 'UM-' + uid + remote + ' UM-min-' + min + ' UM-hr-' + hr + ' UM-day-' + day }, 0]; }, }, diff --git a/src/client/views/nodes/formattedText/nodes_rts.ts b/src/client/views/nodes/formattedText/nodes_rts.ts index 62b8b03d6..5bf942218 100644 --- a/src/client/views/nodes/formattedText/nodes_rts.ts +++ b/src/client/views/nodes/formattedText/nodes_rts.ts @@ -2,16 +2,17 @@ import { DOMOutputSpec, Node, NodeSpec } from 'prosemirror-model'; import { listItem, orderedList } from 'prosemirror-schema-list'; import { ParagraphNodeSpec, toParagraphDOM, getParagraphNodeAttrs } from './ParagraphNodeSpec'; import { DocServer } from '../../../DocServer'; -import { Doc, Field } from '../../../../fields/Doc'; +import { Doc, Field, FieldType } from '../../../../fields/Doc'; +import { schema } from './schema_rts'; -const blockquoteDOM: DOMOutputSpec = ['blockquote', 0], - hrDOM: DOMOutputSpec = ['hr'], - preDOM: DOMOutputSpec = ['pre', ['code', 0]], - brDOM: DOMOutputSpec = ['br'], - ulDOM: DOMOutputSpec = ['ul', 0]; +const blockquoteDOM: DOMOutputSpec = ['blockquote', 0]; +const hrDOM: DOMOutputSpec = ['hr']; +const preDOM: DOMOutputSpec = ['pre', ['code', 0]]; +const brDOM: DOMOutputSpec = ['br']; +// const ulDOM: DOMOutputSpec = ['ul', 0]; -function formatAudioTime(time: number) { - time = Math.round(time); +function formatAudioTime(timeIn: number) { + const time = Math.round(timeIn); const hours = Math.floor(time / 60 / 60); const minutes = Math.floor(time / 60) - hours * 60; const seconds = time % 60; @@ -266,7 +267,7 @@ export const nodes: { [index: string]: NodeSpec } = { hideValue: { default: false }, editable: { default: true }, }, - leafText: node => Field.toString((DocServer.GetCachedRefField(node.attrs.docId as string) as Doc)?.[node.attrs.fieldKey as string] as Field), + leafText: node => Field.toString((DocServer.GetCachedRefField(node.attrs.docId as string) as Doc)?.[node.attrs.fieldKey as string] as FieldType), group: 'inline', draggable: false, toDOM(node) { @@ -353,7 +354,7 @@ export const nodes: { [index: string]: NodeSpec } = { }, { style: 'list-style-type=disc', - getAttrs(dom: any) { + getAttrs() { return { mapStyle: 'bullet' }; }, }, @@ -373,10 +374,10 @@ export const nodes: { [index: string]: NodeSpec } = { ], toDOM(node: Node) { const map = node.attrs.bulletStyle ? node.attrs.mapStyle + node.attrs.bulletStyle : ''; - const fhigh = (found => (found ? `background-color: ${found};` : ''))(node.marks.find(m => m.type.name === 'marker')?.attrs.highlight); - const fsize = (found => (found ? `font-size: ${found};` : ''))(node.marks.find(m => m.type.name === 'pFontSize')?.attrs.fontSize); - const ffam = (found => (found ? `font-family: ${found};` : ''))(node.marks.find(m => m.type.name === 'pFontFamily')?.attrs.family); - const fcol = (found => (found ? `color: ${found};` : ''))(node.marks.find(m => m.type.name === 'pFontColor')?.attrs.color); + const fhigh = (found => (found ? `background-color: ${found};` : ''))(node.marks.find(m => m.type === schema.marks.pFontHighlight)?.attrs.fontHighlight); + const fsize = (found => (found ? `font-size: ${found};` : ''))(node.marks.find(m => m.type === schema.marks.pFontSize)?.attrs.fontSize); + const ffam = (found => (found ? `font-family: ${found};` : ''))(node.marks.find(m => m.type === schema.marks.pFontFamily)?.attrs.fontFamily); + const fcol = (found => (found ? `color: ${found};` : ''))(node.marks.find(m => m.type === schema.marks.pFontColor)?.attrs.fontColor); const marg = node.attrs.indent ? `margin-left: ${node.attrs.indent};` : ''; if (node.attrs.mapStyle === 'bullet') { return [ @@ -421,10 +422,10 @@ export const nodes: { [index: string]: NodeSpec } = { }, ], toDOM(node: Node) { - const fhigh = (found => (found ? `background-color: ${found};` : ''))(node.marks.find(m => m.type.name === 'marker')?.attrs.highlight); - const fsize = (found => (found ? `font-size: ${found};` : ''))(node.marks.find(m => m.type.name === 'pFontSize')?.attrs.fontSize); - const ffam = (found => (found ? `font-family: ${found};` : ''))(node.marks.find(m => m.type.name === 'pFontFamily')?.attrs.family); - const fcol = (found => (found ? `color: ${found};` : ''))(node.marks.find(m => m.type.name === 'pFontColor')?.attrs.color); + const fhigh = (found => (found ? `background-color: ${found};` : ''))(node.marks.find(m => m.type === schema.marks.pFontHighlight)?.attrs.fontHighlight); + const fsize = (found => (found ? `font-size: ${found};` : ''))(node.marks.find(m => m.type === schema.marks.pFontSize)?.attrs.fontSize); + const ffam = (found => (found ? `font-family: ${found};` : ''))(node.marks.find(m => m.type === schema.marks.pFontFamily)?.attrs.fontFamily); + const fcol = (found => (found ? `color: ${found};` : ''))(node.marks.find(m => m.type === schema.marks.pFontColor)?.attrs.fontColor); const map = node.attrs.bulletStyle ? node.attrs.mapStyle + node.attrs.bulletStyle : ''; return [ 'li', diff --git a/src/client/views/nodes/generativeFill/GenerativeFill.tsx b/src/client/views/nodes/generativeFill/GenerativeFill.tsx index a485ea4c3..91b0ebd5c 100644 --- a/src/client/views/nodes/generativeFill/GenerativeFill.tsx +++ b/src/client/views/nodes/generativeFill/GenerativeFill.tsx @@ -1,33 +1,39 @@ +/* eslint-disable jsx-a11y/label-has-associated-control */ +/* eslint-disable jsx-a11y/no-noninteractive-element-interactions */ +/* eslint-disable jsx-a11y/img-redundant-alt */ +/* eslint-disable jsx-a11y/click-events-have-key-events */ +/* eslint-disable react/function-component-definition */ import { Checkbox, FormControlLabel, Slider, TextField } from '@mui/material'; import { IconButton } from 'browndash-components'; +import * as React from 'react'; import { useEffect, useRef, useState } from 'react'; import { CgClose } from 'react-icons/cg'; import { IoMdRedo, IoMdUndo } from 'react-icons/io'; +import { ClientUtils } from '../../../../ClientUtils'; import { Doc, DocListCast } from '../../../../fields/Doc'; import { List } from '../../../../fields/List'; import { NumCast } from '../../../../fields/Types'; -import { Utils } from '../../../../Utils'; -import { Docs, DocUtils } from '../../../documents/Documents'; import { Networking } from '../../../Network'; -import { DocumentManager } from '../../../util/DocumentManager'; +import { DocUtils } from '../../../documents/DocUtils'; +import { Docs } from '../../../documents/Documents'; import { CollectionDockingView } from '../../collections/CollectionDockingView'; import { CollectionFreeFormView } from '../../collections/collectionFreeForm'; -import { OpenWhereMod } from '../DocumentView'; -import { ImageBox, ImageEditorData } from '../ImageBox'; +import { ImageEditorData } from '../ImageBox'; +import { OpenWhereMod } from '../OpenWhere'; import './GenerativeFill.scss'; import Buttons from './GenerativeFillButtons'; import { BrushHandler } from './generativeFillUtils/BrushHandler'; -import { activeColor, canvasSize, eraserColor, freeformRenderSize, newCollectionSize, offsetDistanceY, offsetX } from './generativeFillUtils/generativeFillConstants'; -import { CursorData, ImageDimensions, Point } from './generativeFillUtils/generativeFillInterfaces'; import { APISuccess, ImageUtility } from './generativeFillUtils/ImageHandler'; import { PointerHandler } from './generativeFillUtils/PointerHandler'; -import * as React from 'react'; +import { activeColor, canvasSize, eraserColor, freeformRenderSize, newCollectionSize, offsetDistanceY, offsetX } from './generativeFillUtils/generativeFillConstants'; +import { CursorData, ImageDimensions, Point } from './generativeFillUtils/generativeFillInterfaces'; +import { DocumentView } from '../DocumentView'; -enum BrushStyle { - ADD, - SUBTRACT, - MARQUEE, -} +// enum BrushStyle { +// ADD, +// SUBTRACT, +// MARQUEE, +// } interface GenerativeFillProps { imageEditorOpen: boolean; @@ -52,7 +58,7 @@ const GenerativeFill = ({ imageEditorOpen, imageEditorSource, imageRootDoc, addD // format: array of [image source, corresponding image Doc] const [edits, setEdits] = useState<(string | Doc)[][]>([]); const [edited, setEdited] = useState(false); - const [brushStyle, setBrushStyle] = useState<BrushStyle>(BrushStyle.ADD); + // const [brushStyle] = useState<BrushStyle>(BrushStyle.ADD); const [input, setInput] = useState(''); const [loading, setLoading] = useState(false); const [canvasDims, setCanvasDims] = useState<ImageDimensions>({ @@ -98,8 +104,7 @@ const GenerativeFill = ({ imageEditorOpen, imageEditorSource, imageRootDoc, addD if (!ctx || !currImg.current || !canvasRef.current) return; const target = redoStack.current[redoStack.current.length - 1]; - if (!target) { - } else { + if (target) { undoStack.current = [...undoStack.current, canvasRef.current?.toDataURL()]; const img = new Image(); img.src = target; @@ -131,11 +136,11 @@ const GenerativeFill = ({ imageEditorOpen, imageEditorSource, imageRootDoc, addD setIsBrushing(true); const { x, y } = PointerHandler.getPointRelativeToElement(canvas, e, canvasScale); - BrushHandler.brushCircleOverlay(x, y, cursorData.width / 2 / canvasScale, ctx, eraserColor, brushStyle === BrushStyle.SUBTRACT); + BrushHandler.brushCircleOverlay(x, y, cursorData.width / 2 / canvasScale, ctx, eraserColor /* , brushStyle === BrushStyle.SUBTRACT */); }; // stop brushing, push to undo stack - const handlePointerUp = (e: React.PointerEvent) => { + const handlePointerUp = () => { const ctx = ImageUtility.getCanvasContext(canvasBackgroundRef); if (!ctx) return; if (!isBrushing) return; @@ -144,11 +149,11 @@ const GenerativeFill = ({ imageEditorOpen, imageEditorSource, imageRootDoc, addD // handles brushing on pointer movement useEffect(() => { - if (!isBrushing) return; + if (!isBrushing) return undefined; const canvas = canvasRef.current; - if (!canvas) return; + if (!canvas) return undefined; const ctx = ImageUtility.getCanvasContext(canvasRef); - if (!ctx) return; + if (!ctx) return undefined; const handlePointerMove = (e: PointerEvent) => { const currPoint = PointerHandler.getPointRelativeToElement(canvas, e, canvasScale); @@ -156,7 +161,7 @@ const GenerativeFill = ({ imageEditorOpen, imageEditorSource, imageRootDoc, addD x: currPoint.x - e.movementX / canvasScale, y: currPoint.y - e.movementY / canvasScale, }; - BrushHandler.createBrushPathOverlay(lastPoint, currPoint, cursorData.width / 2 / canvasScale, ctx, eraserColor, brushStyle === BrushStyle.SUBTRACT); + BrushHandler.createBrushPathOverlay(lastPoint, currPoint, cursorData.width / 2 / canvasScale, ctx, eraserColor /* , brushStyle === BrushStyle.SUBTRACT */); }; drawingAreaRef.current?.addEventListener('pointermove', handlePointerMove); @@ -290,12 +295,13 @@ const GenerativeFill = ({ imageEditorOpen, imageEditorSource, imageRootDoc, addD _height: newCollectionSize, title: 'Image edit collection', }); - DocUtils.MakeLink(imageRootDoc, newCollectionRef.current, { link_relationship: 'Image Edit Version History', link_displayLine: false }); + DocUtils.MakeLink(imageRootDoc, newCollectionRef.current, { link_relationship: 'Image Edit Version History' }); // opening new tab CollectionDockingView.AddSplit(newCollectionRef.current, OpenWhereMod.right); // add the doc to the main freeform + // eslint-disable-next-line no-use-before-define await createNewImgDoc(originalImg.current, true); } } else { @@ -309,12 +315,14 @@ const GenerativeFill = ({ imageEditorOpen, imageEditorSource, imageRootDoc, addD const imgUrls = await Promise.all(urls.map(url => ImageUtility.convertImgToCanvasUrl(url, canvasDims.width, canvasDims.height))); const imgRes = await Promise.all( imgUrls.map(async url => { + // eslint-disable-next-line no-use-before-define const saveRes = await onSave(url); return [url, saveRes as Doc]; }) ); setEdits(imgRes); const image = new Image(); + // eslint-disable-next-line prefer-destructuring image.src = imgUrls[0]; ImageUtility.drawImgToCanvas(image, canvasRef, canvasDims.width, canvasDims.height); currImg.current = image; @@ -332,7 +340,7 @@ const GenerativeFill = ({ imageEditorOpen, imageEditorSource, imageRootDoc, addD const startY = NumCast(parentDoc.current.y); const children = DocListCast(parentDoc.current.gen_fill_children); const len = children.length; - let initialYPositions: number[] = []; + const initialYPositions: number[] = []; for (let i = 0; i < len; i++) { initialYPositions.push(startY + i * offsetDistanceY); } @@ -347,10 +355,10 @@ const GenerativeFill = ({ imageEditorOpen, imageEditorSource, imageRootDoc, addD // creates a new image document and returns its reference const createNewImgDoc = async (img: HTMLImageElement, firstDoc: boolean): Promise<Doc | undefined> => { - if (!imageRootDoc) return; - const src = img.src; + if (!imageRootDoc) return undefined; + const { src } = img; const [result] = await Networking.PostToServer('/uploadRemoteImage', { sources: [src] }); - const source = Utils.prepend(result.accessPaths.agnostic.client); + const source = ClientUtils.prepend(result.accessPaths.agnostic.client); if (firstDoc) { const x = 0; @@ -370,51 +378,51 @@ const GenerativeFill = ({ imageEditorOpen, imageEditorSource, imageRootDoc, addD } parentDoc.current = newImg; return newImg; - } else { - if (!parentDoc.current) return; - const x = NumCast(parentDoc.current.x) + freeformRenderSize + offsetX; - const initialY = 0; - - const newImg = Docs.Create.ImageDocument(source, { - x: x, - y: initialY, - _height: freeformRenderSize, - _width: freeformRenderSize, - data_nativeWidth: result.nativeWidth, - data_nativeHeight: result.nativeHeight, - }); + } + if (!parentDoc.current) return undefined; + const x = NumCast(parentDoc.current.x) + freeformRenderSize + offsetX; + const initialY = 0; + + const newImg = Docs.Create.ImageDocument(source, { + x: x, + y: initialY, + _height: freeformRenderSize, + _width: freeformRenderSize, + data_nativeWidth: result.nativeWidth, + data_nativeHeight: result.nativeHeight, + }); - const parentList = DocListCast(parentDoc.current.gen_fill_children); - if (parentList.length > 0) { - parentList.push(newImg); - parentDoc.current.gen_fill_children = new List<Doc>(parentList); - } else { - parentDoc.current.gen_fill_children = new List<Doc>([newImg]); - } + const parentList = DocListCast(parentDoc.current.gen_fill_children); + if (parentList.length > 0) { + parentList.push(newImg); + parentDoc.current.gen_fill_children = new List<Doc>(parentList); + } else { + parentDoc.current.gen_fill_children = new List<Doc>([newImg]); + } - DocUtils.MakeLink(parentDoc.current, newImg, { link_relationship: `Image edit; Prompt: ${input}`, link_displayLine: true }); - adjustImgPositions(); + DocUtils.MakeLink(parentDoc.current, newImg, { link_relationship: `Image edit; Prompt: ${input}` }); + adjustImgPositions(); - if (isNewCollection && newCollectionRef.current) { - Doc.AddDocToList(newCollectionRef.current, undefined, newImg); - } else { - addDoc?.(newImg); - } - return newImg; + if (isNewCollection && newCollectionRef.current) { + Doc.AddDocToList(newCollectionRef.current, undefined, newImg); + } else { + addDoc?.(newImg); } + return newImg; }; // Saves an image to the collection const onSave = async (src: string) => { const img = new Image(); img.src = src; - if (!currImg.current || !originalImg.current || !imageRootDoc) return; + if (!currImg.current || !originalImg.current || !imageRootDoc) return undefined; try { const res = await createNewImgDoc(img, false); return res; } catch (err) { console.log(err); } + return undefined; }; // Closes the editor view @@ -422,7 +430,7 @@ const GenerativeFill = ({ imageEditorOpen, imageEditorSource, imageRootDoc, addD ImageEditorData.Open = false; ImageEditorData.Source = ''; if (newCollectionRef.current) { - DocumentManager.Instance.AddViewRenderedCb(newCollectionRef.current, dv => (dv.ComponentView as CollectionFreeFormView)?.fitContentOnce()); + DocumentView.addViewRenderedCb(newCollectionRef.current, dv => (dv.ComponentView as CollectionFreeFormView)?.fitContentOnce()); } setEdits([]); }; @@ -443,12 +451,12 @@ const GenerativeFill = ({ imageEditorOpen, imageEditorSource, imageRootDoc, addD }} /> } - label={'Create New Collection'} + label="Create New Collection" labelPlacement="end" sx={{ whiteSpace: 'nowrap' }} /> <Buttons getEdit={getEdit} loading={loading} onReset={handleReset} /> - <IconButton color={activeColor} tooltip="close" icon={<CgClose size={'16px'} />} onClick={handleViewClose} /> + <IconButton color={activeColor} tooltip="close" icon={<CgClose size="16px" />} onClick={handleViewClose} /> </div> </div> {/* Main canvas for editing */} @@ -469,7 +477,7 @@ const GenerativeFill = ({ imageEditorOpen, imageEditorSource, imageRootDoc, addD width: cursorData.width, height: cursorData.width, }}> - <div className="innerPointer"></div> + <div className="innerPointer" /> </div> {/* Icons */} <div className="iconContainer"> @@ -519,11 +527,13 @@ const GenerativeFill = ({ imageEditorOpen, imageEditorSource, imageRootDoc, addD /> </div> </div> - {/* Edits thumbnails*/} + {/* Edits thumbnails */} <div className="editsBox"> {edits.map((edit, i) => ( <img + // eslint-disable-next-line react/no-array-index-key key={i} + alt="image edits" width={75} src={edit[0] as string} style={{ cursor: 'pointer' }} @@ -552,6 +562,7 @@ const GenerativeFill = ({ imageEditorOpen, imageEditorSource, imageRootDoc, addD Original </label> <img + alt="image stuff" width={75} src={originalImg.current?.src} style={{ cursor: 'pointer' }} diff --git a/src/client/views/nodes/generativeFill/GenerativeFillButtons.tsx b/src/client/views/nodes/generativeFill/GenerativeFillButtons.tsx index 185ba2280..d1f68ee0e 100644 --- a/src/client/views/nodes/generativeFill/GenerativeFillButtons.tsx +++ b/src/client/views/nodes/generativeFill/GenerativeFillButtons.tsx @@ -1,9 +1,9 @@ import './GenerativeFillButtons.scss'; import * as React from 'react'; import ReactLoading from 'react-loading'; -import { activeColor } from './generativeFillUtils/generativeFillConstants'; import { Button, IconButton, Type } from 'browndash-components'; import { AiOutlineInfo } from 'react-icons/ai'; +import { activeColor } from './generativeFillUtils/generativeFillConstants'; interface ButtonContainerProps { getEdit: () => Promise<void>; @@ -11,7 +11,7 @@ interface ButtonContainerProps { onReset: () => void; } -const Buttons = ({ loading, getEdit, onReset }: ButtonContainerProps) => { +function Buttons({ loading, getEdit, onReset }: ButtonContainerProps) { return ( <div className="generativeFillBtnContainer"> <Button text="RESET" type={Type.PRIM} color={activeColor} onClick={onReset} /> @@ -20,7 +20,7 @@ const Buttons = ({ loading, getEdit, onReset }: ButtonContainerProps) => { text="GET EDITS" type={Type.TERT} color={activeColor} - icon={<ReactLoading type="spin" color={'#ffffff'} width={20} height={20} />} + icon={<ReactLoading type="spin" color="#ffffff" width={20} height={20} />} iconPlacement="right" onClick={() => { if (!loading) getEdit(); @@ -36,9 +36,9 @@ const Buttons = ({ loading, getEdit, onReset }: ButtonContainerProps) => { }} /> )} - <IconButton type={Type.SEC} color={activeColor} tooltip="Open Documentation" icon={<AiOutlineInfo size={'16px'} />} onClick={() => window.open('https://brown-dash.github.io/Dash-Documentation/features/generativeai/#editing', '_blank')} /> + <IconButton type={Type.SEC} color={activeColor} tooltip="Open Documentation" icon={<AiOutlineInfo size="16px" />} onClick={() => window.open('https://brown-dash.github.io/Dash-Documentation/features/generativeai/#editing', '_blank')} /> </div> ); -}; +} export default Buttons; diff --git a/src/client/views/nodes/generativeFill/generativeFillUtils/BrushHandler.ts b/src/client/views/nodes/generativeFill/generativeFillUtils/BrushHandler.ts index f4ec70fbc..16d529d93 100644 --- a/src/client/views/nodes/generativeFill/generativeFillUtils/BrushHandler.ts +++ b/src/client/views/nodes/generativeFill/generativeFillUtils/BrushHandler.ts @@ -3,7 +3,7 @@ import { eraserColor } from './generativeFillConstants'; import { Point } from './generativeFillInterfaces'; export class BrushHandler { - static brushCircleOverlay = (x: number, y: number, brushRadius: number, ctx: CanvasRenderingContext2D, fillColor: string, erase: boolean) => { + static brushCircleOverlay = (x: number, y: number, brushRadius: number, ctx: CanvasRenderingContext2D, fillColor: string /* , erase: boolean */) => { ctx.globalCompositeOperation = 'destination-out'; ctx.fillStyle = fillColor; ctx.shadowColor = eraserColor; @@ -14,12 +14,12 @@ export class BrushHandler { ctx.closePath(); }; - static createBrushPathOverlay = (startPoint: Point, endPoint: Point, brushRadius: number, ctx: CanvasRenderingContext2D, fillColor: string, erase: boolean) => { + static createBrushPathOverlay = (startPoint: Point, endPoint: Point, brushRadius: number, ctx: CanvasRenderingContext2D, fillColor: string /* , erase: boolean */) => { const dist = GenerativeFillMathHelpers.distanceBetween(startPoint, endPoint); for (let i = 0; i < dist; i += 5) { const s = i / dist; - BrushHandler.brushCircleOverlay(startPoint.x * (1 - s) + endPoint.x * s, startPoint.y * (1 - s) + endPoint.y * s, brushRadius, ctx, fillColor, erase); + BrushHandler.brushCircleOverlay(startPoint.x * (1 - s) + endPoint.x * s, startPoint.y * (1 - s) + endPoint.y * s, brushRadius, ctx, fillColor /* , erase */); } }; } diff --git a/src/client/views/nodes/generativeFill/generativeFillUtils/GenerativeFillMathHelpers.ts b/src/client/views/nodes/generativeFill/generativeFillUtils/GenerativeFillMathHelpers.ts index 97e03ff20..6da8c3da0 100644 --- a/src/client/views/nodes/generativeFill/generativeFillUtils/GenerativeFillMathHelpers.ts +++ b/src/client/views/nodes/generativeFill/generativeFillUtils/GenerativeFillMathHelpers.ts @@ -1,10 +1,6 @@ import { Point } from './generativeFillInterfaces'; export class GenerativeFillMathHelpers { - static distanceBetween = (p1: Point, p2: Point) => { - return Math.sqrt(Math.pow(p2.x - p1.x, 2) + Math.pow(p2.y - p1.y, 2)); - }; - static angleBetween = (p1: Point, p2: Point) => { - return Math.atan2(p2.x - p1.x, p2.y - p1.y); - }; + static distanceBetween = (p1: Point, p2: Point) => Math.sqrt((p2.x - p1.x) ** 2 + (p2.y - p1.y) ** 2); + static angleBetween = (p1: Point, p2: Point) => Math.atan2(p2.x - p1.x, p2.y - p1.y); } diff --git a/src/client/views/nodes/generativeFill/generativeFillUtils/ImageHandler.ts b/src/client/views/nodes/generativeFill/generativeFillUtils/ImageHandler.ts index 47a14135f..24dba1778 100644 --- a/src/client/views/nodes/generativeFill/generativeFillUtils/ImageHandler.ts +++ b/src/client/views/nodes/generativeFill/generativeFillUtils/ImageHandler.ts @@ -17,15 +17,14 @@ export class ImageUtility { * @param canvas Canvas to convert * @returns Blob of canvas */ - static canvasToBlob = (canvas: HTMLCanvasElement): Promise<Blob> => { - return new Promise(resolve => { + static canvasToBlob = (canvas: HTMLCanvasElement): Promise<Blob> => + new Promise(resolve => { canvas.toBlob(blob => { if (blob) { resolve(blob); } }, 'image/png'); }); - }; // given a square api image, get the cropped img static getCroppedImg = (img: HTMLImageElement, width: number, height: number): HTMLCanvasElement | undefined => { @@ -48,11 +47,12 @@ export class ImageUtility { } return canvas; } + return undefined; }; // converts an image to a canvas data url - static convertImgToCanvasUrl = async (imageSrc: string, width: number, height: number): Promise<string> => { - return new Promise<string>((resolve, reject) => { + static convertImgToCanvasUrl = async (imageSrc: string, width: number, height: number): Promise<string> => + new Promise<string>((resolve, reject) => { const img = new Image(); img.onload = () => { const canvas = this.getCroppedImg(img, width, height); @@ -66,7 +66,6 @@ export class ImageUtility { }; img.src = imageSrc; }); - }; // calls the openai api to get image edits static getEdit = async (imgBlob: Blob, maskBlob: Blob, prompt: string, n?: number): Promise<APISuccess | APIError> => { @@ -91,7 +90,7 @@ export class ImageUtility { console.log(data.data); return { status: 'success', - urls: (data.data as { b64_json: string }[]).map(data => `data:image/png;base64,${data.b64_json}`), + urls: (data.data as { b64_json: string }[]).map(urlData => `data:image/png;base64,${urlData.b64_json}`), }; } catch (err) { console.log(err); @@ -100,12 +99,10 @@ export class ImageUtility { }; // mock api call - static mockGetEdit = async (mockSrc: string): Promise<APISuccess | APIError> => { - return { - status: 'success', - urls: [mockSrc, mockSrc, mockSrc], - }; - }; + static mockGetEdit = async (mockSrc: string): Promise<APISuccess | APIError> => ({ + status: 'success', + urls: [mockSrc, mockSrc, mockSrc], + }); // Gets the canvas rendering context of a canvas static getCanvasContext = (canvasRef: RefObject<HTMLCanvasElement>): CanvasRenderingContext2D | null => { @@ -150,12 +147,12 @@ export class ImageUtility { // Draws the image to the current canvas static drawImgToCanvas = (img: HTMLImageElement, canvasRef: React.RefObject<HTMLCanvasElement>, width: number, height: number) => { - const drawImg = (img: HTMLImageElement) => { + const drawImg = (htmlImg: HTMLImageElement) => { const ctx = this.getCanvasContext(canvasRef); if (!ctx) return; ctx.globalCompositeOperation = 'source-over'; ctx.clearRect(0, 0, width, height); - ctx.drawImage(img, 0, 0, width, height); + ctx.drawImage(htmlImg, 0, 0, width, height); }; if (img.complete) { @@ -173,7 +170,7 @@ export class ImageUtility { canvas.width = canvasSize; canvas.height = canvasSize; const ctx = canvas.getContext('2d'); - if (!ctx) return; + if (!ctx) return undefined; ctx?.clearRect(0, 0, canvasSize, canvasSize); ctx.drawImage(paddedCanvas, 0, 0); @@ -195,7 +192,7 @@ export class ImageUtility { // Fills in the blank areas of the image with an image reflection (to fill in a square-shaped canvas) static drawHorizontalReflection = (ctx: CanvasRenderingContext2D, canvas: HTMLCanvasElement, xOffset: number) => { const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height); - const data = imageData.data; + const { data } = imageData; for (let i = 0; i < canvas.height; i++) { for (let j = 0; j < xOffset; j++) { const targetIdx = 4 * (i * canvas.width + j); @@ -224,7 +221,7 @@ export class ImageUtility { // Fills in the blank areas of the image with an image reflection (to fill in a square-shaped canvas) static drawVerticalReflection = (ctx: CanvasRenderingContext2D, canvas: HTMLCanvasElement, yOffset: number) => { const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height); - const data = imageData.data; + const { data } = imageData; for (let j = 0; j < canvas.width; j++) { for (let i = 0; i < yOffset; i++) { const targetIdx = 4 * (i * canvas.width + j); @@ -256,7 +253,7 @@ export class ImageUtility { canvas.width = canvasSize; canvas.height = canvasSize; const ctx = canvas.getContext('2d'); - if (!ctx) return; + if (!ctx) return undefined; // fix scaling const scale = Math.min(canvasSize / img.width, canvasSize / img.height); const width = Math.floor(img.width * scale); @@ -310,5 +307,6 @@ export class ImageUtility { } catch (err) { console.error(err); } + return undefined; }; } diff --git a/src/client/views/nodes/generativeFill/generativeFillUtils/PointerHandler.ts b/src/client/views/nodes/generativeFill/generativeFillUtils/PointerHandler.ts index 9e620ad11..260923a64 100644 --- a/src/client/views/nodes/generativeFill/generativeFillUtils/PointerHandler.ts +++ b/src/client/views/nodes/generativeFill/generativeFillUtils/PointerHandler.ts @@ -1,15 +1,11 @@ -import { Point } from "./generativeFillInterfaces"; +import { Point } from './generativeFillInterfaces'; export class PointerHandler { - static getPointRelativeToElement = ( - element: HTMLElement, - e: React.PointerEvent | PointerEvent, - scale: number - ): Point => { - const boundingBox = element.getBoundingClientRect(); - return { - x: (e.clientX - boundingBox.x) / scale, - y: (e.clientY - boundingBox.y) / scale, + static getPointRelativeToElement = (element: HTMLElement, e: React.PointerEvent | PointerEvent, scale: number): Point => { + const boundingBox = element.getBoundingClientRect(); + return { + x: (e.clientX - boundingBox.x) / scale, + y: (e.clientY - boundingBox.y) / scale, + }; }; - }; } diff --git a/src/client/views/nodes/importBox/ImportElementBox.tsx b/src/client/views/nodes/importBox/ImportElementBox.tsx index 6e7c3e612..317719032 100644 --- a/src/client/views/nodes/importBox/ImportElementBox.tsx +++ b/src/client/views/nodes/importBox/ImportElementBox.tsx @@ -1,7 +1,7 @@ import { computed, makeObservable } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; -import { returnFalse } from '../../../../Utils'; +import { returnFalse } from '../../../../ClientUtils'; import { Doc } from '../../../../fields/Doc'; import { ViewBoxBaseComponent } from '../../DocComponent'; import { DocumentView } from '../DocumentView'; @@ -22,13 +22,14 @@ export class ImportElementBox extends ViewBoxBaseComponent<FieldViewProps>() { return ( <div style={{ backgroundColor: 'pink' }}> <DocumentView + // eslint-disable-next-line react/jsx-props-no-spreading {...this._props} // LayoutTemplateString={undefined} Document={this.Document} isContentActive={returnFalse} addDocument={returnFalse} ScreenToLocalTransform={this.screenToLocalXf} - hideResizeHandles={true} + hideResizeHandles /> </div> ); diff --git a/src/client/views/nodes/trails/PresBox.tsx b/src/client/views/nodes/trails/PresBox.tsx index 91fdb90fc..6b4f5e073 100644 --- a/src/client/views/nodes/trails/PresBox.tsx +++ b/src/client/views/nodes/trails/PresBox.tsx @@ -1,68 +1,45 @@ +/* eslint-disable jsx-a11y/no-static-element-interactions */ +/* eslint-disable jsx-a11y/click-events-have-key-events */ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { Tooltip } from '@mui/material'; import { action, computed, IReactionDisposer, makeObservable, observable, ObservableSet, reaction, runInAction } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; -import { Doc, DocListCast, Field, FieldResult, NumListCast, Opt, StrListCast } from '../../../../fields/Doc'; +import { lightOrDark, returnFalse, returnOne, setupMoveUpEvents, StopEvent } from '../../../../ClientUtils'; +import { Doc, DocListCast, Field, FieldResult, FieldType, NumListCast, Opt, StrListCast } from '../../../../fields/Doc'; import { Animation, DocData, TransitionTimer } from '../../../../fields/DocSymbols'; -import { Copy, Id } from '../../../../fields/FieldSymbols'; +import { Copy } from '../../../../fields/FieldSymbols'; import { InkField } from '../../../../fields/InkField'; import { List } from '../../../../fields/List'; import { ObjectField } from '../../../../fields/ObjectField'; import { listSpec } from '../../../../fields/Schema'; import { ComputedField, ScriptField } from '../../../../fields/ScriptField'; -import { BoolCast, Cast, DocCast, NumCast, StrCast } from '../../../../fields/Types'; -import { AudioField } from '../../../../fields/URLField'; -import { emptyFunction, emptyPath, lightOrDark, returnFalse, returnOne, setupMoveUpEvents, StopEvent, stringHash } from '../../../../Utils'; +import { BoolCast, Cast, DocCast, NumCast, StrCast, toList } from '../../../../fields/Types'; +import { emptyFunction, emptyPath, stringHash } from '../../../../Utils'; import { DocServer } from '../../../DocServer'; import { Docs } from '../../../documents/Documents'; import { CollectionViewType, DocumentType } from '../../../documents/DocumentTypes'; -import { DocumentManager } from '../../../util/DocumentManager'; -import { dropActionType } from '../../../util/DragManager'; +import { dropActionType } from '../../../util/DropActionTypes'; import { ScriptingGlobals } from '../../../util/ScriptingGlobals'; -import { SelectionManager } from '../../../util/SelectionManager'; import { SerializationHelper } from '../../../util/SerializationHelper'; -import { SettingsManager } from '../../../util/SettingsManager'; +import { SnappingManager } from '../../../util/SnappingManager'; import { undoBatch, UndoManager } from '../../../util/UndoManager'; import { CollectionDockingView } from '../../collections/CollectionDockingView'; -import { CollectionFreeFormView, MarqueeViewBounds } from '../../collections/collectionFreeForm'; -import { CollectionStackedTimeline } from '../../collections/CollectionStackedTimeline'; +import { CollectionFreeFormView } from '../../collections/collectionFreeForm'; +import { CollectionFreeFormPannableContents } from '../../collections/collectionFreeForm/CollectionFreeFormPannableContents'; import { CollectionView } from '../../collections/CollectionView'; import { TreeView } from '../../collections/TreeView'; import { ViewBoxBaseComponent } from '../../DocComponent'; import { Colors } from '../../global/globalEnums'; import { LightboxView } from '../../LightboxView'; -import { DocumentView, OpenWhere, OpenWhereMod } from '../DocumentView'; -import { FieldView, FieldViewProps, FocusViewOptions } from '../FieldView'; +import { pinDataTypes as dataTypes } from '../../PinFuncs'; +import { DocumentView } from '../DocumentView'; +import { FieldView, FieldViewProps } from '../FieldView'; +import { FocusViewOptions } from '../FocusViewOptions'; +import { OpenWhere, OpenWhereMod } from '../OpenWhere'; import { ScriptingBox } from '../ScriptingBox'; import './PresBox.scss'; import { PresEffect, PresEffectDirection, PresMovement, PresStatus } from './PresEnums'; -export interface pinDataTypes { - scrollable?: boolean; - dataviz?: number[]; - pannable?: boolean; - type_collection?: boolean; - inkable?: boolean; - filters?: boolean; - pivot?: boolean; - temporal?: boolean; - clippable?: boolean; - datarange?: boolean; - dataview?: boolean; - poslayoutview?: boolean; - dataannos?: boolean; - map?: boolean; -} -export interface PinProps { - audioRange?: boolean; - activeFrame?: number; - currentFrame?: number; - hidePresBox?: boolean; - pinViewport?: MarqueeViewBounds; // pin a specific viewport on a freeform view (use MarqueeView.CurViewBounds to compute if no region has been selected) - pinDocLayout?: boolean; // pin layout info (width/height/x/y) - pinAudioPlay?: boolean; // pin audio annotation - pinData?: pinDataTypes; -} @observer export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { @@ -75,8 +52,9 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { super(props); makeObservable(this); if (!PresBox.navigateToDocScript) { - PresBox.navigateToDocScript = ScriptField.MakeFunction('navigateToDoc(this.presentation_targetDoc, self)')!; + PresBox.navigateToDocScript = ScriptField.MakeFunction('navigateToDoc(this.presentation_targetDoc, this)')!; } + CollectionFreeFormPannableContents.SetOverlayPlugin((fform: Doc) => PresBox.Instance.pathLines(fform)); } private _disposers: { [name: string]: IReactionDisposer } = {}; @@ -86,6 +64,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { _unmounting = false; // flag that view is unmounting used to block RemFromMap from deleting things _presTimer: NodeJS.Timeout | undefined; + // eslint-disable-next-line no-use-before-define @observable public static Instance: PresBox; @observable _isChildActive = false; @@ -126,7 +105,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { return DocCast(this.childDocs[NumCast(this.Document._itemIndex)]); } @computed get targetDoc() { - return Cast(this.activeItem?.presentation_targetDoc, Doc, null); + return DocCast(this.activeItem?.presentation_targetDoc); } public static targetRenderedDoc = (doc: Doc) => { const targetDoc = Cast(doc?.presentation_targetDoc, Doc, null); @@ -141,8 +120,9 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { return false; } @computed get selectedDocumentView() { - if (SelectionManager.Views.length) return SelectionManager.Views[0]; - if (this.selectedArray.size) return DocumentManager.Instance.getDocumentView(this.Document); + if (DocumentView.Selected().length) return DocumentView.Selected()[0]; + if (this.selectedArray.size) return DocumentView.getDocumentView(this.Document); + return undefined; } @computed get isPres() { return this.selectedDoc === this.Document; @@ -165,6 +145,10 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { } componentDidMount() { + this._disposers.pause = reaction( + () => SnappingManager.UserPanned, + () => this.pauseAutoPres() + ); this._disposers.keyboard = reaction( () => this.selectedDoc, selected => { @@ -189,7 +173,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { this._unmounting = false; this.turnOffEdit(true); this._disposers.selection = reaction( - () => SelectionManager.Views.slice(), + () => DocumentView.Selected().slice(), views => (!PresBox.Instance || views.some(view => view.Document === this.Document)) && this.updateCurrentPresentation(), { fireImmediately: true } ); @@ -197,7 +181,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { () => this.layoutDoc.presentation_status === PresStatus.Edit, editing => editing && this.childDocs.filter(doc => doc.presentation_indexed !== undefined).forEach(doc => { - this.progressivizedItems(doc)?.forEach(indexedDoc => (indexedDoc.opacity = undefined)); + this.progressivizedItems(doc)?.forEach(indexedDoc => { indexedDoc.opacity = undefined; }); doc.presentation_indexed = Math.min(this.progressivizedItems(doc)?.length ?? 0, 1); }) // prettier-ignore ); @@ -214,7 +198,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { startTempMedia = (targetDoc: Doc, activeItem: Doc) => { const duration: number = NumCast(activeItem.config_clipEnd) - NumCast(activeItem.config_clipStart); if ([DocumentType.VID, DocumentType.AUDIO].includes(targetDoc.type as any)) { - const targMedia = DocumentManager.Instance.getDocumentView(targetDoc); + const targMedia = DocumentView.getDocumentView(targetDoc); targMedia?.ComponentView?.playFrom?.(NumCast(activeItem.config_clipStart), NumCast(activeItem.config_clipStart) + duration); } }; @@ -222,18 +206,18 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { stopTempMedia = (targetDocField: FieldResult) => { const targetDoc = DocCast(DocCast(targetDocField).annotationOn) ?? DocCast(targetDocField); if ([DocumentType.VID, DocumentType.AUDIO].includes(targetDoc.type as any)) { - const targMedia = DocumentManager.Instance.getDocumentView(targetDoc); + const targMedia = DocumentView.getDocumentView(targetDoc); targMedia?.ComponentView?.Pause?.(); } }; - //TODO: al: it seems currently that tempMedia doesn't stop onslidechange after clicking the button; the time the tempmedia stop depends on the start & end time + // TODO: al: it seems currently that tempMedia doesn't stop onslidechange after clicking the button; the time the tempmedia stop depends on the start & end time // TODO: to handle child slides (entering into subtrail and exiting), also the next() and back() functions // No more frames in current doc and next slide is defined, therefore move to next slide nextSlide = (slideNum?: number) => { const nextSlideInd = slideNum ?? this.itemIndex + 1; let curSlideInd = nextSlideInd; - //CollectionStackedTimeline.CurrentlyPlaying?.map(clipView => clipView?.ComponentView?.Pause?.()); + // CollectionStackedTimeline.CurrentlyPlaying?.map(clipView => clipView?.ComponentView?.Pause?.()); this.clearSelectedArray(); const doGroupWithUp = (nextSelected: number, force = false) => @@ -245,7 +229,9 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { if (serial) { this.gotoDocument(nextSelected, this.activeItem, true, async () => { const waitTime = NumCast(this.activeItem.presentation_duration); - await new Promise<void>(res => setTimeout(() => res(), Math.max(0, waitTime))); + await new Promise<void>(res => { + setTimeout(res, Math.max(0, waitTime)); + }); doGroupWithUp(nextSelected + 1)(); }); } else { @@ -264,14 +250,15 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { const targetList = PresBox.targetRenderedDoc(doc); if (doc.presentation_indexed !== undefined && targetList) { const listItems = (Cast(targetList[Doc.LayoutFieldKey(targetList)], listSpec(Doc), null)?.filter(d => d instanceof Doc) as Doc[]) ?? DocListCast(targetList[Doc.LayoutFieldKey(targetList) + '_annotations']); - return listItems.filter(doc => !doc.layout_unrendered); + return listItems.filter(ldoc => !ldoc.layout_unrendered); } + return undefined; }; // go to documents chain runSubroutines = (childrenToRun: Opt<Doc[]>, normallyNextSlide: Doc) => { if (childrenToRun && childrenToRun[0] !== normallyNextSlide) { - childrenToRun.forEach(child => DocumentManager.Instance.showDocument(child, {})); + childrenToRun.forEach(child => DocumentView.showDocument(child, {})); } }; @@ -284,12 +271,14 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { const targetRenderedDoc = PresBox.targetRenderedDoc(this.activeItem); targetRenderedDoc._dataTransition = 'all 1s'; targetRenderedDoc.opacity = 1; - setTimeout(() => (targetRenderedDoc._dataTransition = 'inherit'), 1000); + setTimeout(() => { + targetRenderedDoc._dataTransition = 'inherit'; + }, 1000); const listItems = this.progressivizedItems(this.activeItem); if (listItems && presIndexed < listItems.length) { if (!first) { const listItemDoc = listItems[presIndexed]; - const targetView = listItems && DocumentManager.Instance.getFirstDocumentView(listItemDoc); + const targetView = listItems && DocumentView.getFirstDocumentView(listItemDoc); Doc.linkFollowUnhighlight(); Doc.HighlightDoc(listItemDoc); listItemDoc.presentation_effect = this.activeItem.presBulletEffect; @@ -305,6 +294,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { return true; } } + return undefined; }; if (progressiveReveal(false)) return true; if (this.childDocs[this.itemIndex + 1] !== undefined) { @@ -314,7 +304,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { // before moving onto next slide, run the subroutines :) const currentDoc = this.childDocs[this.itemIndex]; - //could i do this.childDocs[this.itemIndex] for first arg? + // could i do this.childDocs[this.itemIndex] for first arg? this.runSubroutines(TreeView.GetRunningChildren.get(currentDoc)?.(), this.childDocs[this.itemIndex + 1]); this.nextSlide(curLast + 1 === this.childDocs.length ? (this.layoutDoc.presLoop ? 0 : curLast) : curLast + 1); @@ -334,7 +324,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { // Called when the user activates 'back' - to move to the previous part of the pres. trail @action back = () => { - const activeItem: Doc = this.activeItem; + const { activeItem } = this; let prevSelected = this.itemIndex; // Functionality for group with up let didZoom = activeItem.presentation_movement; @@ -353,8 +343,8 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { return this.itemIndex; }; - //The function that is called when a document is clicked or reached through next or back. - //it'll also execute the necessary actions if presentation is playing. + // The function that is called when a document is clicked or reached through next or back. + // it'll also execute the necessary actions if presentation is playing. @undoBatch public gotoDocument = action((index: number, from?: Doc, group?: boolean, finished?: () => void) => { Doc.UnBrushAllDocs(); @@ -371,13 +361,13 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { this.startTempMedia(this.targetDoc, this.activeItem); } if (!group) this.clearSelectedArray(); - this.childDocs[index] && this.addToSelectedArray(this.childDocs[index]); //Update selected array + this.childDocs[index] && this.addToSelectedArray(this.childDocs[index]); // Update selected array this.turnOffEdit(); - this.navigateToActiveItem(finished); //Handles movement to element only when presentationTrail is list - this.doHideBeforeAfter(); //Handles hide after/before + this.navigateToActiveItem(finished); // Handles movement to element only when presentationTrail is list + this.doHideBeforeAfter(); // Handles hide after/before } }); - static pinDataTypes(target?: Doc): pinDataTypes { + static pinDataTypes(target?: Doc): dataTypes { const targetType = target?.type as any; const inkable = [DocumentType.INK].includes(targetType); const scrollable = [DocumentType.PDF, DocumentType.RTF, DocumentType.WEB].includes(targetType) || target?._type_collection === CollectionViewType.Stacking; @@ -388,19 +378,22 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { const datarange = [DocumentType.FUNCPLOT].includes(targetType); const dataview = [DocumentType.INK, DocumentType.COL, DocumentType.IMG, DocumentType.RTF].includes(targetType) && target?.activeFrame === undefined; const poslayoutview = [DocumentType.COL].includes(targetType) && target?.activeFrame === undefined; - const type_collection = targetType === DocumentType.COL; + const typeCollection = targetType === DocumentType.COL; const filters = true; const pivot = true; const dataannos = false; - return { scrollable, pannable, inkable, type_collection, pivot, map, filters, temporal, clippable, dataview, datarange, poslayoutview, dataannos }; + return { scrollable, pannable, inkable, type_collection: typeCollection, pivot, map, filters, temporal, clippable, dataview, datarange, poslayoutview, dataannos }; } @action - playAnnotation = (anno: AudioField) => {}; + playAnnotation = (/* anno: AudioField */) => { + /* empty */ + }; @action - static restoreTargetDocView(bestTargetView: Opt<DocumentView>, activeItem: Doc, transTime: number, pinDocLayout: boolean = BoolCast(activeItem.config_pinLayout), pinDataTypes?: pinDataTypes, targetDoc?: Doc) { + // eslint-disable-next-line default-param-last + static restoreTargetDocView(bestTargetView: Opt<DocumentView>, activeItem: Doc, transTime: number, pinDocLayout: boolean = BoolCast(activeItem.config_pinLayout), pinDataTypes?: dataTypes, targetDoc?: Doc) { const bestTarget = bestTargetView?.Document ?? (targetDoc?.layout_unrendered ? DocCast(targetDoc?.annotationOn) : targetDoc); - if (!bestTarget) return; + if (!bestTarget) return undefined; let changed = false; if (pinDocLayout) { if ( @@ -417,20 +410,22 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { bestTarget.width = NumCast(activeItem.config_width, NumCast(bestTarget.width)); bestTarget.height = NumCast(activeItem.config_height, NumCast(bestTarget.height)); bestTarget[TransitionTimer] && clearTimeout(bestTarget[TransitionTimer]); - bestTarget[TransitionTimer] = setTimeout(() => (bestTarget[TransitionTimer] = bestTarget._dataTransition = undefined), transTime + 10); + bestTarget[TransitionTimer] = setTimeout(() => { + bestTarget[TransitionTimer] = bestTarget._dataTransition = undefined; + }, transTime + 10); changed = true; } } const activeFrame = activeItem.config_activeFrame ?? activeItem.config_currentFrame; if (activeFrame !== undefined) { - const transTime = NumCast(activeItem.presentation_transition, 500); + const frameTime = NumCast(activeItem.presentation_transition, 500); const acontext = activeItem.config_activeFrame !== undefined ? DocCast(DocCast(activeItem.presentation_targetDoc).embedContainer) : DocCast(activeItem.presentation_targetDoc); const context = DocCast(acontext)?.annotationOn ? DocCast(DocCast(acontext).annotationOn) : acontext; if (context) { - const ffview = DocumentManager.Instance.getFirstDocumentView(context)?.CollectionFreeFormView; + const ffview = CollectionFreeFormView.from(DocumentView.getFirstDocumentView(context)); if (ffview?.childDocs) { - PresBox.Instance._keyTimer = CollectionFreeFormView.gotoKeyframe(PresBox.Instance._keyTimer, ffview.childDocs, transTime); + PresBox.Instance._keyTimer = CollectionFreeFormView.gotoKeyframe(PresBox.Instance._keyTimer, ffview.childDocs, frameTime); ffview.layoutDoc._currentFrame = NumCast(activeFrame); } } @@ -443,12 +438,14 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { else { const bestTargetData = bestTarget[DocData]; const current = bestTargetData[fkey]; - const hash = bestTargetData[fkey] ? stringHash(Field.toString(bestTargetData[fkey] as Field)) : undefined; + const hash = bestTargetData[fkey] ? stringHash(Field.toString(bestTargetData[fkey] as FieldType)) : undefined; if (hash) bestTargetData[fkey + '_' + hash] = current instanceof ObjectField ? current[Copy]() : current; bestTargetData[fkey] = activeItem.config_data instanceof ObjectField ? activeItem.config_data[Copy]() : activeItem.config_data; } bestTarget[fkey + '_usePath'] = activeItem.config_usePath; - setTimeout(() => (bestTarget._dataTransition = undefined), transTime + 10); + setTimeout(() => { + bestTarget._dataTransition = undefined; + }, transTime + 10); } if (pinDataTypes?.datarange || (!pinDataTypes && activeItem.config_xRange !== undefined)) { if (bestTarget.xRange !== activeItem.config_xRange) { @@ -590,7 +587,15 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { Doc.AddDocToList(bestTarget[DocData], layoutField, doc); } }); - setTimeout(() => Array.from(transitioned).forEach(action(doc => (doc._dataTransition = undefined))), transTime + 10); + setTimeout( + () => + Array.from(transitioned).forEach( + action(doc => { + doc._dataTransition = undefined; + }) + ), + transTime + 10 + ); } if ((pinDataTypes?.pannable || (!pinDataTypes && (activeItem.config_viewBounds !== undefined || activeItem.config_panX !== undefined || activeItem.config_viewScale !== undefined))) && !bestTarget.isGroup) { const contentBounds = Cast(activeItem.config_viewBounds, listSpec('number')); @@ -598,132 +603,24 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { const viewport = { panX: (contentBounds[0] + contentBounds[2]) / 2, panY: (contentBounds[1] + contentBounds[3]) / 2, width: contentBounds[2] - contentBounds[0], height: contentBounds[3] - contentBounds[1] }; bestTarget._freeform_panX = viewport.panX; bestTarget._freeform_panY = viewport.panY; - const dv = DocumentManager.Instance.getDocumentView(bestTarget); + const dv = DocumentView.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); activeItem.presentation_movement === PresMovement.Zoom && (bestTarget._freeform_scale = computedScale); dv.ComponentView?.brushView?.(viewport, transTime, 2500); } - } else { - if (bestTarget._freeform_panX !== activeItem.config_panX || bestTarget._freeform_panY !== activeItem.config_panY || bestTarget._freeform_scale !== activeItem.config_viewScale) { - bestTarget._freeform_panX = activeItem.config_panX ?? bestTarget._freeform_panX; - bestTarget._freeform_panY = activeItem.config_panY ?? bestTarget._freeform_panY; - bestTarget._freeform_scale = activeItem.config_viewScale ?? bestTarget._freeform_scale; - changed = true; - } + } else if (bestTarget._freeform_panX !== activeItem.config_panX || bestTarget._freeform_panY !== activeItem.config_panY || bestTarget._freeform_scale !== activeItem.config_viewScale) { + bestTarget._freeform_panX = activeItem.config_panX ?? bestTarget._freeform_panX; + bestTarget._freeform_panY = activeItem.config_panY ?? bestTarget._freeform_panY; + bestTarget._freeform_scale = activeItem.config_viewScale ?? bestTarget._freeform_scale; + changed = true; } } if (changed) { return bestTargetView?.setViewTransition('all', transTime); } - } - - /// copies values from the targetDoc (which is the prototype of the pinDoc) to - /// reserved fields on the pinDoc so that those values can be restored to the - /// target doc when navigating to it. - @action - static pinDocView(pinDoc: Doc, pinProps: PinProps, targetDoc: Doc) { - pinDoc.presentation = true; - pinDoc.config = ''; - if (pinProps.pinDocLayout) { - pinDoc.config_pinLayout = true; - pinDoc.config_x = NumCast(targetDoc.x); - pinDoc.config_y = NumCast(targetDoc.y); - pinDoc.config_rotation = NumCast(targetDoc.rotation); - pinDoc.config_width = NumCast(targetDoc.width); - pinDoc.config_height = NumCast(targetDoc.height); - } - if (pinProps.pinAudioPlay) pinDoc.presPlayAudio = true; - if (pinProps.pinData) { - pinDoc.config_pinData = - pinProps.pinData.scrollable || - pinProps.pinData.temporal || - pinProps.pinData.pannable || - pinProps.pinData.type_collection || - pinProps.pinData.clippable || - pinProps.pinData.datarange || - pinProps.pinData.dataview || - pinProps.pinData.poslayoutview || - pinProps?.activeFrame !== undefined; - const fkey = Doc.LayoutFieldKey(targetDoc); - if (pinProps.pinData.dataview) { - pinDoc.config_usePath = targetDoc[fkey + '_usePath']; - pinDoc.config_data = targetDoc[fkey] instanceof ObjectField ? (targetDoc[fkey] as ObjectField)[Copy]() : targetDoc.data; - } - if (pinProps.pinData.dataannos) { - const fkey = Doc.LayoutFieldKey(targetDoc); - pinDoc.config_annotations = new List<Doc>(DocListCast(targetDoc[DocData][fkey + '_annotations']).filter(doc => !doc.layout_unrendered)); - } - if (pinProps.pinData.inkable) { - pinDoc.config_fillColor = targetDoc.fillColor; - pinDoc.config_color = targetDoc.color; - pinDoc.config_width = targetDoc._width; - pinDoc.config_height = targetDoc._height; - } - if (pinProps.pinData.scrollable) pinDoc.config_scrollTop = targetDoc._layout_scrollTop; - if (pinProps.pinData.clippable) { - const fkey = Doc.LayoutFieldKey(targetDoc); - pinDoc.config_clipWidth = targetDoc[fkey + '_clipWidth']; - } - if (pinProps.pinData.datarange) { - pinDoc.config_xRange = undefined; //targetDoc?.xrange; - pinDoc.config_yRange = undefined; //targetDoc?.yrange; - } - if (pinProps.pinData.map) { - // pinDoc.config_latitude = targetDoc?.latitude; - // pinDoc.config_longitude = targetDoc?.longitude; - pinDoc.config_map_zoom = targetDoc?.map_zoom; - pinDoc.config_map_type = targetDoc?.map_type; - //... - } - if (pinProps.pinData.poslayoutview) - pinDoc.config_pinLayoutData = new List<string>( - DocListCast(targetDoc[fkey] as ObjectField).map(d => - JSON.stringify({ - id: d[Id], - x: NumCast(d.x), - y: NumCast(d.y), - w: NumCast(d._width), - h: NumCast(d._height), - fill: StrCast(d._fillColor), - back: StrCast(d._backgroundColor), - data: SerializationHelper.Serialize(d.data instanceof ObjectField ? d.data[Copy]() : ''), - text: SerializationHelper.Serialize(d.text instanceof ObjectField ? d.text[Copy]() : ''), - }) - ) - ); - if (pinProps.pinData.type_collection) pinDoc.config_viewType = targetDoc._type_collection; - if (pinProps.pinData.filters) pinDoc.config_docFilters = ObjectField.MakeCopy(targetDoc.childFilters as ObjectField); - if (pinProps.pinData.pivot) pinDoc.config_pivotField = targetDoc._pivotField; - if (pinProps.pinData.pannable) { - pinDoc.config_panX = NumCast(targetDoc._freeform_panX); - pinDoc.config_panY = NumCast(targetDoc._freeform_panY); - pinDoc.config_viewScale = NumCast(targetDoc._freeform_scale, 1); - } - if (pinProps.pinData.temporal) { - pinDoc.config_clipStart = targetDoc._layout_currentTimecode; - const duration = NumCast(pinDoc[`${Doc.LayoutFieldKey(pinDoc)}_duration`], NumCast(targetDoc.config_clipStart) + 0.1); - pinDoc.config_clipEnd = NumCast(pinDoc.config_clipStart) + NumCast(targetDoc.clipEnd, duration); - } - } - if (pinProps?.pinViewport) { - // If pinWithView option set then update scale and x / y props of slide - const bounds = pinProps.pinViewport; - pinDoc.config_pinView = true; - pinDoc.config_viewScale = NumCast(targetDoc._freeform_scale, 1); - pinDoc.config_panX = bounds.left + bounds.width / 2; - pinDoc.config_panY = bounds.top + bounds.height / 2; - pinDoc.config_viewBounds = new List<number>([bounds.left, bounds.top, bounds.left + bounds.width, bounds.top + bounds.height]); - } - } - - @action - static reversePin(pinDoc: Doc, targetDoc: Doc) { - // const fkey = Doc.LayoutFieldKey(targetDoc); - pinDoc.config_data = targetDoc.data; - - console.log(pinDoc.presData); + return undefined; } /** @@ -735,8 +632,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { * on the right. */ navigateToActiveItem = (afterNav?: () => void) => { - const activeItem: Doc = this.activeItem; - const targetDoc: Doc = this.targetDoc; + const { activeItem, targetDoc } = this; const finished = () => { afterNav?.(); targetDoc[Animation] = undefined; @@ -746,8 +642,8 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { const eleViewCache = Array.from(this._eleArray); const resetSelection = action(() => { if (!this._props.isSelected()) { - const presDocView = DocumentManager.Instance.getDocumentView(this.Document); - if (presDocView) SelectionManager.SelectView(presDocView, false); + const presDocView = DocumentView.getDocumentView(this.Document); + if (presDocView) DocumentView.SelectView(presDocView, false); this.clearSelectedArray(); selViewCache.forEach(doc => this.addToSelectedArray(doc)); this._dragArray.splice(0, this._dragArray.length, ...dragViewCache); @@ -762,7 +658,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { static NavigateToTarget(targetDoc: Doc, activeItem: Doc, finished?: () => void) { if (activeItem.presentation_movement === PresMovement.None && targetDoc.type === DocumentType.SCRIPTING) { - (DocumentManager.Instance.getFirstDocumentView(targetDoc)?.ComponentView as ScriptingBox)?.onRun?.(); + (DocumentView.getFirstDocumentView(targetDoc)?.ComponentView as ScriptingBox)?.onRun?.(); return; } const effect = activeItem.presentation_effect && activeItem.presentation_effect !== PresEffect.None ? activeItem.presentation_effect : undefined; @@ -775,7 +671,6 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { effect: activeItem, noSelect: true, openLocation: targetDoc.type === DocumentType.PRES ? ((OpenWhere.replace + ':' + PresBox.PanelName) as OpenWhere) : OpenWhere.addLeft, - anchorDoc: activeItem, easeFunc: StrCast(activeItem.presEaseFunc, 'ease') as any, zoomTextSelections: BoolCast(activeItem.presentation_zoomText), playAudio: BoolCast(activeItem.presPlayAudio), @@ -783,19 +678,19 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { }; if (activeItem.presentation_openInLightbox) { const context = DocCast(targetDoc.annotationOn) ?? targetDoc; - if (!DocumentManager.Instance.getLightboxDocumentView(context)) { + if (!DocumentView.getLightboxDocumentView(context)) { LightboxView.Instance.SetLightboxDoc(context); } } if (targetDoc) { if (activeItem.presentation_targetDoc instanceof Doc) activeItem.presentation_targetDoc[Animation] = undefined; - DocumentManager.Instance.AddViewRenderedCb(LightboxView.LightboxDoc, dv => { + DocumentView.addViewRenderedCb(LightboxView.LightboxDoc, () => { // if target or the doc it annotates is not in the lightbox, then close the lightbox - if (!DocumentManager.Instance.getLightboxDocumentView(DocCast(targetDoc.annotationOn) ?? targetDoc)) { + if (!DocumentView.getLightboxDocumentView(DocCast(targetDoc.annotationOn) ?? targetDoc)) { LightboxView.Instance.SetLightboxDoc(undefined); } - DocumentManager.Instance.showDocument(targetDoc, options, finished); + DocumentView.showDocument(targetDoc, options, finished); }); } else finished?.(); } @@ -822,7 +717,9 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { opacity = 0; } else if (index === this.itemIndex || !curDoc.presentation_hideAfter) { opacity = 1; - setTimeout(() => (tagDoc._dataTransition = undefined), 1000); + setTimeout(() => { + tagDoc._dataTransition = undefined; + }, 1000); } } const hidingIndAft = @@ -848,16 +745,20 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { }; _exitTrail: Opt<() => void>; - PlayTrail = (docs: Doc[]) => { + playTrail = (docs: Doc[]) => { const savedStates = docs.map(doc => { switch (doc.type) { case DocumentType.COL: - if (doc._type_collection === CollectionViewType.Freeform) return { type: CollectionViewType.Freeform, doc, x: NumCast(doc.freeform_panX), y: NumCast(doc.freeform_panY), s: NumCast(doc.freeform_scale) }; + if (doc._type_collection === CollectionViewType.Freeform) { + return { type: CollectionViewType.Freeform, doc, x: NumCast(doc.freeform_panX), y: NumCast(doc.freeform_panY), s: NumCast(doc.freeform_scale) }; + } break; case DocumentType.INK: if (doc.data instanceof InkField) { return { type: doc.type, doc, data: doc.data?.[Copy](), fillColor: doc.fillColor, color: doc.color, x: NumCast(doc.x), y: NumCast(doc.y) }; } + break; + default: } return undefined; }); @@ -865,7 +766,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { this._exitTrail = () => { savedStates .filter(savedState => savedState) - .map(savedState => { + .forEach(savedState => { switch (savedState?.type) { case CollectionViewType.Freeform: { @@ -885,6 +786,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { doc.color = color; } break; + default: } }); LightboxView.Instance.SetLightboxDoc(undefined); @@ -903,8 +805,8 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { } }; - //The function that resets the presentation by removing every action done by it. It also - //stops the presentaton. + // The function that resets the presentation by removing every action done by it. It also + // stops the presentaton. resetPresentation = () => { this.childDocs .map(doc => PresBox.targetRenderedDoc(doc)) @@ -921,12 +823,14 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { // The function allows for viewing the pres path on toggle @action togglePath = (off?: boolean) => { this._pathBoolean = off ? false : !this._pathBoolean; - CollectionFreeFormView.ShowPresPaths = this._pathBoolean; + SnappingManager.SetShowPresPaths(this._pathBoolean); }; // The function allows for expanding the view of pres on toggle @action toggleExpandMode = () => { - runInAction(() => (this._expandBoolean = !this._expandBoolean)); + runInAction(() => { + this._expandBoolean = !this._expandBoolean; + }); this.Document.expandBoolean = this._expandBoolean; this.childDocs.forEach(doc => { doc.presentation_expandInlineButton = this._expandBoolean; @@ -942,7 +846,9 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { const startInd = NumCast(doc.presentation_indexedStart); this.progressivizedItems(doc) ?.slice(startInd) - .forEach(indexedDoc => (indexedDoc.opacity = 0)); + .forEach(indexedDoc => { + indexedDoc.opacity = 0; + }); doc.presentation_indexed = Math.min(this.progressivizedItems(doc)?.length ?? 0, startInd); } // if (doc.presentation_hide && this.childDocs.indexOf(doc) === startIndex) tagDoc.opacity = 0; @@ -993,13 +899,13 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { public static minimizedWidth = 198; public static OpenPresMinimized(doc: Doc, pt: number[]) { - doc.overlayX = pt[0]; - doc.overlayY = pt[1]; + [doc.overlayX, doc.overlayY] = pt; doc._height = 30; doc._width = PresBox.minimizedWidth; Doc.AddToMyOverlay(doc); PresBox.Instance?.initializePresState(PresBox.Instance.itemIndex); - return (doc.presentation_status = PresStatus.Manual); + doc.presentation_status = PresStatus.Manual; + return doc.presentation_status; } /** @@ -1008,12 +914,11 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { */ @undoBatch viewChanged = action((e: React.ChangeEvent) => { - //@ts-ignore - const type_collection = e.target.selectedOptions[0].value as CollectionViewType; - this.layoutDoc.presFieldKey = this.fieldKey + (type_collection === CollectionViewType.Tree ? '-linearized' : ''); + const typeCollection = (e.target as any).selectedOptions[0].value as CollectionViewType; + this.layoutDoc.presFieldKey = this.fieldKey + (typeCollection === CollectionViewType.Tree ? '-linearized' : ''); // pivot field may be set by the user in timeline view (or some other way) -- need to reset it here - [CollectionViewType.Tree || CollectionViewType.Stacking].includes(type_collection) && (this.Document._pivotField = undefined); - this.Document._type_collection = type_collection; + [CollectionViewType.Tree || CollectionViewType.Stacking].includes(typeCollection) && (this.Document._pivotField = undefined); + this.Document._type_collection = typeCollection; if (this.isTreeOrStack) { this.layoutDoc._gridGap = 0; } @@ -1025,10 +930,9 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { */ // @undoBatch mediaStopChanged = action((e: React.ChangeEvent) => { - const activeItem: Doc = this.activeItem; - //@ts-ignore - const stopDoc = e.target.selectedOptions[0].value as string; - const stopDocIndex: number = Number(stopDoc[0]); + const { activeItem } = this; + const stopDoc = (e.target as any).selectedOptions[0].value as string; + const stopDocIndex = Number(stopDoc[0]); activeItem.mediaStopDoc = stopDocIndex; if (this.childDocs[stopDocIndex - 1].mediaStopTriggerList) { const list = DocListCast(this.childDocs[stopDocIndex - 1].mediaStopTriggerList); @@ -1049,10 +953,12 @@ 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) => { + const results = docs.map(doc => { if (doc.presentation_targetDoc) return true; if (doc.type === DocumentType.LABEL) { const audio = Cast(doc.annotationOn, Doc, null); @@ -1065,17 +971,22 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { return false; } } else if (doc.type !== DocumentType.PRES) { + // eslint-disable-next-line operator-assignment if (!doc.presentation_targetDoc) doc.title = doc.title + ' - Slide'; doc.presentation_targetDoc = doc.createdFrom ?? doc; // dropped document will be a new embedding of an embedded document somewhere else. doc.presentation_movement = PresMovement.Zoom; if (this._expandBoolean) doc.presentation_expandInlineButton = true; } + return false; }); - return true; + return !results.some(r => !r); }; childLayoutTemplate = () => Docs.Create.PresElementBoxDocument(); - removeDocument = (doc: Doc | Doc[]) => !(doc instanceof Doc ? [doc] : doc).map(d => Doc.RemoveDocFromList(this.Document, this.fieldKey, d)).some(p => !p); + removeDocument = (doc: Doc | Doc[]) => + !toList(doc) + .map(d => Doc.RemoveDocFromList(this.Document, this.fieldKey, d)) + .some(p => !p); getTransform = () => this.ScreenToLocalBoxXf().translate(-5, -65); // listBox padding-left and pres-box-cont minHeight panelHeight = () => this._props.PanelHeight() - 40; /** @@ -1092,42 +1003,46 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { const tagDoc = Cast(curDoc.presentation_targetDoc, Doc, null); if (curDoc && curDoc === this.activeItem) return ( + // eslint-disable-next-line react/no-array-index-key <div key={index} className="selectedList-items"> <b> {index + 1}. {curDoc.title} </b> </div> ); - else if (tagDoc) + if (tagDoc) return ( + // eslint-disable-next-line react/no-array-index-key <div key={index} className="selectedList-items"> {index + 1}. {curDoc.title} </div> ); - else if (curDoc) + if (curDoc) return ( + // eslint-disable-next-line react/no-array-index-key <div key={index} className="selectedList-items"> {index + 1}. {curDoc.title} </div> ); + return null; }); } @action selectPres = () => { - const presDocView = DocumentManager.Instance.getDocumentView(this.Document); - presDocView && SelectionManager.SelectView(presDocView, false); + const presDocView = DocumentView.getDocumentView(this.Document); + presDocView && DocumentView.SelectView(presDocView, false); }; - focusElement = (doc: Doc, options: FocusViewOptions) => { + focusElement = (doc: Doc) => { this.selectElement(doc); return undefined; }; - //Regular click + // Regular click @action selectElement = (doc: Doc, noNav = false) => { - CollectionStackedTimeline.CurrentlyPlaying?.map((clip, i) => clip?.ComponentView?.Pause?.()); + DocumentView.CurrentlyPlaying?.map(clip => clip?.ComponentView?.Pause?.()); if (noNav) { const index = this.childDocs.indexOf(doc); if (index >= 0 && index < this.childDocs.length) { @@ -1139,7 +1054,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { this.updateCurrentPresentation(DocCast(doc.embedContainer)); }; - //Command click + // Command click @action multiSelect = (doc: Doc, ref: HTMLElement, drag: HTMLElement) => { if (!this.selectedArray.has(doc)) { @@ -1154,7 +1069,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { this.selectPres(); }; - //Shift click + // Shift click @action shiftSelect = (doc: Doc, ref: HTMLElement, drag: HTMLElement) => { this.clearSelectedArray(); @@ -1169,7 +1084,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { this.selectPres(); }; - //regular click + // regular click @action regularSelect = (doc: Doc, ref: HTMLElement, drag: HTMLElement, noNav: boolean, selectPres = true) => { this.clearSelectedArray(); @@ -1200,9 +1115,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { if (this.layoutDoc.presentation_status === 'edit') { undoBatch( action(() => { - for (const doc of this.selectedArray) { - this.removeDocument(doc); - } + Array.from(this.selectedArray).forEach(doc => this.removeDocument(doc)); this.clearSelectedArray(); this._eleArray.length = 0; this._dragArray.length = 0; @@ -1269,8 +1182,8 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { this.childDocs.forEach(doc => this.addToSelectedArray(doc)); handled = true; } - default: break; + default: } if (handled) { e.stopPropagation(); @@ -1285,11 +1198,12 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { const order: JSX.Element[] = []; const docs = new Set<Doc>(); const presCollection = collection; - const dv = DocumentManager.Instance.getDocumentView(presCollection); + const dv = DocumentView.getDocumentView(presCollection); this.childDocs.forEach((doc, index) => { const tagDoc = PresBox.targetRenderedDoc(doc); const srcContext = Cast(tagDoc.embedContainer, Doc, null); const labelCreator = (top: number, left: number, edge: number, fontSize: number) => ( + // eslint-disable-next-line react/no-array-index-key <div className="pathOrder" key={tagDoc.id + 'pres' + index} style={{ top, left, width: edge, height: edge, fontSize }} onClick={() => this.selectElement(doc)}> <div className="pathOrder-frame">{index + 1}</div> </div> @@ -1322,7 +1236,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { order.push( <> {labelCreator(top - indEdge / 2, left - indEdge / 2, indEdge, indFontSize)} - <div className="pathOrder-presPinView" style={{ top, left, width, height, borderWidth: indEdge / 10 }}></div> + <div className="pathOrder-presPinView" style={{ top, left, width, height, borderWidth: indEdge / 10 }} /> </> ); } @@ -1345,17 +1259,12 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { .filter(doc => PresBox.targetRenderedDoc(doc)?.embedContainer === collection) .forEach((doc, index) => { const tagDoc = PresBox.targetRenderedDoc(doc); - if (tagDoc) { - const n1x = NumCast(tagDoc.x) + NumCast(tagDoc._width) / 2; - const n1y = NumCast(tagDoc.y) + NumCast(tagDoc._height) / 2; - if ((index = 0)) pathPoints = n1x + ',' + n1y; - else pathPoints = pathPoints + ' ' + n1x + ',' + n1y; - } else if (doc.config_pinView) { - const n1x = NumCast(doc.config_panX); - const n1y = NumCast(doc.config_panY); - if ((index = 0)) pathPoints = n1x + ',' + n1y; - else pathPoints = pathPoints + ' ' + n1x + ',' + n1y; - } + const [n1x, n1y] = tagDoc // + ? [NumCast(tagDoc.x) + NumCast(tagDoc._width) / 2, NumCast(tagDoc.y) + NumCast(tagDoc._height) / 2] + : [NumCast(doc.config_panX), NumCast(doc.config_panY)]; + + if (index === 0) pathPoints = n1x + ',' + n1y; + else pathPoints = pathPoints + ' ' + n1x + ',' + n1y; }); return ( <> @@ -1401,7 +1310,14 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { @undoBatch updateTransitionTime = (number: String, change?: number) => { - PresBox.SetTransitionTime(number, (timeInMS: number) => this.selectedArray.forEach(doc => (doc.presentation_transition = timeInMS)), change); + PresBox.SetTransitionTime( + number, + (timeInMS: number) => + this.selectedArray.forEach(doc => { + doc.presentation_transition = timeInMS; + }), + change + ); }; // Converts seconds to ms and updates presentation_transition @@ -1411,7 +1327,9 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { if (change) scale += change; if (scale < 0.01) scale = 0.01; if (scale > 1) scale = 1; - this.selectedArray.forEach(doc => (doc.config_zoom = scale)); + this.selectedArray.forEach(doc => { + doc.config_zoom = scale; + }); }; /* @@ -1423,76 +1341,96 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { if (change) timeInMS += change; if (timeInMS < 100) timeInMS = 100; if (timeInMS > 20000) timeInMS = 20000; - this.selectedArray.forEach(doc => (doc.presentation_duration = timeInMS)); + this.selectedArray.forEach(doc => { + doc.presentation_duration = timeInMS; + }); }; @undoBatch - updateMovement = action((movement: PresMovement, all?: boolean) => (all ? this.childDocs : this.selectedArray).forEach(doc => (doc.presentation_movement = movement))); + updateMovement = action((movement: PresMovement, all?: boolean) => + (all ? this.childDocs : this.selectedArray).forEach(doc => { + doc.presentation_movement = movement; + }) + ); @undoBatch updateHideBefore = (activeItem: Doc) => { activeItem.presentation_hideBefore = !activeItem.presentation_hideBefore; - this.selectedArray.forEach(doc => (doc.presentation_hideBefore = activeItem.presentation_hideBefore)); + this.selectedArray.forEach(doc => { + doc.presentation_hideBefore = activeItem.presentation_hideBefore; + }); }; @undoBatch updateHide = (activeItem: Doc) => { activeItem.presentation_hide = !activeItem.presentation_hide; - this.selectedArray.forEach(doc => (doc.presentation_hide = activeItem.presentation_hide)); + this.selectedArray.forEach(doc => { + doc.presentation_hide = activeItem.presentation_hide; + }); }; @undoBatch updateHideAfter = (activeItem: Doc) => { activeItem.presentation_hideAfter = !activeItem.presentation_hideAfter; - this.selectedArray.forEach(doc => (doc.presentation_hideAfter = activeItem.presentation_hideAfter)); + this.selectedArray.forEach(doc => { + doc.presentation_hideAfter = activeItem.presentation_hideAfter; + }); }; @undoBatch updateOpenDoc = (activeItem: Doc) => { activeItem.presentation_openInLightbox = !activeItem.presentation_openInLightbox; - this.selectedArray.forEach(doc => (doc.presentation_openInLightbox = activeItem.presentation_openInLightbox)); + this.selectedArray.forEach(doc => { + doc.presentation_openInLightbox = activeItem.presentation_openInLightbox; + }); }; @undoBatch updateEaseFunc = (activeItem: Doc) => { activeItem.presEaseFunc = activeItem.presEaseFunc === 'linear' ? 'ease' : 'linear'; - this.selectedArray.forEach(doc => (doc.presEaseFunc = activeItem.presEaseFunc)); + this.selectedArray.forEach(doc => { + doc.presEaseFunc = activeItem.presEaseFunc; + }); }; @undoBatch - updateEffectDirection = (effect: PresEffectDirection, all?: boolean) => (all ? this.childDocs : this.selectedArray).forEach(doc => (doc.presentation_effectDirection = effect)); + updateEffectDirection = (effect: PresEffectDirection, all?: boolean) => + (all ? this.childDocs : this.selectedArray).forEach(doc => { + doc.presentation_effectDirection = effect; + }); @undoBatch - updateEffect = (effect: PresEffect, bullet: boolean, all?: boolean) => (all ? this.childDocs : this.selectedArray).forEach(doc => (bullet ? (doc.presBulletEffect = effect) : (doc.presentation_effect = effect))); + 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; static endBatch = () => { PresBox._sliderBatch.end(); document.removeEventListener('pointerup', PresBox.endBatch, true); }; - public static inputter = (min: string, step: string, max: string, value: number, active: boolean, change: (val: string) => void, hmargin?: number) => { - return ( - <input - type="range" - step={step} - min={min} - max={max} - value={value} - readOnly={true} - style={{ marginLeft: hmargin, marginRight: hmargin, width: `calc(100% - ${2 * (hmargin ?? 0)}px)`, background: SettingsManager.userColor, color: SettingsManager.userVariantColor }} - className={`toolbar-slider ${active ? '' : 'none'}`} - onPointerDown={e => { - PresBox._sliderBatch = UndoManager.StartBatch('pres slider'); - document.addEventListener('pointerup', PresBox.endBatch, true); - e.stopPropagation(); - }} - onChange={e => { - e.stopPropagation(); - change(e.target.value); - }} - /> - ); - }; + public static inputter = (min: string, step: string, max: string, value: number, active: boolean, change: (val: string) => void, hmargin?: number) => ( + <input + type="range" + step={step} + min={min} + max={max} + value={value} + readOnly + style={{ marginLeft: hmargin, marginRight: hmargin, width: `calc(100% - ${2 * (hmargin ?? 0)}px)`, background: SnappingManager.userColor, color: SnappingManager.userVariantColor }} + className={`toolbar-slider ${active ? '' : 'none'}`} + onPointerDown={e => { + PresBox._sliderBatch = UndoManager.StartBatch('pres slider'); + document.addEventListener('pointerup', PresBox.endBatch, true); + e.stopPropagation(); + }} + onChange={e => { + e.stopPropagation(); + change(e.target.value); + }} + /> + ); @undoBatch applyTo = (array: Doc[]) => { @@ -1500,17 +1438,18 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { this.updateEffect(this.activeItem.presentation_effect as PresEffect, false, true); this.updateEffect(this.activeItem.presBulletEffect as PresEffect, true, true); this.updateEffectDirection(this.activeItem.presentation_effectDirection as PresEffectDirection, true); - const { presentation_transition, presentation_duration, presentation_hideBefore, presentation_hideAfter } = this.activeItem; + // eslint-disable-next-line camelcase + const { presentation_transition: pt, presentation_duration: pd, presentation_hideBefore: ph, presentation_hideAfter: pa } = this.activeItem; array.forEach(curDoc => { - curDoc.presentation_transition = presentation_transition; - curDoc.presentation_duration = presentation_duration; - curDoc.presentation_hideBefore = presentation_hideBefore; - curDoc.presentation_hideAfter = presentation_hideAfter; + curDoc.presentation_transition = pt; + curDoc.presentation_duration = pd; + curDoc.presentation_hideBefore = ph; + curDoc.presentation_hideAfter = pa; }); }; @computed get visibilityDurationDropdown() { - const activeItem = this.activeItem; + const { activeItem } = this; if (activeItem && this.targetDoc) { const targetType = this.targetDoc.type; let duration = activeItem.presentation_duration ? NumCast(activeItem.presentation_duration) / 1000 : 0; @@ -1521,36 +1460,36 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { <Tooltip title={<div className="dash-tooltip">Hide before presented</div>}> <div className={`ribbon-toggle ${activeItem.presentation_hideBefore ? 'active' : ''}`} - style={{ border: `solid 1px ${SettingsManager.userColor}`, color: SettingsManager.userColor, background: activeItem.presentation_hideBefore ? SettingsManager.userVariantColor : SettingsManager.userBackgroundColor }} + style={{ border: `solid 1px ${SnappingManager.userColor}`, color: SnappingManager.userColor, background: activeItem.presentation_hideBefore ? SnappingManager.userVariantColor : SnappingManager.userBackgroundColor }} onClick={() => this.updateHideBefore(activeItem)}> Hide before </div> </Tooltip> - <Tooltip title={<div className="dash-tooltip">{'Hide while presented'}</div>}> + <Tooltip title={<div className="dash-tooltip">Hide while presented</div>}> <div className={`ribbon-toggle ${activeItem.presentation_hide ? 'active' : ''}`} - style={{ border: `solid 1px ${SettingsManager.userColor}`, color: SettingsManager.userColor, background: activeItem.presentation_hide ? SettingsManager.userVariantColor : SettingsManager.userBackgroundColor }} + style={{ border: `solid 1px ${SnappingManager.userColor}`, color: SnappingManager.userColor, background: activeItem.presentation_hide ? SnappingManager.userVariantColor : SnappingManager.userBackgroundColor }} onClick={() => this.updateHide(activeItem)}> Hide </div> </Tooltip> - <Tooltip title={<div className="dash-tooltip">{'Hide after presented'}</div>}> + <Tooltip title={<div className="dash-tooltip">Hide after presented</div>}> <div className={`ribbon-toggle ${activeItem.presentation_hideAfter ? 'active' : ''}`} - style={{ border: `solid 1px ${SettingsManager.userColor}`, color: SettingsManager.userColor, background: activeItem.presentation_hideAfter ? SettingsManager.userVariantColor : SettingsManager.userBackgroundColor }} + style={{ border: `solid 1px ${SnappingManager.userColor}`, color: SnappingManager.userColor, background: activeItem.presentation_hideAfter ? SnappingManager.userVariantColor : SnappingManager.userBackgroundColor }} onClick={() => this.updateHideAfter(activeItem)}> Hide after </div> </Tooltip> - <Tooltip title={<div className="dash-tooltip">{'Open in lightbox view'}</div>}> + <Tooltip title={<div className="dash-tooltip">Open in lightbox view</div>}> <div className="ribbon-toggle" style={{ - border: `solid 1px ${SettingsManager.userColor}`, - color: SettingsManager.userColor, - background: activeItem.presentation_openInLightbox ? SettingsManager.userVariantColor : SettingsManager.userBackgroundColor, + border: `solid 1px ${SnappingManager.userColor}`, + color: SnappingManager.userColor, + background: activeItem.presentation_openInLightbox ? SnappingManager.userVariantColor : SnappingManager.userBackgroundColor, }} onClick={() => this.updateOpenDoc(activeItem)}> Lightbox @@ -1559,30 +1498,30 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { <Tooltip title={<div className="dash-tooltip">Transition movement style</div>}> <div className="ribbon-toggle" - style={{ border: `solid 1px ${SettingsManager.userColor}`, color: SettingsManager.userColor, background: activeItem.presEaseFunc === 'ease' ? SettingsManager.userVariantColor : SettingsManager.userBackgroundColor }} + style={{ border: `solid 1px ${SnappingManager.userColor}`, color: SnappingManager.userColor, background: activeItem.presEaseFunc === 'ease' ? SnappingManager.userVariantColor : SnappingManager.userBackgroundColor }} onClick={() => this.updateEaseFunc(activeItem)}> {`${StrCast(activeItem.presEaseFunc, 'ease')}`} </div> </Tooltip> </div> - {[DocumentType.AUDIO, DocumentType.VID].includes(targetType as any as DocumentType) ? null : ( + {[DocumentType.AUDIO, DocumentType.VID].find(dt => dt === targetType) ? null : ( <> <div className="ribbon-doubleButton"> <div className="presBox-subheading">Slide Duration</div> - <div className="ribbon-property" style={{ border: `solid 1px ${SettingsManager.userColor}` }}> - <input className="presBox-input" type="number" readOnly={true} value={duration} onKeyDown={e => e.stopPropagation()} onChange={e => this.updateDurationTime(e.target.value)} /> s + <div className="ribbon-property" style={{ border: `solid 1px ${SnappingManager.userColor}` }}> + <input className="presBox-input" type="number" readOnly value={duration} onKeyDown={e => e.stopPropagation()} onChange={e => this.updateDurationTime(e.target.value)} /> s </div> - <div className="ribbon-propertyUpDown" style={{ color: SettingsManager.userBackgroundColor, background: SettingsManager.userColor }}> + <div className="ribbon-propertyUpDown" style={{ color: SnappingManager.userBackgroundColor, background: SnappingManager.userColor }}> <div className="ribbon-propertyUpDownItem" onClick={() => this.updateDurationTime(String(duration), 1000)}> - <FontAwesomeIcon icon={'caret-up'} /> + <FontAwesomeIcon icon="caret-up" /> </div> <div className="ribbon-propertyUpDownItem" onClick={() => this.updateDurationTime(String(duration), -1000)}> - <FontAwesomeIcon icon={'caret-down'} /> + <FontAwesomeIcon icon="caret-down" /> </div> </div> </div> {PresBox.inputter('0.1', '0.1', '20', duration, targetType !== DocumentType.AUDIO, this.updateDurationTime)} - <div className={'slider-headers'} style={{ display: targetType === DocumentType.AUDIO ? 'none' : 'grid' }}> + <div className="slider-headers" style={{ display: targetType === DocumentType.AUDIO ? 'none' : 'grid' }}> <div className="slider-text">Short</div> <div className="slider-text">Medium</div> <div className="slider-text">Long</div> @@ -1592,17 +1531,18 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { </div> ); } + return undefined; } @computed get progressivizeDropdown() { - const activeItem = this.activeItem; + const { activeItem } = this; if (activeItem && this.targetDoc) { const effect = activeItem.presBulletEffect ? activeItem.presBulletEffect : PresMovement.None; - const bulletEffect = (effect: PresEffect) => ( + const bulletEffect = (presEffect: PresEffect) => ( <div - className={`presBox-dropdownOption ${activeItem.presentation_effect === effect || (effect === PresEffect.None && !activeItem.presentation_effect) ? 'active' : ''}`} + className={`presBox-dropdownOption ${activeItem.presentation_effect === presEffect || (presEffect === PresEffect.None && !activeItem.presentation_effect) ? 'active' : ''}`} onPointerDown={StopEvent} - onClick={() => this.updateEffect(effect, true)}> - {effect} + onClick={() => this.updateEffect(presEffect, true)}> + {presEffect} </div> ); return ( @@ -1611,7 +1551,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { <div className="presBox-subheading">Progressivize Collection</div> <input className="presBox-checkbox" - style={{ margin: 10, border: `solid 1px ${SettingsManager.userColor}` }} + style={{ margin: 10, border: `solid 1px ${SnappingManager.userColor}` }} type="checkbox" onChange={() => { activeItem.presentation_indexed = activeItem.presentation_indexed === undefined ? 0 : undefined; @@ -1622,21 +1562,23 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { // a progressivized slide doesn't have sub-slides, but rather iterates over the data list of the target being progressivized. // to avoid creating a new slide to correspond to each of the target's data list, we create a computedField to refernce the target's data list. let dataField = Doc.LayoutFieldKey(tagDoc); - if (Cast(tagDoc[dataField], listSpec(Doc), null)?.filter(d => d instanceof Doc) === undefined) dataField = dataField + '_annotations'; + if (Cast(tagDoc[dataField], listSpec(Doc), null)?.filter(d => d instanceof Doc) === undefined) dataField += '_annotations'; if (DocCast(activeItem.presentation_targetDoc).annotationOn) activeItem.data = ComputedField.MakeFunction(`this.presentation_targetDoc.annotationOn?.["${dataField}"]`); else activeItem.data = ComputedField.MakeFunction(`this.presentation_targetDoc?.["${dataField}"]`); }} - checked={Cast(activeItem.presentation_indexed, 'number', null) !== undefined ? true : false} + checked={Cast(activeItem.presentation_indexed, 'number', null) !== undefined} /> </div> <div className="ribbon-doubleButton" style={{ display: 'inline-flex' }}> <div className="presBox-subheading">Progressivize First Bullet</div> <input className="presBox-checkbox" - style={{ margin: 10, border: `solid 1px ${SettingsManager.userColor}` }} + style={{ margin: 10, border: `solid 1px ${SnappingManager.userColor}` }} type="checkbox" - onChange={() => (activeItem.presentation_indexedStart = activeItem.presentation_indexedStart ? 0 : 1)} + onChange={() => { + activeItem.presentation_indexedStart = activeItem.presentation_indexedStart ? 0 : 1; + }} checked={!NumCast(activeItem.presentation_indexedStart)} /> </div> @@ -1644,9 +1586,11 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { <div className="presBox-subheading">Expand Current Bullet</div> <input className="presBox-checkbox" - style={{ margin: 10, border: `solid 1px ${SettingsManager.userColor}` }} + style={{ margin: 10, border: `solid 1px ${SnappingManager.userColor}` }} type="checkbox" - onChange={() => (activeItem.presBulletExpand = !activeItem.presBulletExpand)} + onChange={() => { + activeItem.presBulletExpand = !activeItem.presBulletExpand; + }} checked={BoolCast(activeItem.presBulletExpand)} /> </div> @@ -1660,20 +1604,20 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { this._openBulletEffectDropdown = !this._openBulletEffectDropdown; })} style={{ - color: SettingsManager.userColor, - background: SettingsManager.userVariantColor, + color: SnappingManager.userColor, + background: SnappingManager.userVariantColor, borderBottomLeftRadius: this._openBulletEffectDropdown ? 0 : 5, - border: this._openBulletEffectDropdown ? `solid 2px ${SettingsManager.userVariantColor}` : `solid 1px ${SettingsManager.userColor}`, + border: this._openBulletEffectDropdown ? `solid 2px ${SnappingManager.userVariantColor}` : `solid 1px ${SnappingManager.userColor}`, }}> {effect?.toString()} - <FontAwesomeIcon className="presBox-dropdownIcon" style={{ gridColumn: 2, color: this._openBulletEffectDropdown ? Colors.MEDIUM_BLUE : 'black' }} icon={'angle-down'} /> + <FontAwesomeIcon className="presBox-dropdownIcon" style={{ gridColumn: 2, color: this._openBulletEffectDropdown ? Colors.MEDIUM_BLUE : 'black' }} icon="angle-down" /> <div - className={'presBox-dropdownOptions'} - style={{ display: this._openBulletEffectDropdown ? 'grid' : 'none', color: SettingsManager.userColor, background: SettingsManager.userBackgroundColor }} + className="presBox-dropdownOptions" + style={{ display: this._openBulletEffectDropdown ? 'grid' : 'none', color: SnappingManager.userColor, background: SnappingManager.userBackgroundColor }} onPointerDown={e => e.stopPropagation()}> {Object.values(PresEffect) .filter(v => isNaN(Number(v))) - .map(effect => bulletEffect(effect))} + .map(peffect => bulletEffect(peffect))} </div> </div> </div> @@ -1683,7 +1627,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { return null; } @computed get transitionDropdown() { - const activeItem = this.activeItem; + const { activeItem } = this; const preseEffect = (effect: PresEffect) => ( <div className={`presBox-dropdownOption ${activeItem.presentation_effect === effect || (effect === PresEffect.None && !activeItem.presentation_effect) ? 'active' : ''}`} @@ -1698,7 +1642,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { </div> ); const presDirection = (direction: PresEffectDirection, icon: string, gridColumn: number, gridRow: number, opts: object) => { - const color = activeItem.presentation_effectDirection === direction || (direction === PresEffectDirection.Center && !activeItem.presentation_effectDirection) ? SettingsManager.userVariantColor : SettingsManager.userColor; + const color = activeItem.presentation_effectDirection === direction || (direction === PresEffectDirection.Center && !activeItem.presentation_effectDirection) ? SnappingManager.userVariantColor : SnappingManager.userColor; return ( <Tooltip title={<div className="dash-tooltip">{direction}</div>}> <div @@ -1733,20 +1677,20 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { this._openMovementDropdown = !this._openMovementDropdown; })} style={{ - color: SettingsManager.userColor, - background: SettingsManager.userVariantColor, + color: SnappingManager.userColor, + background: SnappingManager.userVariantColor, borderBottomLeftRadius: this._openMovementDropdown ? 0 : 5, - border: this._openMovementDropdown ? `solid 2px ${SettingsManager.userVariantColor}` : `solid 1px ${SettingsManager.userColor}`, + border: this._openMovementDropdown ? `solid 2px ${SnappingManager.userVariantColor}` : `solid 1px ${SnappingManager.userColor}`, }}> {this.movementName(activeItem)} - <FontAwesomeIcon className="presBox-dropdownIcon" style={{ gridColumn: 2, color: this._openMovementDropdown ? Colors.MEDIUM_BLUE : 'black' }} icon={'angle-down'} /> + <FontAwesomeIcon className="presBox-dropdownIcon" style={{ gridColumn: 2, color: this._openMovementDropdown ? Colors.MEDIUM_BLUE : 'black' }} icon="angle-down" /> <div className="presBox-dropdownOptions" - id={'presBoxMovementDropdown'} + id="presBoxMovementDropdown" onPointerDown={StopEvent} style={{ - color: SettingsManager.userColor, - background: SettingsManager.userBackgroundColor, + color: SnappingManager.userColor, + background: SnappingManager.userBackgroundColor, display: this._openMovementDropdown ? 'grid' : 'none', }}> {presMovement(PresMovement.None)} @@ -1758,35 +1702,35 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { </div> <div className="ribbon-doubleButton" style={{ display: activeItem.presentation_movement === PresMovement.Zoom ? 'inline-flex' : 'none' }}> <div className="presBox-subheading">Zoom (% screen filled)</div> - <div className="ribbon-property" style={{ border: `solid 1px ${SettingsManager.userColor}` }}> - <input className="presBox-input" type="number" readOnly={true} value={zoom} onChange={e => this.updateZoom(e.target.value)} />% + <div className="ribbon-property" style={{ border: `solid 1px ${SnappingManager.userColor}` }}> + <input className="presBox-input" type="number" readOnly value={zoom} onChange={e => this.updateZoom(e.target.value)} />% </div> - <div className="ribbon-propertyUpDown" style={{ color: SettingsManager.userBackgroundColor, background: SettingsManager.userColor }}> + <div className="ribbon-propertyUpDown" style={{ color: SnappingManager.userBackgroundColor, background: SnappingManager.userColor }}> <div className="ribbon-propertyUpDownItem" onClick={() => this.updateZoom(String(zoom), 0.1)}> - <FontAwesomeIcon icon={'caret-up'} /> + <FontAwesomeIcon icon="caret-up" /> </div> <div className="ribbon-propertyUpDownItem" onClick={() => this.updateZoom(String(zoom), -0.1)}> - <FontAwesomeIcon icon={'caret-down'} /> + <FontAwesomeIcon icon="caret-down" /> </div> </div> </div> {PresBox.inputter('0', '1', '100', zoom, activeItem.presentation_movement === PresMovement.Zoom, this.updateZoom)} <div className="ribbon-doubleButton" style={{ display: 'inline-flex' }}> <div className="presBox-subheading">Transition Time</div> - <div className="ribbon-property" style={{ border: `solid 1px ${SettingsManager.userColor}` }}> - <input className="presBox-input" type="number" readOnly={true} value={transitionSpeed} onKeyDown={e => e.stopPropagation()} onChange={action(e => this.updateTransitionTime(e.target.value))} /> s + <div className="ribbon-property" style={{ border: `solid 1px ${SnappingManager.userColor}` }}> + <input className="presBox-input" type="number" readOnly value={transitionSpeed} onKeyDown={e => e.stopPropagation()} onChange={action(e => this.updateTransitionTime(e.target.value))} /> s </div> - <div className="ribbon-propertyUpDown" style={{ color: SettingsManager.userBackgroundColor, background: SettingsManager.userColor }}> + <div className="ribbon-propertyUpDown" style={{ color: SnappingManager.userBackgroundColor, background: SnappingManager.userColor }}> <div className="ribbon-propertyUpDownItem" onClick={() => this.updateTransitionTime(String(transitionSpeed), 1000)}> - <FontAwesomeIcon icon={'caret-up'} /> + <FontAwesomeIcon icon="caret-up" /> </div> <div className="ribbon-propertyUpDownItem" onClick={() => this.updateTransitionTime(String(transitionSpeed), -1000)}> - <FontAwesomeIcon icon={'caret-down'} /> + <FontAwesomeIcon icon="caret-down" /> </div> </div> </div> {PresBox.inputter('0.1', '0.1', '100', transitionSpeed, true, this.updateTransitionTime)} - <div className={'slider-headers'}> + <div className="slider-headers"> <div className="slider-text">Fast</div> <div className="slider-text">Medium</div> <div className="slider-text">Slow</div> @@ -1798,9 +1742,11 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { <div className="presBox-subheading">Play Audio Annotation</div> <input className="presBox-checkbox" - style={{ margin: 10, border: `solid 1px ${SettingsManager.userColor}` }} + style={{ margin: 10, border: `solid 1px ${SnappingManager.userColor}` }} type="checkbox" - onChange={() => (activeItem.presPlayAudio = !BoolCast(activeItem.presPlayAudio))} + onChange={() => { + activeItem.presPlayAudio = !BoolCast(activeItem.presPlayAudio); + }} checked={BoolCast(activeItem.presPlayAudio)} /> </div> @@ -1808,9 +1754,11 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { <div className="presBox-subheading">Zoom Text Selections</div> <input className="presBox-checkbox" - style={{ margin: 10, border: `solid 1px ${SettingsManager.userColor}` }} + style={{ margin: 10, border: `solid 1px ${SnappingManager.userColor}` }} type="checkbox" - onChange={() => (activeItem.presentation_zoomText = !BoolCast(activeItem.presentation_zoomText))} + onChange={() => { + activeItem.presentation_zoomText = !BoolCast(activeItem.presentation_zoomText); + }} checked={BoolCast(activeItem.presentation_zoomText)} /> </div> @@ -1821,30 +1769,30 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { this._openEffectDropdown = !this._openEffectDropdown; })} style={{ - color: SettingsManager.userColor, - background: SettingsManager.userVariantColor, + color: SnappingManager.userColor, + background: SnappingManager.userVariantColor, borderBottomLeftRadius: this._openEffectDropdown ? 0 : 5, - border: this._openEffectDropdown ? `solid 2px ${SettingsManager.userVariantColor}` : `solid 1px ${SettingsManager.userColor}`, + border: this._openEffectDropdown ? `solid 2px ${SnappingManager.userVariantColor}` : `solid 1px ${SnappingManager.userColor}`, }}> {effect?.toString()} - <FontAwesomeIcon className="presBox-dropdownIcon" style={{ gridColumn: 2, color: this._openEffectDropdown ? Colors.MEDIUM_BLUE : 'black' }} icon={'angle-down'} /> + <FontAwesomeIcon className="presBox-dropdownIcon" style={{ gridColumn: 2, color: this._openEffectDropdown ? Colors.MEDIUM_BLUE : 'black' }} icon="angle-down" /> <div className="presBox-dropdownOptions" - id={'presBoxMovementDropdown'} + id="presBoxMovementDropdown" style={{ - color: SettingsManager.userColor, - background: SettingsManager.userBackgroundColor, + color: SnappingManager.userColor, + background: SnappingManager.userBackgroundColor, display: this._openEffectDropdown ? 'grid' : 'none', }} onPointerDown={e => e.stopPropagation()}> {Object.values(PresEffect) .filter(v => isNaN(Number(v))) - .map(effect => preseEffect(effect))} + .map(presEffect => preseEffect(presEffect))} </div> </div> <div className="ribbon-doubleButton" style={{ display: effect === PresEffectDirection.None ? 'none' : 'inline-flex' }}> <div className="presBox-subheading">Effect direction</div> - <div className="ribbon-property" style={{ border: `solid 1px ${SettingsManager.userColor}` }}> + <div className="ribbon-property" style={{ border: `solid 1px ${SnappingManager.userColor}` }}> {StrCast(this.activeItem.presentation_effectDirection)} </div> </div> @@ -1864,33 +1812,36 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { </div> ); } + return undefined; } @computed get mediaOptionsDropdown() { - const activeItem = this.activeItem; + const { activeItem } = this; if (activeItem && this.targetDoc) { const renderTarget = PresBox.targetRenderedDoc(this.activeItem); const clipStart = NumCast(renderTarget.clipStart); const clipEnd = NumCast(renderTarget.clipEnd, clipStart + NumCast(renderTarget[Doc.LayoutFieldKey(renderTarget) + '_duration'])); - const config_clipEnd = NumCast(activeItem.config_clipEnd) < NumCast(activeItem.config_clipStart) ? clipEnd - clipStart : NumCast(activeItem.config_clipEnd); + const configClipEnd = NumCast(activeItem.config_clipEnd) < NumCast(activeItem.config_clipStart) ? clipEnd - clipStart : NumCast(activeItem.config_clipEnd); return ( - <div className={'presBox-ribbon'} onClick={e => e.stopPropagation()} onPointerUp={e => e.stopPropagation()} onPointerDown={e => e.stopPropagation()}> + <div className="presBox-ribbon" onClick={e => e.stopPropagation()} onPointerUp={e => e.stopPropagation()} onPointerDown={e => e.stopPropagation()}> <div> <div className="ribbon-box"> - Start {'&'} End Time - <div className={'slider-headers'}> + Start & End Time + <div className="slider-headers"> <div className="slider-block"> <div className="slider-text" style={{ fontWeight: 500 }}> Start time (s) </div> - <div id="startTime" className="slider-number" style={{ color: SettingsManager.userColor, backgroundColor: SettingsManager.userBackgroundColor }}> + <div id="startTime" className="slider-number" style={{ color: SnappingManager.userColor, backgroundColor: SnappingManager.userBackgroundColor }}> <input className="presBox-input" style={{ textAlign: 'center', width: '100%', height: 15, fontSize: 10 }} type="number" - readOnly={true} + readOnly value={NumCast(activeItem.config_clipStart).toFixed(2)} onKeyDown={e => e.stopPropagation()} - onChange={action(e => (activeItem.config_clipStart = Number(e.target.value)))} + onChange={action(e => { + activeItem.config_clipStart = Number(e.target.value); + })} /> </div> </div> @@ -1898,23 +1849,25 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { <div className="slider-text" style={{ fontWeight: 500 }}> Duration (s) </div> - <div className="slider-number" style={{ color: SettingsManager.userColor, backgroundColor: SettingsManager.userBackgroundColor }}> - {Math.round((config_clipEnd - NumCast(activeItem.config_clipStart)) * 10) / 10} + <div className="slider-number" style={{ color: SnappingManager.userColor, backgroundColor: SnappingManager.userBackgroundColor }}> + {Math.round((configClipEnd - NumCast(activeItem.config_clipStart)) * 10) / 10} </div> </div> <div className="slider-block"> <div className="slider-text" style={{ fontWeight: 500 }}> End time (s) </div> - <div id="endTime" className="slider-number" style={{ color: SettingsManager.userColor, backgroundColor: SettingsManager.userBackgroundColor }}> + <div id="endTime" className="slider-number" style={{ color: SnappingManager.userColor, backgroundColor: SnappingManager.userBackgroundColor }}> <input className="presBox-input" onKeyDown={e => e.stopPropagation()} style={{ textAlign: 'center', width: '100%', height: 15, fontSize: 10 }} type="number" - readOnly={true} - value={config_clipEnd.toFixed(2)} - onChange={action(e => (activeItem.config_clipEnd = Number(e.target.value)))} + readOnly + value={configClipEnd.toFixed(2)} + onChange={action(e => { + activeItem.config_clipEnd = Number(e.target.value); + })} /> </div> </div> @@ -1925,15 +1878,15 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { step="0.1" min={clipStart} max={clipEnd} - value={config_clipEnd} - style={{ gridColumn: 1, gridRow: 1, background: SettingsManager.userColor, color: SettingsManager.userVariantColor }} + value={configClipEnd} + style={{ gridColumn: 1, gridRow: 1, background: SnappingManager.userColor, color: SnappingManager.userVariantColor }} className={`toolbar-slider ${'end'}`} id="toolbar-slider" onPointerDown={e => { this._batch = UndoManager.StartBatch('config_clipEnd'); const endBlock = document.getElementById('endTime'); if (endBlock) { - endBlock.style.backgroundColor = SettingsManager.userVariantColor; + endBlock.style.backgroundColor = SnappingManager.userVariantColor ?? ''; } e.stopPropagation(); }} @@ -1941,7 +1894,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { this._batch?.end(); const endBlock = document.getElementById('endTime'); if (endBlock) { - endBlock.style.backgroundColor = SettingsManager.userBackgroundColor; + endBlock.style.backgroundColor = SnappingManager.userBackgroundColor ?? ''; } }} onChange={(e: React.ChangeEvent<HTMLInputElement>) => { @@ -1962,7 +1915,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { this._batch = UndoManager.StartBatch('config_clipStart'); const startBlock = document.getElementById('startTime'); if (startBlock) { - startBlock.style.backgroundColor = SettingsManager.userVariantColor; + startBlock.style.backgroundColor = SnappingManager.userVariantColor ?? ''; } e.stopPropagation(); }} @@ -1970,7 +1923,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { this._batch?.end(); const startBlock = document.getElementById('startTime'); if (startBlock) { - startBlock.style.backgroundColor = SettingsManager.userBackgroundColor; + startBlock.style.backgroundColor = SnappingManager.userBackgroundColor ?? ''; } }} onChange={(e: React.ChangeEvent<HTMLInputElement>) => { @@ -1981,7 +1934,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { </div> <div className="slider-headers"> <div className="slider-text">{clipStart.toFixed(2)} s</div> - <div className="slider-text"></div> + <div className="slider-text" /> <div className="slider-text">{clipEnd.toFixed(2)} s</div> </div> </div> @@ -1993,8 +1946,10 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { <input className="presBox-checkbox" type="checkbox" - style={{ border: `solid 1px ${SettingsManager.userColor}` }} - onChange={() => (activeItem.presentation_mediaStart = 'manual')} + style={{ border: `solid 1px ${SnappingManager.userColor}` }} + onChange={() => { + activeItem.presentation_mediaStart = 'manual'; + }} checked={activeItem.presentation_mediaStart === 'manual'} /> <div>On click</div> @@ -2002,9 +1957,11 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { <div className="checkbox-container"> <input className="presBox-checkbox" - style={{ border: `solid 1px ${SettingsManager.userColor}` }} + style={{ border: `solid 1px ${SnappingManager.userColor}` }} type="checkbox" - onChange={() => (activeItem.presentation_mediaStart = 'auto')} + onChange={() => { + activeItem.presentation_mediaStart = 'auto'; + }} checked={activeItem.presentation_mediaStart === 'auto'} /> <div>Automatically</div> @@ -2016,8 +1973,10 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { <input className="presBox-checkbox" type="checkbox" - style={{ border: `solid 1px ${SettingsManager.userColor}` }} - onChange={() => (activeItem.presentation_mediaStop = 'manual')} + style={{ border: `solid 1px ${SnappingManager.userColor}` }} + onChange={() => { + activeItem.presentation_mediaStop = 'manual'; + }} checked={activeItem.presentation_mediaStop === 'manual'} /> <div>At media end time</div> @@ -2026,8 +1985,10 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { <input className="presBox-checkbox" type="checkbox" - style={{ border: `solid 1px ${SettingsManager.userColor}` }} - onChange={() => (activeItem.presentation_mediaStop = 'auto')} + style={{ border: `solid 1px ${SnappingManager.userColor}` }} + onChange={() => { + activeItem.presentation_mediaStop = 'auto'; + }} checked={activeItem.presentation_mediaStop === 'auto'} /> <div>On slide change</div> @@ -2055,6 +2016,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { </div> ); } + return undefined; } @computed get newDocumentToolbarDropdown() { return ( @@ -2118,9 +2080,9 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { @computed get newDocumentDropdown() { return ( - <div className={'presBox-ribbon'} onClick={e => e.stopPropagation()} onPointerDown={e => e.stopPropagation()}> + <div className="presBox-ribbon" onClick={e => e.stopPropagation()} onPointerDown={e => e.stopPropagation()}> <div className="ribbon-box"> - Slide Title: <br></br> + Slide Title: <br /> <input className="ribbon-textInput" placeholder="..." @@ -2129,16 +2091,31 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { onChange={e => { e.stopPropagation(); e.preventDefault(); - runInAction(() => (this.title = e.target.value)); - }}></input> + runInAction(() => { + this.title = e.target.value; + }); + }} + /> </div> <div className="ribbon-box"> Choose type: <div className="ribbon-doubleButton"> - <div title="Text" className={'ribbon-toggle'} style={{ background: this.addFreeform ? '' : Colors.LIGHT_BLUE }} onClick={action(() => (this.addFreeform = !this.addFreeform))}> + <div + title="Text" + className="ribbon-toggle" + style={{ background: this.addFreeform ? '' : Colors.LIGHT_BLUE }} + onClick={action(() => { + this.addFreeform = !this.addFreeform; + })}> Text </div> - <div title="Freeform" className={'ribbon-toggle'} style={{ background: this.addFreeform ? Colors.LIGHT_BLUE : '' }} onClick={action(() => (this.addFreeform = !this.addFreeform))}> + <div + title="Freeform" + className="ribbon-toggle" + style={{ background: this.addFreeform ? Colors.LIGHT_BLUE : '' }} + onClick={action(() => { + this.addFreeform = !this.addFreeform; + })}> Freeform </div> </div> @@ -2146,23 +2123,49 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { <div className="ribbon-box" style={{ display: this.addFreeform ? 'grid' : 'none' }}> Preset layouts: <div className="layout-container" style={{ height: this.openLayouts ? 'max-content' : '75px' }}> - <div className="layout" style={{ border: this.layout === 'blank' ? `solid 2px ${Colors.MEDIUM_BLUE}` : '' }} onClick={action(() => (this.layout = 'blank'))} /> - <div className="layout" style={{ border: this.layout === 'title' ? `solid 2px ${Colors.MEDIUM_BLUE}` : '' }} onClick={action(() => (this.layout = 'title'))}> + <div + className="layout" + style={{ border: this.layout === 'blank' ? `solid 2px ${Colors.MEDIUM_BLUE}` : '' }} + onClick={action(() => { + this.layout = 'blank'; + })} + /> + <div + className="layout" + style={{ border: this.layout === 'title' ? `solid 2px ${Colors.MEDIUM_BLUE}` : '' }} + onClick={action(() => { + this.layout = 'title'; + })}> <div className="title">Title</div> <div className="subtitle">Subtitle</div> </div> - <div className="layout" style={{ border: this.layout === 'header' ? `solid 2px ${Colors.MEDIUM_BLUE}` : '' }} onClick={action(() => (this.layout = 'header'))}> + <div + className="layout" + style={{ border: this.layout === 'header' ? `solid 2px ${Colors.MEDIUM_BLUE}` : '' }} + onClick={action(() => { + this.layout = 'header'; + })}> <div className="title" style={{ alignSelf: 'center', fontSize: 10 }}> Section header </div> </div> - <div className="layout" style={{ border: this.layout === 'content' ? `solid 2px ${Colors.MEDIUM_BLUE}` : '' }} onClick={action(() => (this.layout = 'content'))}> + <div + className="layout" + style={{ border: this.layout === 'content' ? `solid 2px ${Colors.MEDIUM_BLUE}` : '' }} + onClick={action(() => { + this.layout = 'content'; + })}> <div className="title" style={{ alignSelf: 'center' }}> Title </div> <div className="content">Text goes here</div> </div> - <div className="layout" style={{ border: this.layout === 'twoColumns' ? `solid 2px ${Colors.MEDIUM_BLUE}` : '' }} onClick={action(() => (this.layout = 'twoColumns'))}> + <div + className="layout" + style={{ border: this.layout === 'twoColumns' ? `solid 2px ${Colors.MEDIUM_BLUE}` : '' }} + onClick={action(() => { + this.layout = 'twoColumns'; + })}> <div className="title" style={{ alignSelf: 'center', gridColumn: '1/3' }}> Title </div> @@ -2174,8 +2177,12 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { </div> </div> </div> - <div className="open-layout" onClick={action(() => (this.openLayouts = !this.openLayouts))}> - <FontAwesomeIcon style={{ transition: 'all 0.3s', transform: this.openLayouts ? 'rotate(180deg)' : 'rotate(0deg)' }} icon={'caret-down'} size={'lg'} /> + <div + className="open-layout" + onClick={action(() => { + this.openLayouts = !this.openLayouts; + })}> + <FontAwesomeIcon style={{ transition: 'all 0.3s', transform: this.openLayouts ? 'rotate(180deg)' : 'rotate(0deg)' }} icon="caret-down" size="lg" /> </div> </div> <div className="ribbon-final-box"> @@ -2190,17 +2197,17 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { } createNewSlide = (layout?: string, title?: string, freeform?: boolean) => { - let doc = undefined; + let doc; if (layout) doc = this.createTemplate(layout); if (freeform && layout) doc = this.createTemplate(layout, title); if (!freeform && !layout) doc = Docs.Create.TextDocument('', { _nativeWidth: 400, _width: 225, title: title }); if (doc) { const tabMap = CollectionDockingView.Instance?.tabMap; - const tab = tabMap && Array.from(tabMap).find(tab => tab.DashDoc.type === DocumentType.COL)?.DashDoc; - const presCollection = DocumentManager.GetContextPath(this.activeItem).reverse().lastElement().presentation_targetDoc ?? tab; + const docTab = tabMap && Array.from(tabMap).find(tab => tab.DashDoc.type === DocumentType.COL)?.DashDoc; + const presCollection = DocumentView.getContextPath(this.activeItem).reverse().lastElement().presentation_targetDoc ?? docTab; const data = Cast(presCollection?.data, listSpec(Doc)); - const config_data = Cast(this.Document.data, listSpec(Doc)); - if (data && config_data) { + const configData = Cast(this.Document.data, listSpec(Doc)); + if (data && configData) { data.push(doc); this._props.pinToPres(doc, {}); this.gotoDocument(this.childDocs.length, this.activeItem); @@ -2222,12 +2229,14 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { const content2 = () => Docs.Create.TextDocument('Click to change text', { title: 'Column 2', _width: 185, _height: 140, x: 205, y: 80, _text_fontSize: '14pt' }); // prettier-ignore switch (layout) { - case 'blank': return Docs.Create.FreeformDocument([], { title: input ? input : 'Blank slide', _width: 400, _height: 225, x, y }); - case 'title': return Docs.Create.FreeformDocument([title(), subtitle()], { title: input ? input : 'Title slide', _width: 400, _height: 225, _freeform_fitContentsToBox: true, x, y }); - case 'header': return Docs.Create.FreeformDocument([header()], { title: input ? input : 'Section header', _width: 400, _height: 225, _freeform_fitContentsToBox: true, x, y }); - case 'content': return Docs.Create.FreeformDocument([contentTitle(), content()], { title: input ? input : 'Title and content', _width: 400, _height: 225, _freeform_fitContentsToBox: true, x, y }); - case 'twoColumns': return Docs.Create.FreeformDocument([contentTitle(), content1(), content2()], { title: input ? input : 'Title and two columns', _width: 400, _height: 225, _freeform_fitContentsToBox: true, x, y }) + case 'blank': return Docs.Create.FreeformDocument([], { title: input || 'Blank slide', _width: 400, _height: 225, x, y }); + case 'title': return Docs.Create.FreeformDocument([title(), subtitle()], { title: input || 'Title slide', _width: 400, _height: 225, _freeform_fitContentsToBox: true, x, y }); + case 'header': return Docs.Create.FreeformDocument([header()], { title: input || 'Section header', _width: 400, _height: 225, _freeform_fitContentsToBox: true, x, y }); + case 'content': return Docs.Create.FreeformDocument([contentTitle(), content()], { title: input || 'Title and content', _width: 400, _height: 225, _freeform_fitContentsToBox: true, x, y }); + case 'twoColumns': return Docs.Create.FreeformDocument([contentTitle(), content1(), content2()], { title: input || 'Title and two columns', _width: 400, _height: 225, _freeform_fitContentsToBox: true, x, y }) + default: } + return undefined; }; // Dropdown that appears when the user wants to begin presenting (either minimize or sidebar view) @@ -2270,17 +2279,19 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { } @action - toggleProperties = () => (SettingsManager.Instance.propertiesWidth = SettingsManager.Instance.propertiesWidth > 0 ? 0 : 250); + toggleProperties = () => { + SnappingManager.SetPropertiesWidth(SnappingManager.PropertiesWidth > 0 ? 0 : 250); + }; @computed get toolbar() { - 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 propIcon = SnappingManager.PropertiesWidth > 0 ? 'angle-double-right' : 'angle-double-left'; + const propTitle = SnappingManager.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; - const inactiveColor = lightOrDark(SettingsManager.userBackgroundColor) === Colors.WHITE ? Colors.WHITE : SettingsManager.userBackgroundColor; + const activeColor = SnappingManager.userVariantColor; + const inactiveColor = lightOrDark(SnappingManager.userBackgroundColor) === Colors.WHITE ? Colors.WHITE : SnappingManager.userBackgroundColor; return mode === CollectionViewType.Carousel3D || Doc.IsInMyOverlay(this.Document) ? null : ( - <div id="toolbarContainer" className={'presBox-toolbar'}> + <div id="toolbarContainer" className="presBox-toolbar"> {/* <Tooltip title={<><div className="dash-tooltip">{"Add new slide"}</div></>}><div className={`toolbar-button ${this.newDocumentTools ? "active" : ""}`} onClick={action(() => this.newDocumentTools = !this.newDocumentTools)}> <FontAwesomeIcon icon={"plus"} /> <FontAwesomeIcon className={`dropdown ${this.newDocumentTools ? "active" : ""}`} icon={"angle-down"} /> @@ -2290,7 +2301,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { style={{ opacity: this.childDocs.length > 1 ? 1 : 0.3, color: this._pathBoolean ? Colors.MEDIUM_BLUE : 'white', width: isMini ? '100%' : undefined }} className="toolbar-button" onClick={this.childDocs.length > 1 ? () => this.togglePath() : undefined}> - <FontAwesomeIcon icon={'exchange-alt'} /> + <FontAwesomeIcon icon="exchange-alt" /> </div> </Tooltip> {isMini ? null : ( @@ -2298,12 +2309,12 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { <div className="toolbar-divider" /> <Tooltip title={<div className="dash-tooltip">{this._presKeyEvents ? 'Keys are active' : 'Keys are not active - click anywhere on the presentation trail to activate keys'}</div>}> <div className="toolbar-button" style={{ cursor: this._presKeyEvents ? 'default' : 'pointer', position: 'absolute', right: 30, fontSize: 16 }}> - <FontAwesomeIcon className={'toolbar-thumbtack'} icon={'keyboard'} style={{ color: this._presKeyEvents ? activeColor : inactiveColor }} /> + <FontAwesomeIcon className="toolbar-thumbtack" icon="keyboard" style={{ color: this._presKeyEvents ? activeColor : inactiveColor }} /> </div> </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.Instance.propertiesWidth > 0 ? activeColor : inactiveColor }} /> + <FontAwesomeIcon className="toolbar-thumbtack" icon={propIcon} style={{ color: SnappingManager.PropertiesWidth > 0 ? activeColor : inactiveColor }} /> </div> </Tooltip> </> @@ -2348,7 +2359,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { this.gotoDocument(this.itemIndex, this.activeItem); } })}> - <FontAwesomeIcon icon={'play-circle'} /> + <FontAwesomeIcon icon="play-circle" /> <div style={{ display: this._props.PanelWidth() > 200 ? 'inline-flex' : 'none' }}> Present</div> </div> {mode === CollectionViewType.Carousel3D || isMini ? null : ( @@ -2357,7 +2368,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { onClick={action(() => { if (this.childDocs.length) this._presentTools = !this._presentTools; })}> - <FontAwesomeIcon className="dropdown" style={{ margin: 0, transform: this._presentTools ? 'rotate(180deg)' : 'rotate(0deg)' }} icon={'angle-down'} /> + <FontAwesomeIcon className="dropdown" style={{ margin: 0, transform: this._presentTools ? 'rotate(180deg)' : 'rotate(0deg)' }} icon="angle-down" /> {this.presentDropdown} </div> )} @@ -2378,12 +2389,24 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { // Case 1: There are still other frames and should go through all frames before going to next slide return ( <div className="presPanelOverlay" style={{ display: this.layoutDoc.presentation_status !== 'edit' ? 'inline-flex' : 'none' }}> - <Tooltip title={<div className="dash-tooltip">{'Loop'}</div>}> + <Tooltip title={<div className="dash-tooltip">Loop</div>}> <div className="presPanel-button" style={{ color: this.layoutDoc.presLoop ? Colors.MEDIUM_BLUE : 'white' }} - onPointerDown={e => setupMoveUpEvents(this, e, returnFalse, emptyFunction, () => (this.layoutDoc.presLoop = !this.layoutDoc.presLoop), false, false)}> - <FontAwesomeIcon icon={'redo-alt'} /> + onPointerDown={e => + setupMoveUpEvents( + this, + e, + returnFalse, + emptyFunction, + () => { + this.layoutDoc.presLoop = !this.layoutDoc.presLoop; + }, + false, + false + ) + }> + <FontAwesomeIcon icon="redo-alt" /> </div> </Tooltip> <div className="presPanel-divider" /> @@ -2408,7 +2431,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { false ) }> - <FontAwesomeIcon icon={'arrow-left'} /> + <FontAwesomeIcon icon="arrow-left" /> </div> <Tooltip title={<div className="dash-tooltip">{this.layoutDoc.presentation_status === PresStatus.Autoplay ? 'Pause' : 'Autoplay'}</div>}> <div className="presPanel-button" onPointerDown={e => setupMoveUpEvents(this, e, returnFalse, emptyFunction, () => this.startOrPause(true), false, false)}> @@ -2436,10 +2459,10 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { false ) }> - <FontAwesomeIcon icon={'arrow-right'} /> + <FontAwesomeIcon icon="arrow-right" /> </div> - <div className="presPanel-divider"></div> - <Tooltip title={<div className="dash-tooltip">{'Click to return to 1st slide'}</div>}> + <div className="presPanel-divider" /> + <Tooltip title={<div className="dash-tooltip">Click to return to 1st slide</div>}> <div className="presPanel-button" style={{ border: 'solid 1px white' }} @@ -2463,7 +2486,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { {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> + <div className="presPanel-divider" /> {this._props.PanelWidth() > 250 ? ( <div className="presPanel-button-text" @@ -2477,7 +2500,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { </div> ) : ( <div className="presPanel-button" onPointerDown={e => setupMoveUpEvents(this, e, returnFalse, emptyFunction, this.exitClicked, false, false)}> - <FontAwesomeIcon icon={'times'} /> + <FontAwesomeIcon icon="times" /> </div> )} </div> @@ -2492,7 +2515,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { }; @action - prevClicked = (e: PointerEvent) => { + prevClicked = () => { this.back(); if (this._presTimer) { clearTimeout(this._presTimer); @@ -2501,7 +2524,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { }; @action - nextClicked = (e: PointerEvent) => { + nextClicked = () => { this.next(); if (this._presTimer) { clearTimeout(this._presTimer); @@ -2517,7 +2540,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { AddToMap = (treeViewDoc: Doc, index: number[]) => { if (!treeViewDoc.presentation_targetDoc) return this.childDocs; // if treeViewDoc is not a pres elements, then it's a sub-bullet of a progressivized slide which isn't added to the linearized list of pres elements since it's not really a pres element. - var indexNum = 0; + let indexNum = 0; for (let i = 0; i < index.length; i++) { indexNum += index[i] * 10 ** -i; } @@ -2529,19 +2552,21 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { this.dataDoc[this.presFieldKey] = new List<Doc>(sorted); // this is a flat array of Docs } } + return undefined; }; SlideIndex = (slideDoc: Doc) => DocListCast(this.dataDoc[this.presFieldKey]).indexOf(slideDoc); - RemFromMap = (treeViewDoc: Doc, index: number[]) => { + RemFromMap = (treeViewDoc: Doc) => { if (!treeViewDoc.presentation_targetDoc) return this.childDocs; // if treeViewDoc is not a pres elements, then it's a sub-bullet of a progressivized slide which isn't added to the linearized list of pres elements since it's not really a pres element. if (!this._unmounting && this.isTree) { this._treeViewMap.delete(treeViewDoc); this.dataDoc[this.presFieldKey] = new List<Doc>(this.sort(this._treeViewMap)); } + return undefined; }; - sort = (treeView_Map: Map<Doc, number>) => [...treeView_Map.entries()].sort((a: [Doc, number], b: [Doc, number]) => (a[1] > b[1] ? 1 : a[1] < b[1] ? -1 : 0)).map(kv => kv[0]); + sort = (treeViewMap: Map<Doc, number>) => [...treeViewMap.entries()].sort((a: [Doc, number], b: [Doc, number]) => (a[1] > b[1] ? 1 : a[1] < b[1] ? -1 : 0)).map(kv => kv[0]); render() { // needed to ensure that the childDocs are loaded for looking up fields @@ -2553,21 +2578,38 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { (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 - <div className="miniPres" onClick={e => e.stopPropagation()} onPointerEnter={action(e => (this._forceKeyEvents = true))}> + <div + className="miniPres" + onClick={e => e.stopPropagation()} + onPointerEnter={action(() => { + this._forceKeyEvents = true; + })}> <div className="presPanelOverlay" style={{ display: 'inline-flex', height: 30, background: Doc.ActivePresentation === this.Document ? 'green' : '#323232', top: 0, zIndex: 3000000, boxShadow: this._presKeyEvents ? '0 0 0px 3px ' + Colors.MEDIUM_BLUE : undefined }}> - <Tooltip title={<div className="dash-tooltip">{'Loop'}</div>}> + <Tooltip title={<div className="dash-tooltip">Loop</div>}> <div className="presPanel-button" style={{ color: this.layoutDoc.presLoop ? Colors.MEDIUM_BLUE : undefined }} - onPointerDown={e => setupMoveUpEvents(this, e, returnFalse, returnFalse, () => (this.layoutDoc.presLoop = !this.layoutDoc.presLoop), false, false)}> - <FontAwesomeIcon icon={'redo-alt'} /> + onPointerDown={e => + setupMoveUpEvents( + this, + e, + returnFalse, + returnFalse, + () => { + this.layoutDoc.presLoop = !this.layoutDoc.presLoop; + }, + false, + false + ) + }> + <FontAwesomeIcon icon="redo-alt" /> </div> </Tooltip> - <div className="presPanel-divider"></div> + <div className="presPanel-divider" /> <div className="presPanel-button" style={{ opacity: presStart ? 0.4 : 1 }} onPointerDown={e => setupMoveUpEvents(this, e, returnFalse, returnFalse, this.prevClicked, false, false)}> - <FontAwesomeIcon icon={'arrow-left'} /> + <FontAwesomeIcon icon="arrow-left" /> </div> <Tooltip title={<div className="dash-tooltip">{this.layoutDoc.presentation_status === PresStatus.Autoplay ? 'Pause' : 'Autoplay'}</div>}> <div className="presPanel-button" onPointerDown={e => setupMoveUpEvents(this, e, returnFalse, returnFalse, () => this.startOrPause(true), false, false)}> @@ -2575,10 +2617,10 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { </div> </Tooltip> <div className="presPanel-button" style={{ opacity: presEnd ? 0.4 : 1 }} onPointerDown={e => setupMoveUpEvents(this, e, returnFalse, returnFalse, this.nextClicked, false, false)}> - <FontAwesomeIcon icon={'arrow-right'} /> + <FontAwesomeIcon icon="arrow-right" /> </div> - <div className="presPanel-divider"></div> - <Tooltip title={<div className="dash-tooltip">{'Click to return to 1st slide'}</div>}> + <div className="presPanel-divider" /> + <Tooltip title={<div className="dash-tooltip">Click to return to 1st slide</div>}> <div className="presPanel-button" style={{ border: 'solid 1px white' }} onPointerDown={e => setupMoveUpEvents(this, e, returnFalse, returnFalse, () => this.gotoDocument(0, this.activeItem), false, false)}> <b>1</b> </div> @@ -2602,27 +2644,28 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { <div className="Slide"> {mode !== CollectionViewType.Invalid ? ( <CollectionView + // eslint-disable-next-line react/jsx-props-no-spreading {...this._props} PanelWidth={this._props.PanelWidth} PanelHeight={this.panelHeight} - childIgnoreNativeSize={true} + childIgnoreNativeSize moveDocument={returnFalse} - ignoreUnrendered={true} + ignoreUnrendered childDragAction={dropActionType.move} setContentViewBox={emptyFunction} - //childLayoutFitWidth={returnTrue} + // childLayoutFitWidth={returnTrue} childOpacity={returnOne} childClickScript={PresBox.navigateToDocScript} childLayoutTemplate={this.childLayoutTemplate} childXPadding={Doc.IsComicStyle(this.Document) ? 20 : undefined} filterAddDocument={this.addDocumentFilter} removeDocument={returnFalse} - dontRegisterView={true} + dontRegisterView focus={this.focusElement} ScreenToLocalTransform={this.getTransform} AddToMap={this.AddToMap} RemFromMap={this.RemFromMap} - hierarchyIndex={emptyPath as any as number[]} + hierarchyIndex={emptyPath} /> ) : null} </div> @@ -2641,6 +2684,12 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { } } +// eslint-disable-next-line prefer-arrow-callback ScriptingGlobals.add(function navigateToDoc(bestTarget: Doc, activeItem: Doc) { PresBox.NavigateToTarget(bestTarget, activeItem); }); + +Docs.Prototypes.TemplateMap.set(DocumentType.PRES, { + layout: { view: PresBox, dataField: 'data' }, + options: { acl: '', defaultDoubleClick: 'ignore', hideClickBehaviors: true, layout_hideLinkAnchors: true }, +}); diff --git a/src/client/views/nodes/trails/PresElementBox.tsx b/src/client/views/nodes/trails/PresElementBox.tsx index 28139eb14..306b98190 100644 --- a/src/client/views/nodes/trails/PresElementBox.tsx +++ b/src/client/views/nodes/trails/PresElementBox.tsx @@ -1,27 +1,30 @@ +/* eslint-disable jsx-a11y/no-static-element-interactions */ +/* eslint-disable jsx-a11y/click-events-have-key-events */ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { Tooltip } from '@mui/material'; import { action, computed, IReactionDisposer, makeObservable, observable, reaction, runInAction } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; +import { returnEmptyDoclist, returnFalse, returnTrue, setupMoveUpEvents } from '../../../../ClientUtils'; 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 { emptyFunction } from '../../../../Utils'; import { Docs } from '../../../documents/Documents'; -import { CollectionViewType } from '../../../documents/DocumentTypes'; -import { DocumentManager } from '../../../util/DocumentManager'; +import { CollectionViewType, DocumentType } from '../../../documents/DocumentTypes'; import { DragManager } from '../../../util/DragManager'; -import { SettingsManager } from '../../../util/SettingsManager'; +import { SnappingManager } from '../../../util/SnappingManager'; import { Transform } from '../../../util/Transform'; import { undoable, undoBatch } from '../../../util/UndoManager'; import { TreeView } from '../../collections/TreeView'; import { ViewBoxBaseComponent } from '../../DocComponent'; import { EditableView } from '../../EditableView'; import { Colors } from '../../global/globalEnums'; -import { DocumentView } from '../../nodes/DocumentView'; -import { FieldView, FieldViewProps } from '../../nodes/FieldView'; -import { StyleProp } from '../../StyleProvider'; +import { PinDocView } from '../../PinFuncs'; +import { StyleProp } from '../../StyleProp'; +import { DocumentView } from '../DocumentView'; +import { FieldView, FieldViewProps } from '../FieldView'; import { PresBox } from './PresBox'; import './PresElementBox.scss'; import { PresMovement } from './PresEnums'; @@ -71,7 +74,7 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() { // computes index of this presentation slide in the presBox list @computed get indexInPres() { - return this.presBoxView?.SlideIndex(this.slideDoc); + return this.presBoxView?.SlideIndex(this.slideDoc) ?? 0; } @computed get selectedArray() { @@ -86,7 +89,9 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() { this.layoutDoc.layout_hideLinkButton = true; this._heightDisposer = reaction( () => ({ expand: this.slideDoc.presentation_expandInlineButton, height: this.collapsedHeight }), - ({ expand, height }) => (this.layoutDoc._height = height + (expand ? this.expandViewHeight : 0)), + ({ expand, height }) => { + this.layoutDoc._height = height + (expand ? this.expandViewHeight : 0); + }, { fireImmediately: true } ); } @@ -94,12 +99,14 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() { this._heightDisposer?.(); } - presExpandDocumentClick = () => (this.slideDoc.presentation_expandInlineButton = !this.slideDoc.presentation_expandInlineButton); + presExpandDocumentClick = () => { + this.slideDoc.presentation_expandInlineButton = !this.slideDoc.presentation_expandInlineButton; + }; embedHeight = () => this.collapsedHeight + this.expandViewHeight; embedWidth = () => this._props.PanelWidth() / 2; - styleProvider = (doc: Doc | undefined, props: Opt<FieldViewProps>, property: string): any => { - return property === StyleProp.Opacity ? 1 : this._props.styleProvider?.(doc, props, property); - }; + // prettier-ignore + styleProvider = ( doc: Doc | undefined, props: Opt<FieldViewProps>, property: string ): any => + (property === StyleProp.Opacity ? 1 : this._props.styleProvider?.(doc, props, property)); /** * The function that is responsible for rendering a preview or not for this * presentation element. @@ -113,7 +120,7 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() { PanelHeight={this.embedHeight} isContentActive={this._props.isContentActive} styleProvider={this.styleProvider} - hideLinkButton={true} + hideLinkButton ScreenToLocalTransform={Transform.Identity} renderDepth={this._props.renderDepth + 1} containerViewPath={returnEmptyDoclist} @@ -150,7 +157,7 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() { ref={this._titleRef} editing={undefined} contents={doc.title} - overflow={'ellipsis'} + overflow="ellipsis" GetValue={() => StrCast(doc.title)} SetValue={(value: string) => { doc.title = !value.trim().length ? '-untitled-' : value; @@ -177,10 +184,10 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() { e.preventDefault(); if (element && !(e.ctrlKey || e.metaKey || e.button === 2)) { this.presBoxView?.regularSelect(this.slideDoc, this._itemRef.current!, this._dragRef.current!, true, false); - setupMoveUpEvents(this, e, this.startDrag, emptyFunction, e => { - e.stopPropagation(); - e.preventDefault(); - this.presBoxView?.modifierSelect(this.slideDoc, this._itemRef.current!, this._dragRef.current!, e.shiftKey || e.ctrlKey || e.metaKey, e.ctrlKey || e.metaKey, e.shiftKey); + setupMoveUpEvents(this, e, this.startDrag, emptyFunction, clickEv => { + clickEv.stopPropagation(); + clickEv.preventDefault(); + this.presBoxView?.modifierSelect(this.slideDoc, this._itemRef.current!, this._dragRef.current!, clickEv.shiftKey || clickEv.ctrlKey || clickEv.metaKey, clickEv.ctrlKey || clickEv.metaKey, clickEv.shiftKey); this.presBoxView?.activeItem && this.showRecording(this.presBoxView?.activeItem); }); } @@ -209,7 +216,7 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() { } else if (dragArray.length >= 1) { const doc = document.createElement('div'); doc.className = 'presItem-multiDrag'; - doc.innerText = 'Move ' + this.selectedArray?.size + ' slides'; + doc.innerText = 'Move ' + (this.selectedArray?.size ?? 0) + ' slides'; doc.style.position = 'absolute'; doc.style.top = e.clientY + 'px'; doc.style.left = e.clientX - 50 + 'px'; @@ -217,7 +224,9 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() { } if (activeItem) { - runInAction(() => (this._dragging = true)); + runInAction(() => { + this._dragging = true; + }); DragManager.StartDocumentDrag( dragItem.map(ele => ele), dragData, @@ -225,7 +234,10 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() { e.clientY, undefined, action(() => { - Array.from(classesToRestore).forEach(pair => (pair[0].className = pair[1])); + Array.from(classesToRestore).forEach(pair => { + // eslint-disable-next-line prefer-destructuring + pair[0].className = pair[1]; + }); this._dragging = false; }) ); @@ -234,7 +246,7 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() { return false; }; - onPointerOver = (e: any) => { + onPointerOver = () => { document.removeEventListener('pointermove', this.onPointerMove); document.addEventListener('pointermove', this.onPointerMove); }; @@ -244,7 +256,7 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() { const dragIsPresItem = DragManager.docsBeingDragged.some(d => d.presentation_targetDoc); if (slide && dragIsPresItem) { const rect = slide.getBoundingClientRect(); - const y = e.clientY - rect.top; //y position within the element. + const y = e.clientY - rect.top; // y position within the element. const height = slide.clientHeight; const halfLine = height / 2; if (y <= halfLine) { @@ -258,7 +270,7 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() { document.removeEventListener('pointermove', this.onPointerMove); }; - onPointerLeave = (e: any) => { + onPointerLeave = () => { const slide = this._itemRef.current; if (slide) { slide.style.borderTop = '0px'; @@ -269,8 +281,8 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() { @action toggleProperties = () => { - if (SettingsManager.Instance.propertiesWidth < 5) { - SettingsManager.Instance.propertiesWidth = 250; + if (SnappingManager.PropertiesWidth < 5) { + SnappingManager.SetPropertiesWidth(250); } }; @@ -323,7 +335,7 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() { updateCapturedViewContents = undoable( action((presTargetDoc: Doc, activeItem: Doc) => { const target = DocCast(presTargetDoc.annotationOn) ?? presTargetDoc; - PresBox.pinDocView(activeItem, { pinData: PresBox.pinDataTypes(target) }, target); + PinDocView(activeItem, { pinData: PresBox.pinDataTypes(target) }, target); }), 'updated captured view contents' ); @@ -340,7 +352,7 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() { }; hideRecording = undoable( - action((e: React.MouseEvent, iconClick: boolean = false) => { + action((e: React.MouseEvent) => { e.stopPropagation(); this.removeAllRecordingInOverlay(); }), @@ -395,7 +407,7 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() { lfg = (e: React.MouseEvent) => { e.stopPropagation(); // TODO: fix this bug - const { toggleChildrenRun } = this.slideDoc; + // const { toggleChildrenRun } = this.slideDoc; TreeView.ToggleChildrenRun.get(this.slideDoc)?.(); // call this.slideDoc.recurChildren() to get all the children @@ -404,17 +416,15 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() { @computed get toolbarWidth(): number { - const presBoxDocView = DocumentManager.Instance.getDocumentView(this.presBox); + const presBoxDocView = DocumentView.getDocumentView(this.presBox); const width = NumCast(this.presBox?._width); - return presBoxDocView ? presBoxDocView._props.PanelWidth() : width ? width : 300; + return presBoxDocView ? presBoxDocView._props.PanelWidth() : width || 300; } @computed get presButtons() { - const presBox = this.presBox; + const { presBox, targetDoc, slideDoc: activeItem } = this; const presBoxColor = StrCast(presBox?._backgroundColor); const presColorBool = presBoxColor ? presBoxColor !== Colors.WHITE && presBoxColor !== 'transparent' : false; - const targetDoc = this.targetDoc; - const activeItem = this.slideDoc; const hasChildren = BoolCast(this.slideDoc?.hasChildren); const items: JSX.Element[] = []; @@ -441,7 +451,7 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() { ); items.push( <Tooltip key="slash" title={<div className="dash-tooltip">{this.videoRecordingIsInOverlay ? 'Hide Recording' : `${PresElementBox.videoIsRecorded(activeItem) ? 'Show' : 'Start'} recording`}</div>}> - <div className="slideButton" onClick={e => (this.videoRecordingIsInOverlay ? this.hideRecording(e, true) : this.startRecording(e, activeItem))} style={{ fontWeight: 700 }}> + <div className="slideButton" onClick={e => (this.videoRecordingIsInOverlay ? this.hideRecording(e) : this.startRecording(e, activeItem))} style={{ fontWeight: 700 }}> <FontAwesomeIcon icon={`video${this.videoRecordingIsInOverlay ? '-slash' : ''}`} onPointerDown={e => e.stopPropagation()} /> </div> </Tooltip> @@ -461,7 +471,9 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() { }> <div className="slideButton" - onClick={() => (activeItem.presentation_groupWithUp = (NumCast(activeItem.presentation_groupWithUp) + 1) % 3)} + onClick={() => { + activeItem.presentation_groupWithUp = (NumCast(activeItem.presentation_groupWithUp) + 1) % 3; + }} style={{ zIndex: 1000 - this.indexInPres, fontWeight: 700, @@ -471,7 +483,7 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() { transform: activeItem.presentation_groupWithUp ? 'translate(0, -17px)' : undefined, }}> <div style={{ transform: activeItem.presentation_groupWithUp ? 'rotate(180deg) translate(0, -17.5px)' : 'rotate(0deg)' }}> - <FontAwesomeIcon icon={'arrow-up'} onPointerDown={e => e.stopPropagation()} /> + <FontAwesomeIcon icon="arrow-up" onPointerDown={e => e.stopPropagation()} /> </div> </div> </Tooltip> @@ -500,15 +512,15 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() { this.lfg(e); }} style={{ fontWeight: 700 }}> - <FontAwesomeIcon icon={'circle-play'} onPointerDown={e => e.stopPropagation()} /> + <FontAwesomeIcon icon="circle-play" onPointerDown={e => e.stopPropagation()} /> </div> </Tooltip> ); } items.push( <Tooltip key="trash" title={<div className="dash-tooltip">Remove from presentation</div>}> - <div className={'slideButton'} onClick={this.removePresentationItem}> - <FontAwesomeIcon icon={'trash'} onPointerDown={e => e.stopPropagation()} /> + <div className="slideButton" onClick={this.removePresentationItem}> + <FontAwesomeIcon icon="trash" onPointerDown={e => e.stopPropagation()} /> </div> </Tooltip> ); @@ -516,18 +528,17 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() { } @computed get mainItem() { - const isSelected: boolean = this.selectedArray?.has(this.slideDoc) ? true : false; + const { presBox, slideDoc: activeItem } = this; + const isSelected: boolean = !!this.selectedArray?.has(activeItem); const isCurrent: boolean = this.presBox?._itemIndex === this.indexInPres; const miniView: boolean = this.toolbarWidth <= 110; - const presBox = this.presBox; //presBox const presBoxColor: string = StrCast(presBox?._backgroundColor); const presColorBool: boolean = presBoxColor ? presBoxColor !== Colors.WHITE && presBoxColor !== 'transparent' : false; - const activeItem: Doc = this.slideDoc; return ( <div className="presItem-container" - key={this.slideDoc[Id] + this.indexInPres} + key={activeItem[Id] + this.indexInPres} ref={this._itemRef} style={{ backgroundColor: presColorBool ? (isSelected ? 'rgba(250,250,250,0.3)' : 'transparent') : isSelected ? Colors.LIGHT_BLUE : 'transparent', @@ -537,9 +548,9 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() { paddingTop: NumCast(this.layoutDoc._yPadding, this._props.yPadding), paddingBottom: NumCast(this.layoutDoc._yPadding, this._props.yPadding), }} - onDoubleClick={action(e => { + onDoubleClick={action(() => { this.toggleProperties(); - this.presBoxView?.regularSelect(this.slideDoc, this._itemRef.current!, this._dragRef.current!, false); + this.presBoxView?.regularSelect(activeItem, this._itemRef.current!, this._dragRef.current!, false); })} onPointerOver={this.onPointerOver} onPointerLeave={this.onPointerLeave} @@ -551,11 +562,11 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() { ) : ( <div ref={this._dragRef} - className={`presItem-slide ${isCurrent ? 'active' : ''}${this.slideDoc.runProcess ? ' testingv2' : ''}`} + className={`presItem-slide ${isCurrent ? 'active' : ''}${activeItem.runProcess ? ' testingv2' : ''}`} style={{ display: 'infline-block', 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, + // 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, }}> <div @@ -563,7 +574,7 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() { style={{ display: 'inline-flex', pointerEvents: isSelected ? undefined : 'none', - width: `calc(100% ${this.slideDoc.presentation_expandInlineButton ? '- 50%' : ''} - ${this.presButtons.length * 22}px`, + width: `calc(100% ${activeItem.presentation_expandInlineButton ? '- 50%' : ''} - ${this.presButtons.length * 22}px`, cursor: isSelected ? 'text' : 'grab', }}> <div @@ -576,7 +587,7 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() { } }} onClick={e => e.stopPropagation()}>{`${this.indexInPres + 1}. `}</div> - <EditableView ref={this._titleRef} oneLine={true} editing={!isSelected ? false : undefined} contents={activeItem.title} overflow={'ellipsis'} GetValue={() => StrCast(activeItem.title)} SetValue={this.onSetValue} /> + <EditableView ref={this._titleRef} oneLine editing={!isSelected ? false : undefined} contents={activeItem.title} overflow="ellipsis" GetValue={() => StrCast(activeItem.title)} SetValue={this.onSetValue} /> </div> {/* <Tooltip title={<><div className="dash-tooltip">{"Movement speed"}</div></>}><div className="presItem-time" style={{ display: showMore ? "block" : "none" }}>{this.transition}</div></Tooltip> */} {/* <Tooltip title={<><div className="dash-tooltip">{"Duration"}</div></>}><div className="presItem-time" style={{ display: showMore ? "block" : "none" }}>{this.duration}</div></Tooltip> */} @@ -594,3 +605,8 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() { return !(this.slideDoc instanceof Doc) || this.targetDoc instanceof Promise ? null : this.mainItem; } } + +Docs.Prototypes.TemplateMap.set(DocumentType.PRESELEMENT, { + layout: { view: PresElementBox, dataField: 'data' }, + options: { acl: '', title: 'pres element template', _layout_fitWidth: true, _xMargin: 0, isTemplateDoc: true, isTemplateForField: 'data' }, +}); diff --git a/src/client/views/nodes/trails/index.ts b/src/client/views/nodes/trails/index.ts index 8f3f7b03a..7b18974df 100644 --- a/src/client/views/nodes/trails/index.ts +++ b/src/client/views/nodes/trails/index.ts @@ -1,3 +1,3 @@ -export * from "./PresBox"; -export * from "./PresElementBox"; -export * from "./PresEnums";
\ No newline at end of file +export * from './PresBox'; +export * from './PresElementBox'; +export * from './PresEnums'; |