diff options
Diffstat (limited to 'src/client/views/nodes')
107 files changed, 9629 insertions, 5262 deletions
diff --git a/src/client/views/nodes/AudioBox.scss b/src/client/views/nodes/AudioBox.scss index d40537776..4337401e3 100644 --- a/src/client/views/nodes/AudioBox.scss +++ b/src/client/views/nodes/AudioBox.scss @@ -1,4 +1,4 @@ -@import "../global/globalCssVariables.scss"; +@import '../global/globalCssVariables.module.scss'; .audiobox-container { width: 100%; @@ -116,18 +116,18 @@ width: 10px; } - input[type="range"] { + input[type='range'] { width: 50px; -webkit-appearance: none; background: none; margin: 5px; } - input[type="range"]:focus { + input[type='range']:focus { outline: none; } - input[type="range"]::-webkit-slider-runnable-track { + input[type='range']::-webkit-slider-runnable-track { width: 100%; height: 6px; cursor: pointer; @@ -136,7 +136,7 @@ border-radius: 3px; } - input[type="range"]::-webkit-slider-thumb { + input[type='range']::-webkit-slider-thumb { box-shadow: 0; border: 0; height: 10px; diff --git a/src/client/views/nodes/AudioBox.tsx b/src/client/views/nodes/AudioBox.tsx index 8d80f1364..8a38ef663 100644 --- a/src/client/views/nodes/AudioBox.tsx +++ b/src/client/views/nodes/AudioBox.tsx @@ -1,10 +1,11 @@ -import React = require('react'); import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { Tooltip } from '@material-ui/core'; -import { action, computed, IReactionDisposer, observable, runInAction } from 'mobx'; +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 { 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'; @@ -17,10 +18,9 @@ import { undoBatch } from '../../util/UndoManager'; import { CollectionStackedTimeline, TrimScope } from '../collections/CollectionStackedTimeline'; import { ContextMenu } from '../ContextMenu'; import { ContextMenuProps } from '../ContextMenuItem'; -import { ViewBoxAnnotatableComponent, ViewBoxAnnotatableProps } from '../DocComponent'; +import { ViewBoxAnnotatableComponent } from '../DocComponent'; import './AudioBox.scss'; -import { DocFocusOptions } from './DocumentView'; -import { FieldView, FieldViewProps } from './FieldView'; +import { FocusViewOptions, FieldView, FieldViewProps } from './FieldView'; import { PinProps, PresBox } from './trails'; /** @@ -49,12 +49,18 @@ export enum media_state { } @observer -export class AudioBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps & FieldViewProps>() { +export class AudioBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { public static LayoutString(fieldKey: string) { return FieldView.LayoutString(AudioBox, fieldKey); } + public static Enabled = false; + constructor(props: any) { + super(props); + makeObservable(this); + } + static topControlsHeight = 30; // height of upper controls above timeline static bottomControlsHeight = 20; // height of lower controls below timeline @@ -68,7 +74,7 @@ export class AudioBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp _stream: MediaStream | undefined; // passed to MediaRecorder, records device input audio _play: any = null; // timeout for playback - @observable _stackedTimeline: CollectionStackedTimeline | null | undefined; // CollectionStackedTimeline ref + @observable _stackedTimeline: CollectionStackedTimeline | null | undefined = undefined; // CollectionStackedTimeline ref @observable _finished: boolean = false; // has playback reached end of clip @observable _volume: number = 1; @observable _muted: boolean = false; @@ -84,7 +90,7 @@ export class AudioBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp // if you get rid of it and set the value to 0 the timeline and waveform will set their bounds incorrectly @computed get miniPlayer() { - return this.props.PanelHeight() < 50; + return this._props.PanelHeight() < 50; } // used to collapse timeline when node is shrunk @computed get links() { return LinkManager.Links(this.dataDoc); @@ -94,7 +100,7 @@ export class AudioBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp } @computed get path() { // returns the path of the audio file - const path = Cast(this.props.Document[this.fieldKey], AudioField, null)?.url.href || ''; + const path = Cast(this.Document[this.fieldKey], AudioField, null)?.url.href || ''; return path === nullAudio ? '' : path; } set mediaState(value) { @@ -115,7 +121,7 @@ export class AudioBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp @action componentDidMount() { - this.props.setContentView?.(this); + this._props.setContentViewBox?.(this); if (this.path) { this.mediaState = media_state.Paused; this.setPlayheadTime(NumCast(this.layoutDoc.clipStart)); @@ -139,17 +145,17 @@ export class AudioBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp const timecode = Cast(this.layoutDoc._layout_currentTimecode, 'number', null); const anchor = addAsAnnotation ? CollectionStackedTimeline.createAnchor( - this.rootDoc, + this.Document, this.dataDoc, this.annotationKey, - this._ele?.currentTime || Cast(this.props.Document._layout_currentTimecode, 'number', null) || (this.mediaState === media_state.Recording ? (Date.now() - (this.recordingStart || 0)) / 1000 : undefined), + this._ele?.currentTime || Cast(this.Document._layout_currentTimecode, 'number', null) || (this.mediaState === media_state.Recording ? (Date.now() - (this.recordingStart || 0)) / 1000 : undefined), undefined, undefined, addAsAnnotation - ) || this.rootDoc - : Docs.Create.ConfigDocument({ title: '#' + timecode, _timecodeToShow: timecode, annotationOn: this.rootDoc }); + ) || this.Document + : Docs.Create.ConfigDocument({ title: '#' + timecode, _timecodeToShow: timecode, annotationOn: this.Document }); - PresBox.pinDocView(anchor, { pinDocLayout: pinProps?.pinDocLayout, pinData: { ...(pinProps?.pinData ?? {}), temporal: true } }, this.rootDoc); + PresBox.pinDocView(anchor, { pinDocLayout: pinProps?.pinDocLayout, pinData: { ...(pinProps?.pinData ?? {}), temporal: true } }, this.Document); return anchor; }; @@ -186,13 +192,16 @@ export class AudioBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp this._ele.play(); this.mediaState = media_state.Playing; this.addCurrentlyPlaying(); - this._play = setTimeout(() => { - // need to keep track of if end of clip is reached so on next play, clip restarts - if (fullPlay) this._finished = true; - // removes from currently playing if playback has reached end of range marker - else this.removeCurrentlyPlaying(); - this.Pause(); - }, (end - start) * 1000); + this._play = setTimeout( + () => { + // need to keep track of if end of clip is reached so on next play, clip restarts + if (fullPlay) this._finished = true; + // removes from currently playing if playback has reached end of range marker + else this.removeCurrentlyPlaying(); + this.Pause(); + }, + (end - start) * 1000 + ); } else { this.Pause(); } @@ -202,7 +211,7 @@ export class AudioBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp // removes from currently playing display @action removeCurrentlyPlaying = () => { - const docView = this.props.DocumentView?.(); + const docView = this.DocumentView?.(); if (CollectionStackedTimeline.CurrentlyPlaying && docView) { const index = CollectionStackedTimeline.CurrentlyPlaying.indexOf(docView); index !== -1 && CollectionStackedTimeline.CurrentlyPlaying.splice(index, 1); @@ -212,7 +221,7 @@ export class AudioBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp // adds doc to currently playing display @action addCurrentlyPlaying = () => { - const docView = this.props.DocumentView?.(); + const docView = this.DocumentView?.(); if (!CollectionStackedTimeline.CurrentlyPlaying) { CollectionStackedTimeline.CurrentlyPlaying = []; } @@ -240,7 +249,7 @@ export class AudioBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp this._recorder.ondataavailable = async (e: any) => { const [{ result }] = await Networking.UploadFilesToServer({ file: e.data }); if (!(result instanceof Error)) { - this.props.Document[this.fieldKey] = new AudioField(result.accessPaths.agnostic.client); + this.Document[this.fieldKey] = new AudioField(result.accessPaths.agnostic.client); } }; this._recordStart = new Date().getTime(); @@ -363,17 +372,18 @@ export class AudioBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp returnFalse, returnFalse, action(() => { - const newDoc = DocUtils.GetNewTextDoc('', NumCast(this.rootDoc.x), NumCast(this.rootDoc.y) + NumCast(this.layoutDoc._height) + 10, NumCast(this.layoutDoc._width), 2 * NumCast(this.layoutDoc._height)); + const newDoc = DocUtils.GetNewTextDoc('', NumCast(this.Document.x), NumCast(this.Document.y) + NumCast(this.layoutDoc._height) + 10, NumCast(this.layoutDoc._width), 2 * NumCast(this.layoutDoc._height)); const textField = Doc.LayoutFieldKey(newDoc); - Doc.GetProto(newDoc)[`${textField}_recordingSource`] = this.dataDoc; - Doc.GetProto(newDoc)[`${textField}_recordingStart`] = ComputedField.MakeFunction(`self.${textField}_recordingSource.${this.fieldKey}_recordingStart`); - Doc.GetProto(newDoc).mediaState = ComputedField.MakeFunction(`self.${textField}_recordingSource.mediaState`); - if (Doc.IsInMyOverlay(this.rootDoc)) { - newDoc.overlayX = this.rootDoc.x; - newDoc.overlayY = NumCast(this.rootDoc.y) + NumCast(this.rootDoc._height); + const newDocData = newDoc[DocData]; + newDocData[`${textField}_recordingSource`] = this.dataDoc; + newDocData[`${textField}_recordingStart`] = ComputedField.MakeFunction(`this.${textField}_recordingSource.${this.fieldKey}_recordingStart`); + newDocData.mediaState = ComputedField.MakeFunction(`this.${textField}_recordingSource.mediaState`); + if (Doc.IsInMyOverlay(this.Document)) { + newDoc.overlayX = this.Document.x; + newDoc.overlayY = NumCast(this.Document.y) + NumCast(this.layoutDoc._height); Doc.AddToMyOverlay(newDoc); } else { - this.props.addDocument?.(newDoc); + this._props.addDocument?.(newDoc); } }), false @@ -423,8 +433,8 @@ export class AudioBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp }; // plays link - playLink = (link: Doc, options: DocFocusOptions) => { - if (link.annotationOn === this.rootDoc) { + 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)); } else { @@ -449,9 +459,9 @@ export class AudioBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp }; @action - timelineWhenChildContentsActiveChanged = (isActive: boolean) => this.props.whenChildContentsActiveChanged((this._isAnyChildContentActive = isActive)); + timelineWhenChildContentsActiveChanged = (isActive: boolean) => this._props.whenChildContentsActiveChanged((this._isAnyChildContentActive = isActive)); - timelineScreenToLocal = () => this.props.ScreenToLocalTransform().translate(0, -AudioBox.topControlsHeight); + timelineScreenToLocal = () => this.ScreenToLocalBoxXf().translate(0, -AudioBox.topControlsHeight); setPlayheadTime = (time: number) => (this._ele!.currentTime /*= this.layoutDoc._layout_currentTimecode*/ = time); @@ -460,8 +470,8 @@ export class AudioBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp isActiveChild = () => this._isAnyChildContentActive; // timeline dimensions - timelineWidth = () => this.props.PanelWidth(); - timelineHeight = () => this.props.PanelHeight() - (AudioBox.topControlsHeight + AudioBox.bottomControlsHeight); + timelineWidth = () => this._props.PanelWidth(); + timelineHeight = () => this._props.PanelHeight() - (AudioBox.topControlsHeight + AudioBox.bottomControlsHeight); // ends trim, hides trim controls and displays new clip @undoBatch @@ -555,11 +565,10 @@ export class AudioBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp this._dropDisposer = DragManager.MakeDropTarget( r, (e, de) => { - const [xp, yp] = this.props.ScreenToLocalTransform().transformPoint(de.x, de.y); + const [xp, yp] = this.ScreenToLocalBoxXf().transformPoint(de.x, de.y); de.complete.docDragData && this.timeline?.internalDocDrop(e, de, de.complete.docDragData, xp); }, - this.layoutDoc, - undefined + this.layoutDoc ); } }; @@ -597,7 +606,7 @@ export class AudioBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp <div className="audiobox-file" style={{ - pointerEvents: this._isAnyChildContentActive || this.props.isContentActive() ? 'all' : 'none', + pointerEvents: this._isAnyChildContentActive || this._props.isContentActive() ? 'all' : 'none', flexDirection: this.miniPlayer ? 'row' : 'column', justifyContent: this.miniPlayer ? 'flex-start' : 'space-between', }}> @@ -670,7 +679,7 @@ export class AudioBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp <div className="audiobox-timecodes"> <div className="timecode-current">{this.timeline && formatTime(Math.round(NumCast(this.layoutDoc._layout_currentTimecode) - NumCast(this.timeline.clipStart)))}</div> {this.miniPlayer ? ( - <div>/</div> + <div /> ) : ( <div className="bottom-controls-middle"> <FontAwesomeIcon icon="search-plus" /> @@ -699,16 +708,15 @@ export class AudioBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp return ( <CollectionStackedTimeline ref={action((r: CollectionStackedTimeline | null) => (this._stackedTimeline = r))} - {...this.props} + {...this._props} CollectionFreeFormDocumentView={undefined} dataFieldKey={this.fieldKey} fieldKey={this.annotationKey} dictationKey={this.fieldKey + '_dictation'} mediaPath={this.path} - renderDepth={this.props.renderDepth + 1} + renderDepth={this._props.renderDepth + 1} startTag={'_timecodeToShow' /* audioStart */} endTag={'_timecodeToHide' /* audioEnd */} - bringToFront={emptyFunction} playFrom={this.playFrom} setTime={this.setPlayheadTime} playing={this.playing} @@ -719,7 +727,7 @@ export class AudioBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp ScreenToLocalTransform={this.timelineScreenToLocal} Play={this.Play} Pause={this.Pause} - isContentActive={this.props.isContentActive} + isContentActive={this._props.isContentActive} isAnyChildContentActive={this.isAnyChildContentActive} playLink={this.playLink} PanelWidth={this.timelineWidth} @@ -734,7 +742,7 @@ export class AudioBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp return ( <audio ref={this.setRef} - className={`audiobox-control${this.props.isContentActive() ? '-interactive' : ''}`} + className={`audiobox-control${this._props.isContentActive() ? '-interactive' : ''}`} onLoadedData={action(e => this._ele?.duration && this._ele?.duration !== Infinity && (this.dataDoc[this.fieldKey + '_duration'] = this._ele.duration))}> <source src={this.path} type="audio/mpeg" /> Not supported. diff --git a/src/client/views/nodes/CollectionFreeFormDocumentView.scss b/src/client/views/nodes/CollectionFreeFormDocumentView.scss index f99011b8f..7f0a39550 100644 --- a/src/client/views/nodes/CollectionFreeFormDocumentView.scss +++ b/src/client/views/nodes/CollectionFreeFormDocumentView.scss @@ -1,9 +1,9 @@ .collectionFreeFormDocumentView-container { - transform-origin: left top; + transform-origin: 50% 50%; position: absolute; background-color: transparent; touch-action: manipulation; top: 0; left: 0; - //pointer-events: none; + pointer-events: none; } diff --git a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx index f9afe4d53..0ae4ed62c 100644 --- a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx +++ b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx @@ -1,42 +1,146 @@ -import { action, computed, observable } from 'mobx'; +import { action, computed, makeObservable, observable } from 'mobx'; import { observer } from 'mobx-react'; -import { Doc, Opt } from '../../../fields/Doc'; +import * as React from 'react'; +import { OmitKeys, numberRange } from '../../../Utils'; +import { Doc, DocListCast, Opt } from '../../../fields/Doc'; 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 { numberRange, OmitKeys } from '../../../Utils'; import { DocumentManager } from '../../util/DocumentManager'; +import { ScriptingGlobals } from '../../util/ScriptingGlobals'; import { SelectionManager } from '../../util/SelectionManager'; -import { Transform } from '../../util/Transform'; -import { CollectionFreeFormView } from '../collections/collectionFreeForm/CollectionFreeFormView'; import { DocComponent } from '../DocComponent'; +import { ObservableReactComponent } from '../ObservableReactComponent'; import { StyleProp } from '../StyleProvider'; +import { CollectionFreeFormView } from '../collections/collectionFreeForm/CollectionFreeFormView'; import './CollectionFreeFormDocumentView.scss'; import { DocumentView, DocumentViewProps, OpenWhere } from './DocumentView'; -import React = require('react'); +import { FieldViewProps } from './FieldView'; -export interface CollectionFreeFormDocumentViewProps extends DocumentViewProps { - dataProvider?: (doc: Doc, replica: string) => { x: number; y: number; zIndex?: number; rotation?: number; color?: string; backgroundColor?: string; opacity?: number; highlight?: boolean; z: number; transition?: string } | undefined; - sizeProvider?: (doc: Doc, replica: string) => { width: number; height: number } | undefined; - renderCutoffProvider: (doc: Doc) => boolean; +export interface CollectionFreeFormDocumentViewWrapperProps extends DocumentViewProps { + x: number; + y: number; + z: number; + width: number; + height: number; zIndex?: number; + autoDim?: number; // 1 means use Panel Width/Height, 0 means use width/height + rotation?: number; + color?: string; + backgroundColor?: string; + opacity?: number; + highlight?: boolean; + transition?: string; dataTransition?: string; - replica: string; + RenderCutoffProvider: (doc: Doc) => boolean; + CollectionFreeFormView: CollectionFreeFormView; +} +@observer +export class CollectionFreeFormDocumentViewWrapper extends ObservableReactComponent<CollectionFreeFormDocumentViewWrapperProps> { + constructor(props: CollectionFreeFormDocumentViewWrapperProps) { + super(props); + makeObservable(this); + } + @observable X = this.props.x; + @observable Y = this.props.y; + @observable Z = this.props.z; + @observable ZIndex = this.props.zIndex; + @observable Rotation = this.props.rotation; + @observable Opacity = this.props.opacity; + @observable BackgroundColor = this.props.backgroundColor; + @observable Color = this.props.color; + @observable Highlight = this.props.highlight; + @observable Width = this.props.width; + @observable Height = this.props.height; + @observable AutoDim = this.props.autoDim; + @observable Transition = this.props.transition; + @observable DataTransition = this.props.dataTransition; + CollectionFreeFormView = this.props.CollectionFreeFormView; // needed for type checking + RenderCutoffProvider = this.props.RenderCutoffProvider; // needed for type checking + + get Document() { + return this._props.Document; + } + @computed get WrapperKeys() { + return Object.keys(this).filter(key => key.startsWith('w_')).map(key => key.replace('w_', '')) + .map(key => ({upper:key, lower:key[0].toLowerCase() + key.substring(1)})); // prettier-ignore + } + + // wrapper functions around prop fields that have been converted to observables to keep 'props' from ever changing. + // this way, downstream code only invalidates when it uses a specific prop, not when any prop changes + w_X = () => this.X; // prettier-ignore + w_Y = () => this.Y; // prettier-ignore + w_Z = () => this.Z; // prettier-ignore + w_ZIndex = () => this.ZIndex ?? NumCast(this.Document.zIndex); // prettier-ignore + w_Rotation = () => this.Rotation ?? NumCast(this.Document._rotation); // prettier-ignore + w_Opacity = () => this.Opacity; // prettier-ignore + w_BackgroundColor = () => this.BackgroundColor ?? Cast(this.Document._backgroundColor, 'string', null); // prettier-ignore + w_Color = () => this.Color ?? Cast(this.Document._color, 'string', null); // prettier-ignore + w_Highlight = () => this.Highlight; // prettier-ignore + w_Width = () => this.Width; // prettier-ignore + w_Height = () => this.Height; // prettier-ignore + w_AutoDim = () => this.AutoDim; // prettier-ignore + w_Transition = () => this.Transition; // prettier-ignore + w_DataTransition = () => this.DataTransition; // prettier-ignore + + PanelWidth = () => this._props.autoDim ? this._props.PanelWidth?.() : this.Width; // prettier-ignore + PanelHeight = () => this._props.autoDim ? this._props.PanelHeight?.() : this.Height; // prettier-ignore + + componentDidUpdate(prevProps: Readonly<React.PropsWithChildren<CollectionFreeFormDocumentViewWrapperProps & { fieldKey: string }>>) { + super.componentDidUpdate(prevProps); + this.WrapperKeys.forEach(action(keys => ((this as any)[keys.upper] = (this.props as any)[keys.lower]))); + } + render() { + const layoutProps = this.WrapperKeys.reduce((val, keys) => [(val['w_' + keys.upper] = (this as any)['w_' + keys.upper]), val][1], {} as { [key: string]: Function }); + return ( + <CollectionFreeFormDocumentView + {...OmitKeys(this._props, this.WrapperKeys.map(keys => keys.lower) ).omit} // prettier-ignore + {...layoutProps} + PanelWidth={this.PanelWidth} + PanelHeight={this.PanelHeight} + /> + ); + } +} +export interface CollectionFreeFormDocumentViewProps extends DocumentViewProps { + w_X: () => number; + w_Y: () => number; + w_Z: () => number; + w_ZIndex?: () => number; + w_Rotation?: () => number; + w_Color: () => string; + w_BackgroundColor: () => string; + w_Opacity: () => number | undefined; + w_Highlight: () => boolean | undefined; + w_Transition: () => string | undefined; + w_Width: () => number; + w_Height: () => number; + w_DataTransition: () => string | undefined; + PanelWidth: () => number; + PanelHeight: () => number; + RenderCutoffProvider: (doc: Doc) => boolean; CollectionFreeFormView: CollectionFreeFormView; } @observer export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeFormDocumentViewProps>() { + constructor(props: CollectionFreeFormDocumentViewProps) { + super(props); + makeObservable(this); + } + get displayName() { // this makes mobx trace() statements more descriptive + return 'CollectionFreeFormDocumentView(' + this.Document.title + ')'; + } // prettier-ignore public static animFields: { key: string; val?: number }[] = [ - { key: '_height' }, - { key: '_width' }, { key: 'x' }, { key: 'y' }, + { key: 'opacity', val: 1 }, + { key: '_height' }, + { key: '_width' }, { key: '_rotation', val: 0 }, { key: '_layout_scrollTop' }, - { key: 'opacity', val: 1 }, { key: '_currentFrame' }, { key: 'freeform_scale', val: 1 }, { key: 'freeform_panX' }, @@ -44,124 +148,93 @@ 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 - @observable _animPos: number[] | undefined = undefined; - @observable _contentView: DocumentView | undefined | null; - get displayName() { - // this makes mobx trace() statements more descriptive - return 'CollectionFreeFormDocumentView(' + this.rootDoc.title + ')'; - } - get transform() { - return `translate(${this.X}px, ${this.Y}px) rotate(${NumCast(this.Rot, this.Rot)}deg)`; - } - get X() { - return this.dataProvider?.x ?? NumCast(this.Document.x); - } - get Y() { - return this.dataProvider?.y ?? NumCast(this.Document.y); - } - get ZInd() { - return this.dataProvider?.zIndex ?? NumCast(this.Document.zIndex); - } - get Rot() { - return this.dataProvider?.rotation ?? NumCast(this.Document._rotation); - } - get Opacity() { - return this.dataProvider?.opacity; - } - get BackgroundColor() { - return this.dataProvider?.backgroundColor ?? Cast(this.Document._backgroundColor, 'string', null); - } - get Color() { - return this.dataProvider?.color ?? Cast(this.Document._color, 'string', null); - } - @computed get dataProvider() { - return this.props.dataProvider?.(this.props.Document, this.props.replica); - } - @computed get sizeProvider() { - return this.props.sizeProvider?.(this.props.Document, this.props.replica); + get CollectionFreeFormView() { + return this._props.CollectionFreeFormView; } - styleProvider = (doc: Doc | undefined, props: Opt<DocumentViewProps>, property: string) => { + styleProvider = (doc: Doc | undefined, props: Opt<FieldViewProps>, property: string) => { if (doc === this.layoutDoc) { - // prettier-ignore switch (property) { - 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; - } + case StyleProp.Opacity: return this._props.w_Opacity(); // only change the opacity for this specific document, not its children + case StyleProp.BackgroundColor: return this._props.w_BackgroundColor(); + case StyleProp.Color: return this._props.w_Color(); + } // prettier-ignore } - return this.props.styleProvider?.(doc, props, property); + return this._props.styleProvider?.(doc, props, property); }; public static getValues(doc: Doc, time: number, fillIn: boolean = true) { - return CollectionFreeFormDocumentView.animFields.reduce((p, val) => { - p[val.key] = Cast(doc[`${val.key}-indexed`], listSpec('number'), fillIn ? [NumCast(doc[val.key], val.val)] : []).reduce((p, v, i) => ((i <= Math.round(time) && v !== undefined) || p === undefined ? v : p), undefined as any as number); - return p; - }, {} as { [val: string]: Opt<number> }); + return CollectionFreeFormDocumentView.animFields.reduce( + (p, val) => { + p[val.key] = Cast(doc[`${val.key}_indexed`], listSpec('number'), fillIn ? [NumCast(doc[val.key], val.val)] : []).reduce((p, v, i) => ((i <= Math.round(time) && v !== undefined) || p === undefined ? v : p), undefined as any as number); + return p; + }, + {} as { [val: string]: Opt<number> } + ); } public static getStringValues(doc: Doc, time: number) { - return CollectionFreeFormDocumentView.animStringFields.reduce((p, val) => { - p[val] = Cast(doc[`${val}-indexed`], listSpec('string'), [StrCast(doc[val])]).reduce((p, v, i) => ((i <= Math.round(time) && v !== undefined) || p === undefined ? v : p), undefined as any as string); - return p; - }, {} as { [val: string]: Opt<string> }); + return CollectionFreeFormDocumentView.animStringFields.reduce( + (p, val) => { + p[val] = Cast(doc[`${val}_indexed`], listSpec('string'), [StrCast(doc[val])]).reduce((p, v, i) => ((i <= Math.round(time) && v !== undefined) || p === undefined ? v : p), undefined as any as string); + return p; + }, + {} as { [val: string]: Opt<string> } + ); } public static setStringValues(time: number, d: Doc, vals: { [val: string]: Opt<string> }) { const timecode = Math.round(time); Object.keys(vals).forEach(val => { - const findexed = Cast(d[`${val}-indexed`], listSpec('string'), []).slice(); + const findexed = Cast(d[`${val}_indexed`], listSpec('string'), []).slice(); findexed[timecode] = vals[val] as any as string; - d[`${val}-indexed`] = new List<string>(findexed); + d[`${val}_indexed`] = new List<string>(findexed); }); } public static setValues(time: number, d: Doc, vals: { [val: string]: Opt<number> }) { const timecode = Math.round(time); Object.keys(vals).forEach(val => { - const findexed = Cast(d[`${val}-indexed`], listSpec('number'), []).slice(); + const findexed = Cast(d[`${val}_indexed`], listSpec('number'), []).slice(); findexed[timecode] = vals[val] as any as number; - d[`${val}-indexed`] = new List<number>(findexed); + d[`${val}_indexed`] = new List<number>(findexed); }); } - - public static setupZoom(doc: Doc, targDoc: Doc) { - const width = new List<number>(); - const height = new List<number>(); - const top = new List<number>(); - const left = new List<number>(); - width.push(NumCast(targDoc._width)); - height.push(NumCast(targDoc._height)); - top.push(NumCast(targDoc._height) / -2); - left.push(NumCast(targDoc._width) / -2); - doc['viewfinder-width-indexed'] = width; - doc['viewfinder-height-indexed'] = height; - doc['viewfinder-top-indexed'] = top; - doc['viewfinder-left-indexed'] = left; + public static gotoKeyFrame(doc: Doc, newFrame: number) { + if (doc) { + const childDocs = DocListCast(doc[Doc.LayoutFieldKey(doc)]); + const currentFrame = Cast(doc._currentFrame, 'number', null); + if (currentFrame === undefined) { + doc._currentFrame = 0; + CollectionFreeFormDocumentView.setupKeyframes(childDocs, 0); + } + CollectionFreeFormView.updateKeyframe(undefined, [...childDocs, doc], currentFrame || 0); + doc._currentFrame = newFrame === undefined ? 0 : Math.max(0, newFrame); + } } 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))); 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!!!) - targetDoc.activeFrame = ComputedField.MakeFunction('self.embedContainer?._currentFrame||0'); + targetDoc.activeFrame = ComputedField.MakeFunction('this.embedContainer?._currentFrame||0'); targetDoc.dataTransition = 'inherit'; }); } - @action public float = () => { - const topDoc = this.rootDoc; - const containerDocView = this.props.docViewPath().lastElement(); - const screenXf = containerDocView?.screenToLocalTransform(); + float = () => { + const topDoc = this.Document; + const containerDocView = this._props.containerViewPath?.().lastElement(); + const screenXf = containerDocView?.screenToContentsTransform(); if (screenXf) { SelectionManager.DeselectAll(); if (topDoc.z) { @@ -169,8 +242,8 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF topDoc.z = 0; topDoc.x = spt[0]; topDoc.y = spt[1]; - this.props.removeDocument?.(topDoc); - this.props.addDocTab(topDoc, OpenWhere.inParentFromScreen); + this._props.removeDocument?.(topDoc); + this._props.addDocTab(topDoc, OpenWhere.inParentFromScreen); } else { const spt = this.screenToLocalTransform().inverse().transformPoint(0, 0); const fpt = screenXf.transformPoint(spt[0], spt[1]); @@ -182,16 +255,16 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF } }; - dragEnding = () => this.props.CollectionFreeFormView?.dragEnding(); - dragStarting = () => this.props.CollectionFreeFormView?.dragStarting(false, true); - nudge = (x: number, y: number) => { - this.props.Document.x = NumCast(this.props.Document.x) + x; - this.props.Document.y = NumCast(this.props.Document.y) + y; + const [locX, locY] = this._props.ScreenToLocalTransform().transformDirection(x, y); + this.Document.x = this._props.w_X() + locX; + this.Document.y = this._props.w_Y() + locY; }; - panelWidth = () => this.sizeProvider?.width || this.props.PanelWidth?.(); - panelHeight = () => this.sizeProvider?.height || this.props.PanelHeight?.(); - screenToLocalTransform = (): Transform => this.props.ScreenToLocalTransform().translate(-this.X, -this.Y); + screenToLocalTransform = () => + this._props + .ScreenToLocalTransform() + .translate(-this._props.w_X(), -this._props.w_Y()) + .rotateDeg(-(this._props.w_Rotation?.() || 0)); returnThis = () => this; /// this indicates whether the doc view is activated because of its relationshop to a group @@ -200,40 +273,34 @@ 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.props.CollectionFreeFormView.isAnyChildContentActive()) return undefined; - const isGroup = this.rootDoc._isGroup && (!this.rootDoc.backgroundColor || this.rootDoc.backgroundColor === 'transparent'); - return isGroup ? (this.props.isDocumentActive?.() ? 'group' : this.props.isGroupActive?.() ? 'child' : 'inactive') : this.props.isGroupActive?.() ? 'child' : undefined; + if (this.CollectionFreeFormView.isAnyChildContentActive()) return undefined; + const isGroup = this.dataDoc.isGroup && (!this.layoutDoc.backgroundColor || this.layoutDoc.backgroundColor === 'transparent'); + return isGroup ? (this._props.isDocumentActive?.() ? 'group' : this._props.isGroupActive?.() ? 'child' : 'inactive') : this._props.isGroupActive?.() ? 'child' : undefined; }; + public static CollectionFreeFormDocViewClassName = 'collectionFreeFormDocumentView-container'; render() { TraceMobx(); - const divProps: DocumentViewProps = { - ...this.props, - CollectionFreeFormDocumentView: this.returnThis, - styleProvider: this.styleProvider, - ScreenToLocalTransform: this.screenToLocalTransform, - PanelWidth: this.panelWidth, - PanelHeight: this.panelHeight, - isGroupActive: this.isGroupActive, - }; + const passOnProps = OmitKeys(this._props, Object.keys(this._props).filter(key => key.startsWith('w_'))).omit; // prettier-ignore return ( <div - className="collectionFreeFormDocumentView-container" + className={CollectionFreeFormDocumentView.CollectionFreeFormDocViewClassName} style={{ - width: this.panelWidth(), - height: this.panelHeight(), - transform: this.transform, - transformOrigin: '50% 50%', - transition: this.dataProvider?.transition ?? (this.props.dataTransition ? this.props.dataTransition : this.dataProvider ? this.dataProvider.transition : StrCast(this.layoutDoc.dataTransition)), - zIndex: this.ZInd, - display: this.sizeProvider?.width ? undefined : 'none', - pointerEvents: 'none', + width: this._props.PanelWidth(), + height: this._props.PanelHeight(), + transform: `translate(${this._props.w_X()}px, ${this._props.w_Y()}px) rotate(${NumCast(this._props.w_Rotation?.())}deg)`, + transition: this._props.w_Transition?.() ?? (this._props.w_DataTransition?.() || this._props.w_Transition?.()), + zIndex: this._props.w_ZIndex?.(), + display: this._props.w_Width?.() ? undefined : 'none', }}> - {this.props.renderCutoffProvider(this.props.Document) ? ( - <div style={{ position: 'absolute', width: this.panelWidth(), height: this.panelHeight(), background: 'lightGreen' }} /> + {this._props.RenderCutoffProvider(this.Document) ? ( + <div style={{ position: 'absolute', width: this._props.PanelWidth(), height: this._props.PanelHeight(), background: 'lightGreen' }} /> ) : ( - <DocumentView {...divProps} ref={action((r: DocumentView | null) => (this._contentView = r))} /> + <DocumentView {...passOnProps} CollectionFreeFormDocumentView={this.returnThis} styleProvider={this.styleProvider} ScreenToLocalTransform={this.screenToLocalTransform} isGroupActive={this.isGroupActive} /> )} </div> ); } } +ScriptingGlobals.add(function gotoFrame(doc: any, newFrame: any) { + CollectionFreeFormDocumentView.gotoKeyFrame(doc, newFrame); +}); diff --git a/src/client/views/nodes/ColorBox.scss b/src/client/views/nodes/ColorBox.scss deleted file mode 100644 index d5f2a7ec7..000000000 --- a/src/client/views/nodes/ColorBox.scss +++ /dev/null @@ -1,22 +0,0 @@ -.colorBox-container, .colorBox-container-interactive { - width:100%; - height:100%; - position: relative; - pointer-events: none; - transform-origin: top left; - - .sketch-picker { - div { - cursor: crosshair; - } - .flexbox-fix { - cursor: pointer; - div { - cursor:pointer; - } - } - } -} -.colorBox-container-interactive { - pointer-events:all; -}
\ No newline at end of file diff --git a/src/client/views/nodes/ColorBox.tsx b/src/client/views/nodes/ColorBox.tsx deleted file mode 100644 index 1b6fe5748..000000000 --- a/src/client/views/nodes/ColorBox.tsx +++ /dev/null @@ -1,92 +0,0 @@ -import React = require('react'); -import { action } from 'mobx'; -import { observer } from 'mobx-react'; -import { ColorState, SketchPicker } from 'react-color'; -import { Doc } from '../../../fields/Doc'; -import { Height, Width } from '../../../fields/DocSymbols'; -import { InkTool } from '../../../fields/InkField'; -import { StrCast } from '../../../fields/Types'; -import { DocumentType } from '../../documents/DocumentTypes'; -import { SelectionManager } from '../../util/SelectionManager'; -import { undoBatch } from '../../util/UndoManager'; -import { ViewBoxBaseComponent } from '../DocComponent'; -import { ActiveInkColor, ActiveInkWidth, SetActiveInkColor, SetActiveInkWidth } from '../InkingStroke'; -import './ColorBox.scss'; -import { FieldView, FieldViewProps } from './FieldView'; -import { RichTextMenu } from './formattedText/RichTextMenu'; -import { ScriptingGlobals } from '../../util/ScriptingGlobals'; -import { DashColor } from '../../../Utils'; - -@observer -export class ColorBox extends ViewBoxBaseComponent<FieldViewProps>() { - public static LayoutString(fieldKey: string) { - return FieldView.LayoutString(ColorBox, fieldKey); - } - - @undoBatch - @action - static switchColor(color: ColorState) { - SetActiveInkColor(color.hex); - - SelectionManager.Views().map(view => { - const targetDoc = - view.props.Document.dragFactory instanceof Doc - ? view.props.Document.dragFactory - : view.props.Document.layout instanceof Doc - ? view.props.Document.layout - : view.props.Document.isTemplateForField - ? view.props.Document - : Doc.GetProto(view.props.Document); - if (targetDoc) { - if (view.props.LayoutTemplate?.() || view.props.LayoutTemplateString) { - // this situation typically occurs when you have a link dot - targetDoc.backgroundColor = color.hex; // bcz: don't know how to change the color of an inline template... - } else if (RichTextMenu.Instance?.TextViewFieldKey && window.getSelection()?.toString() !== '') { - Doc.Layout(view.props.Document)[RichTextMenu.Instance.TextViewFieldKey + '-color'] = color.hex; - } else { - Doc.Layout(view.props.Document)._backgroundColor = color.hex + (color.rgb.a ? Math.round(color.rgb.a * 255).toString(16) : ''); // '_backgroundColor' is template specific. 'backgroundColor' would apply to all templates, but has no UI at the moment - } - } - }); - } - - render() { - const scaling = Math.min(this.layoutDoc.layout_fitWidth ? 10000 : this.props.PanelHeight() / this.rootDoc[Height](), this.props.PanelWidth() / this.rootDoc[Width]()); - return ( - <div - className={`colorBox-container${this.props.isContentActive() ? '-interactive' : ''}`} - onPointerDown={e => e.button === 0 && !e.ctrlKey && e.stopPropagation()} - onClick={e => e.stopPropagation()} - style={{ transform: `scale(${scaling})`, width: `${100 * scaling}%`, height: `${100 * scaling}%` }}> - <SketchPicker - onChange={c => Doc.ActiveTool === InkTool.None && ColorBox.switchColor(c)} - color={StrCast(SelectionManager.Views()?.[0]?.rootDoc?._backgroundColor, ActiveInkColor())} - presetColors={['#D0021B', '#F5A623', '#F8E71C', '#8B572A', '#7ED321', '#417505', '#9013FE', '#4A90E2', '#50E3C2', '#B8E986', '#000000', '#4A4A4A', '#9B9B9B', '#FFFFFF', '#f1efeb', 'transparent']} - /> - - <div style={{ width: this.props.PanelWidth() / scaling, display: 'flex', paddingTop: '10px' }}> - <div> {ActiveInkWidth()}</div> - <input - type="range" - defaultValue={ActiveInkWidth()} - min={1} - max={100} - onChange={(e: React.ChangeEvent<HTMLInputElement>) => { - SetActiveInkWidth(e.target.value); - SelectionManager.Views() - .filter(i => StrCast(i.rootDoc.type) === DocumentType.INK) - .map(i => (i.rootDoc.stroke_width = Number(e.target.value))); - }} - /> - </div> - </div> - ); - } -} - - -ScriptingGlobals.add( - function interpColors(c1:string, c2:string, weight=0.5) { - return DashColor(c1).mix(DashColor(c2),weight) - } -)
\ No newline at end of file diff --git a/src/client/views/nodes/ComparisonBox.tsx b/src/client/views/nodes/ComparisonBox.tsx index b09fcd882..116dc48a6 100644 --- a/src/client/views/nodes/ComparisonBox.tsx +++ b/src/client/views/nodes/ComparisonBox.tsx @@ -1,27 +1,30 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { action, computed, observable } from 'mobx'; +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 { Doc, Opt } from '../../../fields/Doc'; import { DocCast, NumCast, StrCast } from '../../../fields/Types'; -import { emptyFunction, returnFalse, returnNone, returnZero, setupMoveUpEvents } from '../../../Utils'; -import { Docs, DocUtils } from '../../documents/Documents'; +import { DocUtils, Docs } from '../../documents/Documents'; import { DragManager } from '../../util/DragManager'; import { undoBatch } from '../../util/UndoManager'; -import { ViewBoxAnnotatableComponent, ViewBoxAnnotatableProps } from '../DocComponent'; +import { ViewBoxAnnotatableComponent, ViewBoxInterface } from '../DocComponent'; import { StyleProp } from '../StyleProvider'; import './ComparisonBox.scss'; -import { DocumentView, DocumentViewProps } from './DocumentView'; +import { DocumentView } from './DocumentView'; import { FieldView, FieldViewProps } from './FieldView'; import { PinProps, PresBox } from './trails'; -import React = require('react'); @observer -export class ComparisonBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps & FieldViewProps>() { +export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implements ViewBoxInterface { public static LayoutString(fieldKey: string) { return FieldView.LayoutString(ComparisonBox, fieldKey); } - protected _multiTouchDisposer?: import('../../util/InteractionUtils').InteractionUtils.MultiTouchEventDisposer | undefined; private _disposers: (DragManager.DragDropDisposer | undefined)[] = [undefined, undefined]; + constructor(props: FieldViewProps) { + super(props); + makeObservable(this); + } @observable _animating = ''; @@ -29,10 +32,10 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatabl return NumCast(this.layoutDoc[this.clipWidthKey], 50); } get clipWidthKey() { - return '_' + this.props.fieldKey + '_clipWidth'; + return '_' + this._props.fieldKey + '_clipWidth'; } componentDidMount() { - this.props.setContentView?.(this); + this._props.setContentViewBox?.(this); } protected createDropTarget = (ele: HTMLDivElement | null, fieldKey: string, disposerId: number) => { this._disposers[disposerId]?.(); @@ -45,7 +48,7 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatabl 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.rootDoc, (doc: Doc | Doc[]) => this.addDoc(doc instanceof Doc ? doc : doc.lastElement(), fieldKey)); + 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); !added && e.preventDefault(); e.stopPropagation(); // prevent parent Doc from registering new position so that it snaps back into place @@ -73,7 +76,7 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatabl if (this._isAnyChildContentActive) return; this._animating = 'all 200ms'; // on click, animate slider movement to the targetWidth - this.layoutDoc[this.clipWidthKey] = (targetWidth * 100) / this.props.PanelWidth(); + this.layoutDoc[this.clipWidthKey] = (targetWidth * 100) / this._props.PanelWidth(); setTimeout( action(() => (this._animating = '')), 200 @@ -85,27 +88,27 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatabl @action private onPointerMove = ({ movementX }: PointerEvent) => { - const width = movementX * this.props.ScreenToLocalTransform().Scale + (this.clipWidth / 100) * this.props.PanelWidth(); - if (width && width > 5 && width < this.props.PanelWidth()) { - this.layoutDoc[this.clipWidthKey] = (width * 100) / this.props.PanelWidth(); + const width = movementX * this.ScreenToLocalBoxXf().Scale + (this.clipWidth / 100) * this._props.PanelWidth(); + if (width && width > 5 && width < this._props.PanelWidth()) { + this.layoutDoc[this.clipWidthKey] = (width * 100) / this._props.PanelWidth(); } return false; }; getAnchor = (addAsAnnotation: boolean, pinProps?: PinProps) => { const anchor = Docs.Create.ConfigDocument({ - title: 'CompareAnchor:' + this.rootDoc.title, + title: 'CompareAnchor:' + this.Document.title, // set presentation timing properties for restoring view presentation_transition: 1000, - annotationOn: this.rootDoc, + annotationOn: this.Document, }); if (anchor) { if (!addAsAnnotation) anchor.backgroundColor = 'transparent'; /* addAsAnnotation &&*/ this.addDocument(anchor); - PresBox.pinDocView(anchor, { pinDocLayout: pinProps?.pinDocLayout, pinData: { ...(pinProps?.pinData ?? {}), clippable: true } }, this.rootDoc); + PresBox.pinDocView(anchor, { pinDocLayout: pinProps?.pinDocLayout, pinData: { ...(pinProps?.pinData ?? {}), clippable: true } }, this.Document); return anchor; } - return this.rootDoc; + return this.Document; }; @undoBatch @@ -145,9 +148,9 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatabl e => this.clearDoc(which) ); }; - docStyleProvider = (doc: Opt<Doc>, props: Opt<DocumentViewProps>, property: string): any => { + docStyleProvider = (doc: Opt<Doc>, props: Opt<FieldViewProps>, property: string): any => { if (property === StyleProp.PointerEvents) return 'none'; - return this.props.styleProvider?.(doc, props, property); + 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); @@ -172,9 +175,10 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatabl return targetDoc ? ( <> <DocumentView - {...this.props} + {...this._props} Document={targetDoc} - DataDoc={undefined} + TemplateDataDocument={undefined} + containerViewPath={this.DocumentView?.().docViewPath} moveDocument={which.endsWith('1') ? this.moveDoc1 : this.moveDoc2} removeDocument={which.endsWith('1') ? this.remDoc1 : this.remDoc2} NativeWidth={returnZero} @@ -182,7 +186,7 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatabl isContentActive={emptyFunction} isDocumentActive={returnFalse} whenChildContentsActiveChanged={this.whenChildContentsActiveChanged} - styleProvider={this._isAnyChildContentActive ? this.props.styleProvider : this.docStyleProvider} + styleProvider={this._isAnyChildContentActive ? this._props.styleProvider : this.docStyleProvider} hideLinkButton={true} pointerEvents={this._isAnyChildContentActive ? undefined : returnNone} /> @@ -196,15 +200,15 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatabl }; 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)}> + <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 */}> - {displayBox(`${this.fieldKey}_2`, 1, this.props.PanelWidth() - 3)} + <div className={`comparisonBox${this._props.isContentActive() ? '-interactive' : ''}` /* change className to easily disable/enable pointer events in CSS */}> + {displayBox(`${this.fieldKey}_2`, 1, this._props.PanelWidth() - 3)} <div className="clip-div" style={{ width: this.clipWidth + '%', transition: this._animating, background: StrCast(this.layoutDoc._backgroundColor, 'gray') }}> {displayBox(`${this.fieldKey}_1`, 0, 0)} </div> @@ -213,9 +217,9 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatabl className="slide-bar" style={{ left: `calc(${this.clipWidth + '%'} - 0.5px)`, - cursor: this.clipWidth < 5 ? 'e-resize' : this.clipWidth / 100 > (this.props.PanelWidth() - 5) / this.props.PanelWidth() ? 'w-resize' : undefined, + cursor: this.clipWidth < 5 ? 'e-resize' : this.clipWidth / 100 > (this._props.PanelWidth() - 5) / this._props.PanelWidth() ? 'w-resize' : undefined, }} - onPointerDown={e => !this._isAnyChildContentActive && this.registerSliding(e, this.props.PanelWidth() / 2)} /* if clicked, return slide-bar to center */ + onPointerDown={e => !this._isAnyChildContentActive && this.registerSliding(e, this._props.PanelWidth() / 2)} /* if clicked, return slide-bar to center */ > <div className="slide-handle" /> </div> diff --git a/src/client/views/nodes/DataVizBox/DataVizBox.scss b/src/client/views/nodes/DataVizBox/DataVizBox.scss index 430446c06..a3132dc6e 100644 --- a/src/client/views/nodes/DataVizBox/DataVizBox.scss +++ b/src/client/views/nodes/DataVizBox/DataVizBox.scss @@ -1,4 +1,4 @@ -.dataviz { +.dataViz-box { overflow: auto; height: 100%; width: 100%; @@ -7,9 +7,46 @@ display: flex; flex-direction: row; } + + .dataviz-overlayButton-sidebar { + background: #121721; + height: 25px; + width: 25px; + right: 5px; + display: flex; + position: absolute; + align-items: center; + justify-content: center; + border-radius: 3px; + pointer-events: all; + z-index: 1; // so it appears on top of the document's title, if shown + + // box-shadow: $standard-box-shadow; + // transition: 0.2s; + + &:hover { + filter: brightness(0.85); + } + } + + .dataviz-sidebar { + position: absolute; + right: 0; + top: 0; + height: 100%; + } .button-container { pointer-events: unset; } + + .dataVizBox-annotationLayer{ + position: absolute; + transform-origin: left top; + top: 0; + width: 100%; + pointer-events: none; + mix-blend-mode: multiply; + } } .start-message { margin: 10px; diff --git a/src/client/views/nodes/DataVizBox/DataVizBox.tsx b/src/client/views/nodes/DataVizBox/DataVizBox.tsx index 299494c83..8365f4770 100644 --- a/src/client/views/nodes/DataVizBox/DataVizBox.tsx +++ b/src/client/views/nodes/DataVizBox/DataVizBox.tsx @@ -1,20 +1,32 @@ -import { Toggle, ToggleType, Type } from 'browndash-components'; -import { action, computed, ObservableMap } from 'mobx'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { Colors, Toggle, ToggleType, Type } from 'browndash-components'; +import { ObservableMap, action, computed, makeObservable, observable, runInAction } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; -import { Doc, Field, StrListCast } from '../../../../fields/Doc'; +import { emptyFunction, returnEmptyString, returnFalse, returnOne, setupMoveUpEvents } from '../../../../Utils'; +import { Doc, DocListCast, Field, Opt, StrListCast } from '../../../../fields/Doc'; +import { InkTool } from '../../../../fields/InkField'; import { List } from '../../../../fields/List'; -import { Cast, CsvCast, StrCast } from '../../../../fields/Types'; +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 { Docs } from '../../../documents/Documents'; -import { ViewBoxAnnotatableComponent } from '../../DocComponent'; -import { FieldView, FieldViewProps } from '../FieldView'; +import { DocumentManager } from '../../../util/DocumentManager'; +import { UndoManager, undoable } from '../../../util/UndoManager'; +import { ViewBoxAnnotatableComponent, ViewBoxInterface } from '../../DocComponent'; +import { MarqueeAnnotator } from '../../MarqueeAnnotator'; +import { SidebarAnnos } from '../../SidebarAnnos'; +import { AnchorMenu } from '../../pdf/AnchorMenu'; +import { GPTPopup } from '../../pdf/GPTPopup/GPTPopup'; +import { DocumentView } from '../DocumentView'; +import { FocusViewOptions, FieldView, FieldViewProps } from '../FieldView'; import { PinProps } from '../trails'; +import './DataVizBox.scss'; import { Histogram } from './components/Histogram'; import { LineChart } from './components/LineChart'; import { PieChart } from './components/PieChart'; import { TableBox } from './components/TableBox'; -import './DataVizBox.scss'; export enum DataVizView { TABLE = 'table', @@ -24,7 +36,49 @@ export enum DataVizView { } @observer -export class DataVizBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { +export class DataVizBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implements ViewBoxInterface { + private _mainCont: React.RefObject<HTMLDivElement> = React.createRef(); + private _marqueeref = React.createRef<MarqueeAnnotator>(); + private _annotationLayer: React.RefObject<HTMLDivElement> = React.createRef(); + anchorMenuClick?: () => undefined | ((anchor: Doc) => void); + crop: ((region: Doc | undefined, addCrop?: boolean) => Doc | undefined) | undefined; + @observable _schemaDataVizChildren: any = undefined; + @observable _marqueeing: number[] | undefined = undefined; + @observable _savedAnnotations = new ObservableMap<number, HTMLDivElement[]>(); + + constructor(props: FieldViewProps) { + super(props); + makeObservable(this); + this._props.setContentViewBox?.(this); + } + + @computed get annotationLayer() { + TraceMobx(); + return <div className="dataVizBox-annotationLayer" style={{ height: this._props.PanelHeight(), width: this._props.PanelWidth() }} ref={this._annotationLayer} />; + } + marqueeDown = (e: React.PointerEvent) => { + if (!e.altKey && e.button === 0 && NumCast(this.Document._freeform_scale, 1) <= NumCast(this.Document.freeform_scaleMin, 1) && this._props.isContentActive() && ![InkTool.Highlighter, InkTool.Pen, InkTool.Write].includes(Doc.ActiveTool)) { + setupMoveUpEvents( + this, + e, + action(e => { + MarqueeAnnotator.clearAnnotations(this._savedAnnotations); + this._marqueeref.current?.onInitiateSelection([e.clientX, e.clientY]); + return true; + }), + returnFalse, + () => MarqueeAnnotator.clearAnnotations(this._savedAnnotations), + false + ); + } + }; + @action + finishMarquee = () => { + this._marqueeref.current?.onTerminateSelection(); + this._props.select(false); + }; + savedAnnotations = () => this._savedAnnotations; + public static LayoutString(fieldStr: string) { return FieldView.LayoutString(DataVizBox, fieldStr); } @@ -32,10 +86,11 @@ export class DataVizBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { // all datasets that have been retrieved from the server stored as a map from the dataset url to an array of records static dataset = new ObservableMap<string, { [key: string]: string }[]>(); private _vizRenderer: LineChart | Histogram | PieChart | undefined; + private _sidebarRef = React.createRef<SidebarAnnos>(); // all CSV records in the dataset (that aren't an empty row) @computed.struct get records() { - var records = DataVizBox.dataset.get(CsvCast(this.rootDoc[this.fieldKey]).url.href); + var records = DataVizBox.dataset.get(CsvCast(this.dataDoc[this.fieldKey]).url.href); return records?.filter(record => Object.keys(record).some(key => record[key])) ?? []; } @@ -73,11 +128,18 @@ export class DataVizBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { } return func() ?? false; }; - getAnchor = (addAsAnnotation?: boolean, pinProps?: PinProps) => { + getAnchor = (addAsAnnotation: boolean, pinProps?: PinProps) => { + const visibleAnchor = AnchorMenu.Instance.GetAnchor?.(undefined, addAsAnnotation); const anchor = !pinProps - ? this.rootDoc + ? this.Document : this._vizRenderer?.getAnchor(pinProps) ?? + visibleAnchor ?? Docs.Create.ConfigDocument({ + title: 'ImgAnchor:' + this.Document.title, + config_panX: NumCast(this.layoutDoc._freeform_panX), + config_panY: NumCast(this.layoutDoc._freeform_panY), + config_viewScale: Cast(this.layoutDoc._freeform_scale, 'number', null), + 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*/ @@ -93,73 +155,288 @@ export class DataVizBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { anchor[key] = this.layoutDoc[key]; } }); + 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' + ); + if (!this.layoutDoc.layout_showSidebar) { + this.toggleSidebar(); + setTimeout(createFunc); + } else createFunc(); + }; + + @observable _showSidebar = false; + @observable _previewNativeWidth: Opt<number> = undefined; + @observable _previewWidth: Opt<number> = undefined; + @action + toggleSidebar = () => { + const prevWidth = this.sidebarWidth(); + this.layoutDoc._layout_showSidebar = (this.layoutDoc._layout_sidebarWidthPercent = StrCast(this.layoutDoc._layout_sidebarWidthPercent, '0%') === '0%' ? `${(100 * 0.2) / 1.2}%` : '0%') !== '0%'; + 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; + } + @computed get sidebarHandle() { + return ( + <div + className="dataviz-overlayButton-sidebar" + key="sidebar" + title="Toggle Sidebar" + style={{ + display: !this._props.isContentActive() ? 'none' : undefined, + top: StrCast(this.Document._layout_showTitle) === 'title' ? 20 : 5, + backgroundColor: this.SidebarShown ? Colors.MEDIUM_BLUE : Colors.BLACK, + }} + onPointerDown={this.sidebarBtnDown}> + <FontAwesomeIcon style={{ color: Colors.WHITE }} icon={'comment-alt'} size="sm" /> + </div> + ); + } + /** + * Toggle sidebar onclick the tiny comment button on the top right corner + * @param e + */ + sidebarBtnDown = (e: React.PointerEvent) => { + setupMoveUpEvents( + this, + e, + (e, down, delta) => + runInAction(() => { + const localDelta = this._props + .ScreenToLocalTransform() + .scale(this._props.NativeDimScaling?.() || 1) + .transformDirection(delta[0], delta[1]); + const fullWidth = NumCast(this.layoutDoc._width); + const mapWidth = fullWidth - this.sidebarWidth(); + if (this.sidebarWidth() + localDelta[0] > 0) { + this.layoutDoc._layout_showSidebar = true; + this.layoutDoc._layout_sidebarWidthPercent = ((100 * (this.sidebarWidth() + localDelta[0])) / (fullWidth + localDelta[0])).toString() + '%'; + this.layoutDoc._width = fullWidth + localDelta[0]; + } else { + this.layoutDoc._layout_showSidebar = false; + this.layoutDoc._width = mapWidth; + this.layoutDoc._layout_sidebarWidthPercent = '0%'; + } + return false; + }), + emptyFunction, + () => UndoManager.RunInBatch(this.toggleSidebar, 'toggle sidebar') + ); + }; + getView = async (doc: Doc, options: FocusViewOptions) => { + if (this._sidebarRef?.current?.makeDocUnfiltered(doc) && !this.SidebarShown) { + options.didMove = true; + this.toggleSidebar(); + } + return new Promise<Opt<DocumentView>>(res => DocumentManager.Instance.AddViewRenderedCb(doc, dv => res(dv))); + }; + @computed get sidebarWidthPercent() { + return StrCast(this.layoutDoc._layout_sidebarWidthPercent, '0%'); + } + @computed get sidebarColor() { + return StrCast(this.layoutDoc.sidebar_color, StrCast(this.layoutDoc[this._props.fieldKey + '_backgroundColor'], '#e4e4e4')); + } + sidebarWidth = () => (Number(this.sidebarWidthPercent.substring(0, this.sidebarWidthPercent.length - 1)) / 100) * this._props.PanelWidth(); + sidebarAddDocument = (doc: Doc | Doc[], sidebarKey?: string) => { + if (!this.SidebarShown) this.toggleSidebar(); + return this.addDocument(doc, sidebarKey); + }; + sidebarRemoveDocument = (doc: Doc | Doc[], sidebarKey?: string) => this.removeDocument(doc, sidebarKey); + componentDidMount() { - this.props.setContentView?.(this); - if (!DataVizBox.dataset.has(CsvCast(this.rootDoc[this.fieldKey]).url.href)) this.fetchData(); + this._props.setContentViewBox?.(this); + if (!DataVizBox.dataset.has(CsvCast(this.dataDoc[this.fieldKey]).url.href)) this.fetchData(); } fetchData = () => { - DataVizBox.dataset.set(CsvCast(this.rootDoc[this.fieldKey]).url.href, []); // assign temporary dataset as a lock to prevent duplicate server requests + 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.rootDoc[this.fieldKey]).url.href, res)))); + .then(res => res.json().then(action(res => !res.errno && DataVizBox.dataset.set(CsvCast(this.dataDoc[this.fieldKey]).url.href, res)))); }; // toggles for user to decide which chart type to view the data in - renderVizView = () => { + @computed get renderVizView() { + const scale = this._props.NativeDimScaling?.() || 1; const sharedProps = { - rootDoc: this.rootDoc, + Document: this.Document, layoutDoc: this.layoutDoc, records: this.records, axes: this.axes, - height: (this.props.PanelHeight() - 32) /* height of 'change view' button */ * 0.9, - width: 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 }, }; if (!this.records.length) return 'no data/visualization'; switch (this.dataVizView) { - case DataVizView.TABLE: - return <TableBox {...sharedProps} docView={this.props.DocumentView} selectAxes={this.selectAxes} />; - case DataVizView.LINECHART: - return <LineChart {...sharedProps} dataDoc={this.dataDoc} fieldKey={this.fieldKey} ref={r => (this._vizRenderer = r ?? undefined)} />; - 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.TABLE: return <TableBox {...sharedProps} docView={this.DocumentView} selectAxes={this.selectAxes} />; + 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 }} />; + } // prettier-ignore + } + + @action + onPointerDown = (e: React.PointerEvent): void => { + if ((this.Document._freeform_scale || 1) !== 1) return; + if (!e.altKey && e.button === 0 && this._props.isContentActive() && ![InkTool.Highlighter, InkTool.Pen, InkTool.Write].includes(Doc.ActiveTool)) { + this._props.select(false); + MarqueeAnnotator.clearAnnotations(this._savedAnnotations); + 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'))) { + } else { + // if textLayer is hit, then we select text instead of using a marquee so clear out the marquee. + setTimeout( + 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. + + document.addEventListener('pointerup', this.onSelectEnd); + } } }; + @action + onSelectEnd = (e: PointerEvent): void => { + this._props.select(false); + document.removeEventListener('pointerup', this.onSelectEnd); + + const sel = window.getSelection(); + if (sel) { + AnchorMenu.Instance.setSelectedText(sel.toString()); + } + + if (sel?.type === 'Range') { + AnchorMenu.Instance.jumpTo(e.clientX, e.clientY); + } + + // Changing which document to add the annotation to (the currently selected PDF) + GPTPopup.Instance.setSidebarId('data_sidebar'); + GPTPopup.Instance.addDoc = this.sidebarAddDocument; + }; + + @action + updateSchemaViz = () => { + 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 }[] = []; + for (let i = 0; i < children.length; i++) { + var row: { [key: string]: string } = {}; + if (children[i]) { + for (let j = 0; j < keys.length; j++) { + var cell = children[i][keys[j]]; + if (cell && (cell as string)) cell = cell.toString().replace(/\,/g, ''); + row[keys[j]] = StrCast(cell); + } + } + current.push(row); + } + DataVizBox.dataset.set(CsvCast(this.Document[this.fieldKey]).url.href, current); + }; + render() { + if (this.layoutDoc && this.layoutDoc.dataViz_asSchema) { + this._schemaDataVizChildren = DocListCast(DocCast(this.layoutDoc.dataViz_asSchema)[Doc.LayoutFieldKey(DocCast(this.layoutDoc.dataViz_asSchema))]).length; + this.updateSchemaViz(); + } + + const scale = this._props.NativeDimScaling?.() || 1; 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="dataViz" + className="dataViz-box" + onPointerDown={this.marqueeDown} style={{ - pointerEvents: this.props.isContentActive() === true ? 'all' : 'none', + pointerEvents: this._props.isContentActive() === true ? 'all' : 'none', + width: `${100 / scale}%`, + height: `${100 / scale}%`, + transform: `scale(${scale})`, + position: 'absolute', }} onWheel={e => e.stopPropagation()} - ref={r => - r?.addEventListener( - 'wheel', // 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) - (e: WheelEvent) => { - if (!r.scrollTop && e.deltaY <= 0) e.preventDefault(); - e.stopPropagation(); - }, - { passive: false } - ) - }> - <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} /> + 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} /> + </div> + + {/* <CollectionFreeFormView + ref={this._ffref} + {...this._props} + setContentView={emptyFunction} + renderDepth={this._props.renderDepth - 1} + fieldKey={this.annotationKey} + styleProvider={this._props.styleProvider} + isAnnotationOverlay={true} + annotationLayerHostsContent={false} + PanelWidth={this._props.PanelWidth} + PanelHeight={this._props.PanelHeight} + select={emptyFunction} + isAnyChildContentActive={returnFalse} + whenChildContentsActiveChanged={this.whenChildContentsActiveChanged} + removeDocument={this.removeDocument} + moveDocument={this.moveDocument} + addDocument={this.addDocument}> + {this.renderVizView} + </CollectionFreeFormView> */} + + {this.renderVizView} + <div className="dataviz-sidebar" style={{ width: `${this.sidebarWidthPercent}`, backgroundColor: `${this.sidebarColor}` }} onPointerDown={this.onPointerDown}> + <SidebarAnnos + ref={this._sidebarRef} + {...this._props} + fieldKey={this.fieldKey} + Document={this.Document} + layoutDoc={this.layoutDoc} + dataDoc={this.dataDoc} + usePanelWidth={true} + showSidebar={this.SidebarShown} + nativeWidth={NumCast(this.layoutDoc._nativeWidth)} + whenChildContentsActiveChanged={this.whenChildContentsActiveChanged} + PanelWidth={this.sidebarWidth} + sidebarAddDocument={this.sidebarAddDocument} + moveDocument={this.moveDocument} + removeDocument={this.sidebarRemoveDocument} + /> </div> - {this.renderVizView()} + {this.sidebarHandle} + {this.annotationLayer} + {!this._mainCont.current || !this.DocumentView || !this._annotationLayer.current ? null : ( + <MarqueeAnnotator + ref={this._marqueeref} + Document={this.Document} + anchorMenuClick={this.anchorMenuClick} + scrollTop={0} + annotationLayerScrollTop={NumCast(this.Document._layout_scrollTop)} + scaling={returnOne} + docView={this.DocumentView} + addDocument={this.sidebarAddDocument} + finishMarquee={this.finishMarquee} + savedAnnotations={this.savedAnnotations} + selectionText={returnEmptyString} + annotationLayer={this._annotationLayer.current} + marqueeContainer={this._mainCont.current} + anchorMenuCrop={this.crop} + /> + )} </div> ); } diff --git a/src/client/views/nodes/DataVizBox/SchemaCSVPopUp.scss b/src/client/views/nodes/DataVizBox/SchemaCSVPopUp.scss new file mode 100644 index 000000000..63a693918 --- /dev/null +++ b/src/client/views/nodes/DataVizBox/SchemaCSVPopUp.scss @@ -0,0 +1,175 @@ +$textgrey: #707070; +$lighttextgrey: #a3a3a3; +$greyborder: #d3d3d3; +$lightgrey: #ececec; +$button: #5b97ff; +$highlightedText: #82e0ff; + +.summary-box { + position: fixed; + bottom: 10px; + right: 10px; + width: 250px; + min-height: 200px; + border-radius: 15px; + padding: 15px; + padding-bottom: 0; + z-index: 999; + display: flex; + flex-direction: column; + justify-content: space-between; + background-color: #ffffff; + box-shadow: 0 2px 5px #7474748d; + color: $textgrey; + + .summary-heading { + display: flex; + justify-content: space-between; + align-items: center; + border-bottom: 1px solid $greyborder; + padding-bottom: 5px; + + .summary-text { + font-size: 12px; + font-weight: 500; + } + } + + label { + color: $textgrey; + font-size: 12px; + font-weight: 400; + letter-spacing: 1px; + margin: 0; + padding-right: 5px; + } + + a { + cursor: pointer; + } + + .content-wrapper { + padding-top: 10px; + min-height: 50px; + max-height: 150px; + overflow-y: auto; + } + + .btns-wrapper { + height: 50px; + display: flex; + justify-content: space-between; + align-items: center; + + .summarizing { + display: flex; + align-items: center; + } + } + + button { + font-size: 9px; + padding: 10px; + color: #ffffff; + background-color: $button; + border-radius: 5px; + } + + .text-btn { + &:hover { + background-color: $button; + } + } + + .btn-secondary { + font-size: 8px; + padding: 10px 5px; + background-color: $lightgrey; + color: $textgrey; + &:hover { + background-color: $lightgrey; + } + } + + .icon-btn { + background-color: #ffffff; + padding: 10px; + border-radius: 50%; + color: $button; + border: 1px solid $button; + } +} + +.image-content-wrapper { + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 8px; + padding-bottom: 16px; + + .img-wrapper { + position: relative; + cursor: pointer; + + .img-container { + pointer-events: none; + position: relative; + + img { + pointer-events: all; + position: relative; + } + } + + .img-container::after { + content: ''; + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + background-color: rgba(0, 0, 0, 0.5); + opacity: 0; + transition: opacity 0.3s ease; + } + + .btn-container { + position: absolute; + right: 8px; + bottom: 8px; + opacity: 0; + transition: opacity 0.3s ease; + } + + &:hover { + .img-container::after { + opacity: 1; + } + + .btn-container { + opacity: 1; + } + } + } +} + +// Typist CSS +.Typist .Cursor { + display: inline-block; +} +.Typist .Cursor--blinking { + opacity: 1; + animation: blink 1s linear infinite; +} + +@keyframes blink { + 0% { + opacity: 1; + } + 50% { + opacity: 0; + } + 100% { + opacity: 1; + } +} diff --git a/src/client/views/nodes/DataVizBox/SchemaCSVPopUp.tsx b/src/client/views/nodes/DataVizBox/SchemaCSVPopUp.tsx new file mode 100644 index 000000000..24023077f --- /dev/null +++ b/src/client/views/nodes/DataVizBox/SchemaCSVPopUp.tsx @@ -0,0 +1,109 @@ +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 { Doc } from '../../../../fields/Doc'; +import { StrCast } from '../../../../fields/Types'; +import { DragManager } from '../../../util/DragManager'; +import { DocumentView } from '../DocumentView'; +import './SchemaCSVPopUp.scss'; + +interface SchemaCSVPopUpProps {} + +@observer +export class SchemaCSVPopUp extends React.Component<SchemaCSVPopUpProps> { + static Instance: SchemaCSVPopUp; + + @observable + public dataVizDoc: Doc | undefined = undefined; + @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> + </div> + </div> + </div> + ); + }; + + heading = (headingText: string) => ( + <div className="summary-heading"> + <label className="summary-text">{headingText}</label> + <IconButton color={StrCast(Doc.UserDoc().userVariantColor)} tooltip="close" icon={<CgClose size="16px" />} onClick={() => this.setVisible(false)} /> + </div> + ); + + drag = (e: React.PointerEvent) => { + const downX = e.clientX; + const downY = e.clientY; + setupMoveUpEvents( + {}, + e, + e => { + const sourceAnchorCreator = () => this.dataVizDoc!; + const targetCreator = (annotationOn: Doc | undefined) => { + 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); + }, + }); + return true; + } + return false; + }, + emptyFunction, + action(e => {}) + ); + }; + + render() { + return ( + <div className="summary-box" style={{ display: this.visible ? 'flex' : 'none' }}> + {this.dataBox()} + </div> + ); + } +} diff --git a/src/client/views/nodes/DataVizBox/components/Chart.scss b/src/client/views/nodes/DataVizBox/components/Chart.scss index c788a64c2..2f7dd0487 100644 --- a/src/client/views/nodes/DataVizBox/components/Chart.scss +++ b/src/client/views/nodes/DataVizBox/components/Chart.scss @@ -1,10 +1,10 @@ -@import '../../../global/globalCssVariables'; +@import '../../../global/globalCssVariables.module.scss'; .chart-container { display: flex; flex-direction: column; align-items: center; cursor: default; - margin-top: 10px; + margin-top: 30px; overflow-y: visible; .graph { @@ -15,8 +15,8 @@ font-size: larger; display: flex; flex-direction: row; - margin-top: -10px; - margin-bottom: -10px; + margin-top: -20px; + margin-bottom: -20px; } .asHistogram-checkBox { // display: flex; @@ -93,6 +93,7 @@ display: flex; flex-direction: column; cursor: default; + margin-top: 30px; height: calc(100% - 40px); // bcz: hack 40px is the size of the button rows .tableBox-container { overflow: scroll; @@ -111,6 +112,7 @@ white-space: pre; max-width: 150; overflow: hidden; + margin-left: 2px; } } } diff --git a/src/client/views/nodes/DataVizBox/components/Histogram.tsx b/src/client/views/nodes/DataVizBox/components/Histogram.tsx index e67e2bf31..4a1fb2ed1 100644 --- a/src/client/views/nodes/DataVizBox/components/Histogram.tsx +++ b/src/client/views/nodes/DataVizBox/components/Histogram.tsx @@ -1,7 +1,7 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { ColorPicker, EditableText, IconButton, Size, Type } from 'browndash-components'; import * as d3 from 'd3'; -import { action, computed, IReactionDisposer, observable, reaction } from 'mobx'; +import { IReactionDisposer, action, computed, makeObservable, observable, reaction } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; import { FaFillDrip } from 'react-icons/fa'; @@ -10,14 +10,14 @@ import { List } from '../../../../../fields/List'; import { listSpec } from '../../../../../fields/Schema'; import { Cast, DocCast, StrCast } from '../../../../../fields/Types'; import { Docs } from '../../../../documents/Documents'; -import { LinkManager } from '../../../../util/LinkManager'; import { undoable } from '../../../../util/UndoManager'; +import { ObservableReactComponent } from '../../../ObservableReactComponent'; import { PinProps, PresBox } from '../../trails'; import { scaleCreatorNumerical, yAxisCreator } from '../utils/D3Utils'; import './Chart.scss'; export interface HistogramProps { - rootDoc: Doc; + Document: Doc; layoutDoc: Doc; axes: string[]; records: { [key: string]: any }[]; @@ -34,7 +34,7 @@ export interface HistogramProps { } @observer -export class Histogram extends React.Component<HistogramProps> { +export class Histogram extends ObservableReactComponent<HistogramProps> { private _disposers: { [key: string]: IReactionDisposer } = {}; private _histogramRef: React.RefObject<HTMLDivElement> = React.createRef(); private _histogramSvg: d3.Selection<SVGGElement, unknown, null, undefined> | undefined; @@ -46,46 +46,51 @@ export class Histogram extends React.Component<HistogramProps> { private selectedData: any = undefined; // Selection of selected bar private hoverOverData: any = undefined; // Selection of bar being hovered over + constructor(props: any) { + super(props); + makeObservable(this); + } + @computed get _tableDataIds() { - return !this.parentViz ? this.props.records.map((rec, i) => i) : NumListCast(this.parentViz.dataViz_selectedRows); + return !this.parentViz ? this._props.records.map((rec, i) => i) : NumListCast(this.parentViz.dataViz_selectedRows); } // returns all the data records that will be rendered by only returning those records that have been selected by the parent visualization (or all records if there is no parent) @computed get _tableData() { - return !this.parentViz ? this.props.records : this._tableDataIds.map(rowId => this.props.records[rowId]); + return !this.parentViz ? this._props.records : this._tableDataIds.map(rowId => this._props.records[rowId]); } // filters all data to just display selected data if brushed (created from an incoming link) @computed get _histogramData() { - if (this.props.axes.length < 1) return []; - if (this.props.axes.length < 2) { - var ax0 = this.props.axes[0]; - if (/\d/.test(this.props.records[0][ax0])) { + if (this._props.axes.length < 1) return []; + if (this._props.axes.length < 2) { + var ax0 = this._props.axes[0]; + if (/\d/.test(this._props.records[0][ax0])) { this.numericalXData = true; } - return this._tableData.map(record => ({ [ax0]: record[this.props.axes[0]] })); + return this._tableData.map(record => ({ [ax0]: record[this._props.axes[0]] })); } - var ax0 = this.props.axes[0]; - var ax1 = this.props.axes[1]; - if (/\d/.test(this.props.records[0][ax0])) { + var ax0 = this._props.axes[0]; + var ax1 = this._props.axes[1]; + if (/\d/.test(this._props.records[0][ax0])) { this.numericalXData = true; } - if (/\d/.test(this.props.records[0][ax1])) { + if (/\d/.test(this._props.records[0][ax1])) { this.numericalYData = true; } - return this._tableData.map(record => ({ [ax0]: record[this.props.axes[0]], [ax1]: record[this.props.axes[1]] })); + return this._tableData.map(record => ({ [ax0]: record[this._props.axes[0]], [ax1]: record[this._props.axes[1]] })); } @computed get defaultGraphTitle() { - var ax0 = this.props.axes[0]; - var ax1 = this.props.axes.length > 1 ? this.props.axes[1] : undefined; - if (this.props.axes.length < 2 || !ax1 || !/\d/.test(this.props.records[0][ax1]) || !this.numericalYData) { + var ax0 = this._props.axes[0]; + var ax1 = this._props.axes.length > 1 ? this._props.axes[1] : undefined; + 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'; } @computed get parentViz() { - return DocCast(this.props.rootDoc.dataViz_parentViz); - // return LinkManager.Instance.getAllRelatedLinks(this.props.rootDoc) // out of all links - // .filter(link => link.link_anchor_1 == this.props.rootDoc.dataViz_parentViz) // get links where this chart doc is the target of the link + return DocCast(this._props.Document.dataViz_parentViz); + // return LinkManager.Instance.getAllRelatedLinks(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 } @@ -100,13 +105,13 @@ export class Histogram extends React.Component<HistogramProps> { componentWillUnmount() { Array.from(Object.keys(this._disposers)).forEach(key => this._disposers[key]()); } - componentDidMount = () => { + componentDidMount() { this._disposers.chartData = reaction( () => ({ dataSet: this._histogramData, w: this.width, h: this.height }), ({ dataSet, w, h }) => dataSet!.length > 0 && this.drawChart(dataSet, w, h), { fireImmediately: true } ); - }; + } @action restoreView = (data: Doc) => {}; @@ -115,16 +120,16 @@ export class Histogram extends React.Component<HistogramProps> { const anchor = Docs.Create.ConfigDocument({ title: 'histogram doc selection' + this._currSelected, }); - PresBox.pinDocView(anchor, { pinDocLayout: pinProps?.pinDocLayout, pinData: pinProps?.pinData }, this.props.rootDoc); + PresBox.pinDocView(anchor, { pinDocLayout: pinProps?.pinDocLayout, pinData: pinProps?.pinData }, this._props.Document); return anchor; }; @computed get height() { - return this.props.height - this.props.margin.top - this.props.margin.bottom; + return this._props.height - this._props.margin.top - this._props.margin.bottom; } @computed get width() { - return this.props.width - this.props.margin.left - this.props.margin.right; + return this._props.width - this._props.margin.left - this._props.margin.right; } // cleans data by converting numerical data to numbers and taking out empty cells @@ -212,10 +217,10 @@ export class Histogram extends React.Component<HistogramProps> { .select(this._histogramRef.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) + .attr('width', width + this._props.margin.right + this._props.margin.left) + .attr('height', height + this._props.margin.top + this._props.margin.bottom) .append('g') - .attr('transform', 'translate(' + this.props.margin.left + ',' + this.props.margin.top + ')')); + .attr('transform', 'translate(' + this._props.margin.left + ',' + this._props.margin.top + ')')); var x = d3 .scaleLinear() .domain(this.numericalXData ? [startingPoint!, endingPoint!] : [0, numBins]) @@ -383,7 +388,7 @@ export class Histogram extends React.Component<HistogramProps> { ) .attr('fill', d => { var barColor; - const barColors = StrListCast(this.props.layoutDoc.dataViz_histogram_barColors).map(each => each.split('::')); + 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]; else { @@ -391,24 +396,24 @@ export class Histogram extends React.Component<HistogramProps> { if (Number(range[0]) <= d[0] && d[0] <= Number(range[1])) barColor = each[1]; } }); - return barColor ? StrCast(barColor) : StrCast(this.props.layoutDoc.dataViz_histogram_defaultColor); + return barColor ? StrCast(barColor) : StrCast(this._props.layoutDoc.dataViz_histogram_defaultColor); }); }; @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); + 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)); barColors.push(StrCast(barName + '::' + color)); }; @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, '')); + 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 barColors = Cast(this.props.layoutDoc.dataViz_histogram_barColors, listSpec('string'), null); + 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)); }; @@ -417,7 +422,7 @@ export class Histogram extends React.Component<HistogramProps> { if (svg) svg.selectAll('rect').attr('fill', (d: any) => { var barColor; - const barColors = StrListCast(this.props.layoutDoc.dataViz_histogram_barColors).map(each => each.split('::')); + 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]; else { @@ -425,7 +430,7 @@ export class Histogram extends React.Component<HistogramProps> { if (Number(range[0]) <= d[0] && d[0] <= Number(range[1])) barColor = each[1]; } }); - return barColor ? StrCast(barColor) : StrCast(this.props.layoutDoc.dataViz_histogram_defaultColor); + return barColor ? StrCast(barColor) : StrCast(this._props.layoutDoc.dataViz_histogram_defaultColor); }); }; @@ -434,14 +439,14 @@ export class Histogram extends React.Component<HistogramProps> { this._histogramData; var curSelectedBarName = ''; var titleAccessor: any = ''; - if (this.props.axes.length == 2) titleAccessor = 'dataViz_histogram_title' + this.props.axes[0] + '-' + this.props.axes[1]; - else if (this.props.axes.length > 0) titleAccessor = 'dataViz_histogram_title' + 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>(); + if (this._props.axes.length == 2) titleAccessor = 'dataViz_histogram_title' + this._props.axes[0] + '-' + this._props.axes[1]; + else if (this._props.axes.length > 0) titleAccessor = 'dataViz_histogram_title' + 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'; 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 => key // @@ -451,17 +456,17 @@ export class Histogram extends React.Component<HistogramProps> { selected = selected.substring(0, selected.length - 2) + ' }'; } var selectedBarColor; - var barColors = StrListCast(this.props.layoutDoc.histogramBarColors).map(each => each.split('::')); + var barColors = StrListCast(this._props.layoutDoc.histogramBarColors).map(each => each.split('::')); barColors.forEach(each => each[0] === curSelectedBarName && (selectedBarColor = each[1])); if (this._histogramData.length > 0 || !this.parentViz) { - return this.props.axes.length >= 1 ? ( - <div className="chart-container"> + return this._props.axes.length >= 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])} + 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'} @@ -473,9 +478,9 @@ export class Histogram extends React.Component<HistogramProps> { 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')} + 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')} size={Size.XSMALL} /> </div> diff --git a/src/client/views/nodes/DataVizBox/components/LineChart.tsx b/src/client/views/nodes/DataVizBox/components/LineChart.tsx index 3de7a0c4a..2a9a8b354 100644 --- a/src/client/views/nodes/DataVizBox/components/LineChart.tsx +++ b/src/client/views/nodes/DataVizBox/components/LineChart.tsx @@ -1,6 +1,6 @@ -import { EditableText, Size } from 'browndash-components'; +import { Button, EditableText, Size } from 'browndash-components'; import * as d3 from 'd3'; -import { action, computed, IReactionDisposer, observable, reaction } from 'mobx'; +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'; @@ -10,6 +10,7 @@ import { Cast, DocCast, StrCast } from '../../../../../fields/Types'; import { Docs } from '../../../../documents/Documents'; import { DocumentManager } from '../../../../util/DocumentManager'; import { undoable } from '../../../../util/UndoManager'; +import { ObservableReactComponent } from '../../../ObservableReactComponent'; import { PinProps, PresBox } from '../../trails'; import { DataVizBox } from '../DataVizBox'; import { createLineGenerator, drawLine, minMaxRange, scaleCreatorNumerical, xAxisCreator, xGrid, yAxisCreator, yGrid } from '../utils/D3Utils'; @@ -23,7 +24,8 @@ export interface SelectedDataPoint extends DataPoint { elem?: d3.Selection<d3.BaseType, unknown, SVGGElement, unknown>; } export interface LineChartProps { - rootDoc: Doc; + vizBox: DataVizBox; + Document: Doc; layoutDoc: Doc; axes: string[]; records: { [key: string]: any }[]; @@ -40,33 +42,37 @@ export interface LineChartProps { } @observer -export class LineChart extends React.Component<LineChartProps> { +export class LineChart extends ObservableReactComponent<LineChartProps> { private _disposers: { [key: string]: IReactionDisposer } = {}; private _lineChartRef: React.RefObject<HTMLDivElement> = React.createRef(); private _lineChartSvg: d3.Selection<SVGGElement, unknown, null, undefined> | undefined; @observable _currSelected: SelectedDataPoint | undefined = undefined; // TODO: nda - some sort of mapping that keeps track of the annotated points so we can easily remove when annotations list updates + constructor(props: any) { + super(props); + makeObservable(this); + } @computed get _tableDataIds() { - return !this.parentViz ? this.props.records.map((rec, i) => i) : NumListCast(this.parentViz.dataViz_selectedRows); + return !this.parentViz ? this._props.records.map((rec, i) => i) : NumListCast(this.parentViz.dataViz_selectedRows); } // returns all the data records that will be rendered by only returning those records that have been selected by the parent visualization (or all records if there is no parent) @computed get _tableData() { - return !this.parentViz ? this.props.records : this._tableDataIds.map(rowId => this.props.records[rowId]); + 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)); + 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)); } @computed get graphTitle() { - return this.props.axes[1] + ' vs. ' + this.props.axes[0] + ' Line Chart'; + return this._props.axes[1] + ' vs. ' + this._props.axes[0] + ' Line Chart'; } @computed get parentViz() { - return DocCast(this.props.rootDoc.dataViz_parentViz); - // return LinkManager.Instance.getAllRelatedLinks(this.props.rootDoc) // out of all links + return DocCast(this._props.Document.dataViz_parentViz); + // return LinkManager.Instance.getAllRelatedLinks(this._props.Document) // out of all links // .filter(link => { - // return link.link_anchor_1 == this.props.rootDoc.dataViz_parentViz; + // return 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 } @@ -74,7 +80,7 @@ export class LineChart extends React.Component<LineChartProps> { // 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 highlitedRowIds = NumListCast(incomingVizBox?.rootDoc?.dataViz_highlitedRows); + 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 } @computed get rangeVals(): { xMin?: number; xMax?: number; yMin?: number; yMax?: number } { @@ -83,7 +89,7 @@ export class LineChart extends React.Component<LineChartProps> { componentWillUnmount() { Array.from(Object.keys(this._disposers)).forEach(key => this._disposers[key]()); } - componentDidMount = () => { + componentDidMount() { this._disposers.chartData = reaction( () => ({ dataSet: this._lineChartData, w: this.width, h: this.height }), ({ dataSet, w, h }) => { @@ -94,7 +100,7 @@ export class LineChart extends React.Component<LineChartProps> { { fireImmediately: true } ); this._disposers.annos = reaction( - () => DocListCast(this.props.dataDoc[this.props.fieldKey + '_annotations']), + () => DocListCast(this._props.dataDoc[this._props.fieldKey + '_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 @@ -114,11 +120,11 @@ export class LineChart extends React.Component<LineChartProps> { // redraw annotations when the chart data has changed, or the local or inherited selection has changed this.clearAnnotations(); selected && this.drawAnnotations(Number(selected.x), Number(selected.y), true); - incomingHighlited?.forEach((record: any) => this.drawAnnotations(Number(record[this.props.axes[0]]), Number(record[this.props.axes[1]]))); + incomingHighlited?.forEach((record: any) => this.drawAnnotations(Number(record[this._props.axes[0]]), Number(record[this._props.axes[1]]))); }, { fireImmediately: true } ); - }; + } // anything that doesn't need to be recalculated should just be stored as drawCharts (i.e. computed values) and drawChart is gonna iterate over these observables and generate svgs based on that @@ -170,23 +176,23 @@ export class LineChart extends React.Component<LineChartProps> { // title: 'line doc selection' + this._currSelected?.x, }); - PresBox.pinDocView(anchor, { pinDocLayout: pinProps?.pinDocLayout, pinData: pinProps?.pinData }, this.props.rootDoc); + PresBox.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; }; @computed get height() { - return this.props.height - this.props.margin.top - this.props.margin.bottom; + return this._props.height - this._props.margin.top - this._props.margin.bottom; } @computed get width() { - return this.props.width - this.props.margin.left - this.props.margin.right; + return this._props.width - this._props.margin.left - this._props.margin.right; } @computed get defaultGraphTitle() { - var ax0 = this.props.axes[0]; - var 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) { + var ax0 = this._props.axes[0]; + var 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'; } @@ -210,7 +216,7 @@ export class LineChart extends React.Component<LineChartProps> { // TODO: nda - get rid of svg element in the list? 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>) { @@ -245,7 +251,7 @@ export class LineChart extends React.Component<LineChartProps> { const yScale = scaleCreatorNumerical(0, yMax, height, 0); // adding svg - const margin = this.props.margin; + const margin = this._props.margin; const svg = (this._lineChartSvg = d3 .select(this._lineChartRef.current) .append('svg') @@ -317,7 +323,7 @@ export class LineChart extends React.Component<LineChartProps> { svg.append('text') .attr('transform', 'translate(' + width / 2 + ' ,' + (height + 40) + ')') .style('text-anchor', 'middle') - .text(this.props.axes[0]); + .text(this._props.axes[0]); svg.append('text') .attr('transform', 'rotate(-90)' + ' ' + 'translate( 0, ' + -10 + ')') .attr('x', -(height / 2)) @@ -325,7 +331,7 @@ export class LineChart extends React.Component<LineChartProps> { .attr('height', 20) .attr('width', 20) .style('text-anchor', 'middle') - .text(this.props.axes[1]); + .text(this._props.axes[1]); }; private updateTooltip( @@ -346,18 +352,18 @@ export class LineChart extends React.Component<LineChartProps> { render() { var titleAccessor: any = ''; - if (this.props.axes.length == 2) titleAccessor = 'dataViz_lineChart_title' + this.props.axes[0] + '-' + this.props.axes[1]; - else if (this.props.axes.length > 0) titleAccessor = 'dataViz_lineChart_title' + 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'; + if (this._props.axes.length == 2) titleAccessor = 'dataViz_lineChart_title' + this._props.axes[0] + '-' + this._props.axes[1]; + else if (this._props.axes.length > 0) titleAccessor = 'dataViz_lineChart_title' + 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'; 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"> + 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])} + 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'} @@ -366,7 +372,17 @@ export class LineChart extends React.Component<LineChartProps> { /> </div> <div ref={this._lineChartRef} /> - {selectedPt != 'none' ? <div className={'selected-data'}> {`Selected: ${selectedPt}`}</div> : null} + {selectedPt != 'none' ? ( + <div className={'selected-data'}> + {`Selected: ${selectedPt}`} + <Button + onClick={e => { + console.log('test plzz'); + 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> diff --git a/src/client/views/nodes/DataVizBox/components/PieChart.tsx b/src/client/views/nodes/DataVizBox/components/PieChart.tsx index 561f39141..1259a13ff 100644 --- a/src/client/views/nodes/DataVizBox/components/PieChart.tsx +++ b/src/client/views/nodes/DataVizBox/components/PieChart.tsx @@ -1,6 +1,7 @@ +import { Checkbox } from '@mui/material'; import { ColorPicker, EditableText, Size, Type } from 'browndash-components'; import * as d3 from 'd3'; -import { action, computed, IReactionDisposer, observable, reaction } from 'mobx'; +import { IReactionDisposer, action, computed, makeObservable, observable, reaction } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; import { FaFillDrip } from 'react-icons/fa'; @@ -10,12 +11,12 @@ import { listSpec } from '../../../../../fields/Schema'; 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 './Chart.scss'; -import { Checkbox } from '@material-ui/core'; export interface PieChartProps { - rootDoc: Doc; + Document: Doc; layoutDoc: Doc; axes: string[]; records: { [key: string]: any }[]; @@ -32,7 +33,7 @@ export interface PieChartProps { } @observer -export class PieChart extends React.Component<PieChartProps> { +export class PieChart extends ObservableReactComponent<PieChartProps> { private _disposers: { [key: string]: IReactionDisposer } = {}; private _piechartRef: React.RefObject<HTMLDivElement> = React.createRef(); private _piechartSvg: d3.Selection<SVGGElement, unknown, null, undefined> | undefined; @@ -41,61 +42,62 @@ export class PieChart extends React.Component<PieChartProps> { private hoverOverData: any = undefined; // Selection of slice being hovered over @observable _currSelected: any | undefined = undefined; // Object of selected slice + constructor(props: any) { + super(props); + makeObservable(this); + } + @computed get _tableDataIds() { - return !this.parentViz ? this.props.records.map((rec, i) => i) : NumListCast(this.parentViz.dataViz_selectedRows); + return !this.parentViz ? this._props.records.map((rec, i) => i) : NumListCast(this.parentViz.dataViz_selectedRows); } // returns all the data records that will be rendered by only returning those records that have been selected by the parent visualization (or all records if there is no parent) @computed get _tableData() { - return !this.parentViz ? this.props.records : this._tableDataIds.map(rowId => this.props.records[rowId]); + return !this.parentViz ? this._props.records : this._tableDataIds.map(rowId => this._props.records[rowId]); } // organized by specified number percentages/ratios if one column is selected and it contains numbers // otherwise, assume data is organized by categories @computed get byCategory() { - return !/\d/.test(this.props.records[0][this.props.axes[0]]) || this.props.layoutDoc.dataViz_pie_asHistogram; + return !/\d/.test(this._props.records[0][this._props.axes[0]]) || this._props.layoutDoc.dataViz_pie_asHistogram; } // filters all data to just display selected data if brushed (created from an incoming link) @computed get _pieChartData() { - if (this.props.axes.length < 1) return []; + if (this._props.axes.length < 1) return []; - const ax0 = this.props.axes[0]; - if (this.props.axes.length < 2) { - return this._tableData.map(record => ({ [ax0]: record[this.props.axes[0]] })); + const ax0 = this._props.axes[0]; + if (this._props.axes.length < 2) { + return this._tableData.map(record => ({ [ax0]: record[this._props.axes[0]] })); } - const ax1 = this.props.axes[1]; - return this._tableData.map(record => ({ [ax0]: record[this.props.axes[0]], [ax1]: record[this.props.axes[1]] })); + const ax1 = this._props.axes[1]; + return this._tableData.map(record => ({ [ax0]: record[this._props.axes[0]], [ax1]: record[this._props.axes[1]] })); } @computed get defaultGraphTitle() { - var ax0 = this.props.axes[0]; - var 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) { + var ax0 = this._props.axes[0]; + var 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'; } return ax1 + ' by ' + ax0 + ' Pie Chart'; } @computed get parentViz() { - return DocCast(this.props.rootDoc.dataViz_parentViz); - // return LinkManager.Instance.getAllRelatedLinks(this.props.rootDoc) // out of all links - // .filter(link => link.link_anchor_1 == this.props.rootDoc.dataViz_parentViz) // get links where this chart doc is the target of the link + return DocCast(this._props.Document.dataViz_parentViz); + // return LinkManager.Instance.getAllRelatedLinks(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 } componentWillUnmount() { Array.from(Object.keys(this._disposers)).forEach(key => this._disposers[key]()); } - componentDidMount = () => { + componentDidMount() { this._disposers.chartData = reaction( () => ({ dataSet: this._pieChartData, w: this.width, h: this.height }), - ({ dataSet, w, h }) => { - if (dataSet!.length > 0) { - this.drawChart(dataSet, w, h); - } - }, + ({ dataSet, w, h }) => dataSet!.length > 0 && this.drawChart(dataSet, w, h), { fireImmediately: true } ); - }; + } @action restoreView = (data: Doc) => {}; @@ -105,16 +107,16 @@ export class PieChart extends React.Component<PieChartProps> { // title: 'piechart doc selection' + this._currSelected, }); - PresBox.pinDocView(anchor, { pinDocLayout: pinProps?.pinDocLayout, pinData: pinProps?.pinData }, this.props.rootDoc); + PresBox.pinDocView(anchor, { pinDocLayout: pinProps?.pinDocLayout, pinData: pinProps?.pinData }, this._props.Document); return anchor; }; @computed get height() { - return this.props.height - this.props.margin.top - this.props.margin.bottom; + return this._props.height - this._props.margin.top - this._props.margin.bottom; } @computed get width() { - return this.props.width - this.props.margin.left - this.props.margin.right; + return this._props.width - this._props.margin.left - this._props.margin.right; } // cleans data by converting numerical data to numbers and taking out empty cells @@ -185,7 +187,7 @@ export class PieChart extends React.Component<PieChartProps> { 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; + var radius = Math.min(width, height - this._props.margin.top - this._props.margin.bottom) / 2; // converts data into Objects var data = this.data(dataSet); @@ -213,10 +215,10 @@ export class PieChart extends React.Component<PieChartProps> { .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) + .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 + ')'); + 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); @@ -253,7 +255,7 @@ export class PieChart extends React.Component<PieChartProps> { } catch (error) {} possibleDataPointVals.push(dataPointVal); }); - const sliceColors = StrListCast(this.props.layoutDoc.dataViz_pie_sliceColors).map(each => each.split('::')); + const sliceColors = StrListCast(this._props.layoutDoc.dataViz_pie_sliceColors).map(each => each.split('::')); arcs.append('path') .attr('fill', (d, i) => { var dataPoint; @@ -265,7 +267,7 @@ export class PieChart extends React.Component<PieChartProps> { } var sliceColor; if (dataPoint) { - const sliceTitle = dataPoint[this.props.axes[0]]; + 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])); } @@ -281,6 +283,7 @@ export class PieChart extends React.Component<PieChartProps> { return 'slice'; } ) + // @ts-ignore .attr('d', arc) .on('click', onPointClick) .on('mouseover', onHover) @@ -312,10 +315,10 @@ export class PieChart extends React.Component<PieChartProps> { @action changeSelectedColor = (color: string) => { this.curSliceSelected.attr('fill', color); - const sliceTitle = this._currSelected[this.props.axes[0]]; + const sliceTitle = this._currSelected[this._props.axes[0]]; 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); + 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); }); @@ -323,20 +326,20 @@ export class PieChart extends React.Component<PieChartProps> { }; @action changeHistogramCheckBox = () => { - this.props.layoutDoc.dataViz_pie_asHistogram = !this.props.layoutDoc.dataViz_pie_asHistogram; + this._props.layoutDoc.dataViz_pie_asHistogram = !this._props.layoutDoc.dataViz_pie_asHistogram; this.drawChart(this._pieChartData, this.width, this.height); }; render() { var titleAccessor: any = ''; - if (this.props.axes.length == 2) titleAccessor = 'dataViz_pie_title' + this.props.axes[0] + '-' + this.props.axes[1]; - else if (this.props.axes.length > 0) titleAccessor = 'dataViz_pie_title' + 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>(); + if (this._props.axes.length == 2) titleAccessor = 'dataViz_pie_title' + this._props.axes[0] + '-' + this._props.axes[1]; + else if (this._props.axes.length > 0) titleAccessor = 'dataViz_pie_title' + 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 = ''; if (this._currSelected) { - const sliceTitle = this._currSelected[this.props.axes[0]]; + const sliceTitle = this._currSelected[this._props.axes[0]]; curSelectedSliceName = StrCast(sliceTitle) ? StrCast(sliceTitle).replace(/\$/g, '').replace(/\%/g, '').replace(/\#/g, '').replace(/\</g, '') : sliceTitle; selected = '{ '; Object.keys(this._currSelected).map(key => { @@ -346,19 +349,19 @@ export class PieChart extends React.Component<PieChartProps> { selected += ' }'; } else selected = 'none'; var selectedSliceColor; - var sliceColors = StrListCast(this.props.layoutDoc.dataViz_pie_sliceColors).map(each => each.split('::')); + var sliceColors = StrListCast(this._props.layoutDoc.dataViz_pie_sliceColors).map(each => each.split('::')); sliceColors.forEach(each => { if (each[0] == curSelectedSliceName!) selectedSliceColor = each[1]; }); if (this._pieChartData.length > 0 || !this.parentViz) { - return this.props.axes.length >= 1 ? ( - <div className="chart-container"> + return this._props.axes.length >= 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])} + 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'} @@ -366,9 +369,9 @@ export class PieChart extends React.Component<PieChartProps> { 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 }}> - <Checkbox color="primary" onChange={this.changeHistogramCheckBox} checked={this.props.layoutDoc.dataViz_pie_asHistogram as boolean} /> + {this._props.axes.length === 1 && /\d/.test(this._props.records[0][this._props.axes[0]]) ? ( + <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} diff --git a/src/client/views/nodes/DataVizBox/components/TableBox.tsx b/src/client/views/nodes/DataVizBox/components/TableBox.tsx index b88389de6..ed44d9269 100644 --- a/src/client/views/nodes/DataVizBox/components/TableBox.tsx +++ b/src/client/views/nodes/DataVizBox/components/TableBox.tsx @@ -1,20 +1,20 @@ import { Button, Type } from 'browndash-components'; -import { action, computed, IReactionDisposer, observable, reaction } from 'mobx'; +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 { Doc, Field, NumListCast } from '../../../../../fields/Doc'; import { List } from '../../../../../fields/List'; import { listSpec } from '../../../../../fields/Schema'; import { Cast, DocCast } from '../../../../../fields/Types'; -import { emptyFunction, setupMoveUpEvents, Utils } from '../../../../../Utils'; import { DragManager } from '../../../../util/DragManager'; +import { ObservableReactComponent } from '../../../ObservableReactComponent'; import { DocumentView } from '../../DocumentView'; import { DataVizView } from '../DataVizBox'; -import { DATA_VIZ_TABLE_ROW_HEIGHT } from '../../../global/globalCssVariables.scss'; import './Chart.scss'; - +const { default: { DATA_VIZ_TABLE_ROW_HEIGHT } } = require('../../../global/globalCssVariables.module.scss'); // prettier-ignore interface TableBoxProps { - rootDoc: Doc; + Document: Doc; layoutDoc: Doc; records: { [key: string]: any }[]; selectAxes: (axes: string[]) => void; @@ -31,13 +31,17 @@ interface TableBoxProps { } @observer -export class TableBox extends React.Component<TableBoxProps> { +export class TableBox extends ObservableReactComponent<TableBoxProps> { _inputChangedDisposer?: IReactionDisposer; _containerRef: HTMLDivElement | null = null; @observable _scrollTop = -1; @observable _tableHeight = 0; @observable _tableContainerHeight = 0; + constructor(props: any) { + super(props); + makeObservable(this); + } componentDidMount() { // if the tableData changes (ie., when records are selected by the parent (input) visulization), @@ -49,17 +53,17 @@ export class TableBox extends React.Component<TableBoxProps> { this._inputChangedDisposer?.(); } @computed get _tableDataIds() { - return !this.parentViz ? this.props.records.map((rec, i) => i) : NumListCast(this.parentViz.dataViz_selectedRows); + return !this.parentViz ? this._props.records.map((rec, i) => i) : NumListCast(this.parentViz.dataViz_selectedRows); } // returns all the data records that will be rendered by only returning those records that have been selected by the parent visualization (or all records if there is no parent) @computed get _tableData() { - return !this.parentViz ? this.props.records : this._tableDataIds.map(rowId => this.props.records[rowId]); + return !this.parentViz ? this._props.records : this._tableDataIds.map(rowId => this._props.records[rowId]); } @computed get parentViz() { - return DocCast(this.props.rootDoc.dataViz_parentViz); - // return LinkManager.Instance.getAllRelatedLinks(this.props.rootDoc) // out of all links - // .filter(link => link.link_anchor_1 == this.props.rootDoc.dataViz_parentViz) // get links where this chart doc is the target of the link + return DocCast(this._props.Document.dataViz_parentViz); + // return LinkManager.Instance.getAllRelatedLinks(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 } @@ -69,33 +73,35 @@ export class TableBox extends React.Component<TableBoxProps> { // updates the 'dataViz_selectedRows' and 'dataViz_highlightedRows' fields to no longer include rows that aren't in the table filterSelectedRowsDown = () => { - const selected = NumListCast(this.props.layoutDoc.dataViz_selectedRows); - this.props.layoutDoc.dataViz_selectedRows = new List<number>(selected.filter(rowId => this._tableDataIds.includes(rowId))); // filters through selected to remove guids that were removed in the incoming data - const highlighted = NumListCast(this.props.layoutDoc.dataViz_highlitedRows); - this.props.layoutDoc.dataViz_highlitedRows = new List<number>(highlighted.filter(rowId => this._tableDataIds.includes(rowId))); // filters through highlighted to remove guids that were removed in the incoming data + const selected = NumListCast(this._props.layoutDoc.dataViz_selectedRows); + this._props.layoutDoc.dataViz_selectedRows = new List<number>(selected.filter(rowId => this._tableDataIds.includes(rowId))); // filters through selected to remove guids that were removed in the incoming data + const highlighted = NumListCast(this._props.layoutDoc.dataViz_highlitedRows); + this._props.layoutDoc.dataViz_highlitedRows = new List<number>(highlighted.filter(rowId => this._tableDataIds.includes(rowId))); // filters through highlighted to remove guids that were removed in the incoming data }; @computed get viewScale() { - return this.props.docView?.()?.props.ScreenToLocalTransform().Scale || 1; + return this._props.docView?.()?.screenToViewTransform().Scale || 1; } @computed get rowHeight() { + console.log('scale = ' + this.viewScale + ' table = ' + this._tableHeight + ' ids = ' + this._tableDataIds.length); return (this.viewScale * this._tableHeight) / this._tableDataIds.length; } @computed get startID() { - return this.rowHeight ? Math.floor(this._scrollTop / this.rowHeight) : 0; + return this.rowHeight ? Math.max(Math.floor(this._scrollTop / this.rowHeight) - 1, 0) : 0; } @computed get endID() { + console.log('start = ' + this.startID + ' container = ' + this._tableContainerHeight + ' scale = ' + this.viewScale + ' row = ' + this.rowHeight); return Math.ceil(this.startID + (this._tableContainerHeight * this.viewScale) / (this.rowHeight || 1)); } @action handleScroll = () => { - if (!this.props.docView?.()?.ContentDiv?.hidden) { + if (!this._props.docView?.()?.ContentDiv?.hidden) { this._scrollTop = this._containerRef?.scrollTop ?? 0; } }; @action tableRowClick = (e: React.MouseEvent, rowId: number) => { - const highlited = Cast(this.props.layoutDoc.dataViz_highlitedRows, listSpec('number'), null); - const selected = Cast(this.props.layoutDoc.dataViz_selectedRows, listSpec('number'), null); + const highlited = Cast(this._props.layoutDoc.dataViz_highlitedRows, listSpec('number'), null); + const selected = Cast(this._props.layoutDoc.dataViz_selectedRows, listSpec('number'), null); if (e.metaKey) { // highlighting a row if (highlited?.includes(rowId)) highlited.splice(highlited.indexOf(rowId), 1); @@ -119,26 +125,26 @@ export class TableBox extends React.Component<TableBoxProps> { e, e => { // dragging off a column to create a brushed DataVizBox - const sourceAnchorCreator = () => this.props.docView?.()!.rootDoc!; + const sourceAnchorCreator = () => this._props.docView?.()!.Document!; const targetCreator = (annotationOn: Doc | undefined) => { - const embedding = Doc.MakeEmbedding(this.props.docView?.()!.rootDoc!); + const embedding = Doc.MakeEmbedding(this._props.docView?.()!.Document!); embedding._dataViz = DataVizView.TABLE; embedding._dataViz_axes = new List<string>([col, col]); - embedding._dataViz_parentViz = this.props.rootDoc; + embedding._dataViz_parentViz = this._props.Document; embedding.annotationOn = annotationOn; - embedding.histogramBarColors = Field.Copy(this.props.layoutDoc.histogramBarColors); - embedding.defaultHistogramColor = this.props.layoutDoc.defaultHistogramColor; - embedding.pieSliceColors = Field.Copy(this.props.layoutDoc.pieSliceColors); + embedding.histogramBarColors = Field.Copy(this._props.layoutDoc.histogramBarColors); + embedding.defaultHistogramColor = this._props.layoutDoc.defaultHistogramColor; + 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, { + 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.rootDoc; + // e.annoDragData.linkSourceDoc.followLinkToggle = e.annoDragData.dropDocument.annotationOn === this._props.Document; // e.annoDragData.linkSourceDoc.followLinkZoom = false; } }, @@ -149,11 +155,11 @@ export class TableBox extends React.Component<TableBoxProps> { }, emptyFunction, action(e => { - const newAxes = this.props.axes; + const newAxes = this._props.axes; if (newAxes.includes(col)) newAxes.splice(newAxes.indexOf(col), 1); else if (newAxes.length > 1) newAxes[1] = col; else newAxes.push(col); - this.props.selectAxes(newAxes); + this._props.selectAxes(newAxes); }) ); }; @@ -163,16 +169,17 @@ export class TableBox extends React.Component<TableBoxProps> { return ( <div className="tableBox" + style={{ width: this._props.width + this._props.margin.right }} tabIndex={0} onKeyDown={e => { - if (this.props.layoutDoc && e.key === 'a' && (e.ctrlKey || e.metaKey)) { + if (this._props.layoutDoc && e.key === 'a' && (e.ctrlKey || e.metaKey)) { e.stopPropagation(); - this.props.layoutDoc.dataViz_selectedRows = new List<number>(this._tableDataIds); + this._props.layoutDoc.dataViz_selectedRows = new List<number>(this._tableDataIds); } }}> <div className="selectAll-buttons"> - <Button onClick={action(() => (this.props.layoutDoc.dataViz_selectedRows = new List<number>(this._tableDataIds)))} text="Select All" type={Type.SEC} color={'black'} /> - <Button onClick={action(() => (this.props.layoutDoc.dataViz_selectedRows = new List<number>()))} text="Deselect All" type={Type.SEC} color={'black'} /> + <Button onClick={action(() => (this._props.layoutDoc.dataViz_selectedRows = new List<number>(this._tableDataIds)))} text="Select All" type={Type.SEC} color={'black'} /> + <Button onClick={action(() => (this._props.layoutDoc.dataViz_selectedRows = new List<number>()))} text="Deselect All" type={Type.SEC} color={'black'} /> </div> <div className={`tableBox-container ${this.columns[0]}`} @@ -180,7 +187,7 @@ export class TableBox extends React.Component<TableBoxProps> { onScroll={this.handleScroll} ref={action((r: HTMLDivElement | null) => { this._containerRef = r; - if (!this.props.docView?.()?.ContentDiv?.hidden && r) { + if (!this._props.docView?.()?.ContentDiv?.hidden && r) { this._tableContainerHeight = r.getBoundingClientRect().height ?? 0; r.addEventListener( 'wheel', // 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) @@ -195,7 +202,7 @@ export class TableBox extends React.Component<TableBoxProps> { <table className="tableBox-table" ref={action((r: HTMLTableElement | null) => { - if (!this.props.docView?.()?.ContentDiv?.hidden && r) { + if (!this._props.docView?.()?.ContentDiv?.hidden && r) { this._tableHeight = r?.getBoundingClientRect().height ?? 0; } })}> @@ -206,8 +213,8 @@ export class TableBox extends React.Component<TableBoxProps> { <th key={this.columns.indexOf(col)} style={{ - color: this.props.axes.slice().reverse().lastElement() === col ? 'darkgreen' : this.props.axes.lastElement() === col ? 'darkred' : undefined, - background: this.props.axes.slice().reverse().lastElement() === col ? '#E3fbdb' : this.props.axes.lastElement() === col ? '#Fbdbdb' : undefined, + color: this._props.axes.slice().reverse().lastElement() === col ? 'darkgreen' : this._props.axes.lastElement() === col ? 'darkred' : undefined, + background: this._props.axes.slice().reverse().lastElement() === col ? '#E3fbdb' : this._props.axes.lastElement() === col ? '#Fbdbdb' : undefined, fontWeight: 'bolder', border: '3px solid black', }} @@ -219,20 +226,20 @@ export class TableBox extends React.Component<TableBoxProps> { </thead> <tbody> {this._tableDataIds - .filter(rowId => this.startID <= rowId && rowId <= this.endID) + .filter((rowId, i) => this.startID - 2 <= i && i <= this.endID + 2) ?.map(rowId => ( <tr key={rowId} className={`tableBox-row ${this.columns[0]}`} onClick={e => this.tableRowClick(e, rowId)} style={{ - background: NumListCast(this.props.layoutDoc.dataViz_highlitedRows).includes(rowId) ? 'lightYellow' : NumListCast(this.props.layoutDoc.dataViz_selectedRows).includes(rowId) ? 'lightgrey' : '', + background: NumListCast(this._props.layoutDoc.dataViz_highlitedRows).includes(rowId) ? 'lightYellow' : NumListCast(this._props.layoutDoc.dataViz_selectedRows).includes(rowId) ? 'lightgrey' : '', }}> {this.columns.map(col => { - const colSelected = this.props.axes.length > 1 ? this.props.axes[0] == col || this.props.axes[1] == col : this.props.axes.length > 0 ? this.props.axes[0] == col : false; + const colSelected = this._props.axes.length > 1 ? this._props.axes[0] == col || this._props.axes[1] == col : this._props.axes.length > 0 ? this._props.axes[0] == col : false; 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> + <div className="tableBox-cell">{this._props.records[rowId][col]}</div> </td> ); })} diff --git a/src/client/views/nodes/DocumentContentsView.tsx b/src/client/views/nodes/DocumentContentsView.tsx index e1de2fa76..07e179246 100644 --- a/src/client/views/nodes/DocumentContentsView.tsx +++ b/src/client/views/nodes/DocumentContentsView.tsx @@ -1,54 +1,52 @@ -import { computed } from 'mobx'; +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 { Doc, Opt } from '../../../fields/Doc'; -import { AclPrivate } from '../../../fields/DocSymbols'; +import { AclPrivate, DocData } from '../../../fields/DocSymbols'; import { ScriptField } from '../../../fields/ScriptField'; import { Cast, StrCast } from '../../../fields/Types'; import { GetEffectiveAcl, TraceMobx } from '../../../fields/util'; -import { emptyPath, OmitKeys, Without } from '../../../Utils'; -import { DirectoryImportBox } from '../../util/Import & Export/DirectoryImportBox'; +import { InkingStroke } from '../InkingStroke'; +import { ObservableReactComponent } from '../ObservableReactComponent'; +import { CollectionCalendarView } from '../collections/CollectionCalendarView'; import { CollectionDockingView } from '../collections/CollectionDockingView'; +import { CollectionView } from '../collections/CollectionView'; import { CollectionFreeFormView } from '../collections/collectionFreeForm/CollectionFreeFormView'; import { CollectionSchemaView } from '../collections/collectionSchema/CollectionSchemaView'; import { SchemaRowBox } from '../collections/collectionSchema/SchemaRowBox'; -import { CollectionView } from '../collections/CollectionView'; -import { InkingStroke } from '../InkingStroke'; 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 { ColorBox } from './ColorBox'; import { ComparisonBox } from './ComparisonBox'; import { DataVizBox } from './DataVizBox/DataVizBox'; -import { DocumentViewProps } from './DocumentView'; import './DocumentView.scss'; import { EquationBox } from './EquationBox'; import { FieldView, FieldViewProps } from './FieldView'; import { FontIconBox } from './FontIconBox/FontIconBox'; -import { FormattedTextBox } from './formattedText/FormattedTextBox'; import { FunctionPlotBox } from './FunctionPlotBox'; import { ImageBox } from './ImageBox'; -import { ImportElementBox } from './importBox/ImportElementBox'; 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'; +import { MapPushpinBox } from './MapBox/MapPushpinBox'; import { PDFBox } from './PDFBox'; import { PhysicsSimulationBox } from './PhysicsBox/PhysicsSimulationBox'; import { RecordingBox } from './RecordingBox'; import { ScreenshotBox } from './ScreenshotBox'; import { ScriptingBox } from './ScriptingBox'; -import { PresBox } from './trails/PresBox'; import { VideoBox } from './VideoBox'; import { WebBox } from './WebBox'; -import React = require('react'); -import XRegExp = require('xregexp'); -import { MapPushpinBox } from './MapBox/MapPushpinBox'; - -const JsxParser = require('react-jsx-parser').default; //TODO Why does this need to be imported like this? +import { FormattedTextBox } from './formattedText/FormattedTextBox'; +import { ImportElementBox } from './importBox/ImportElementBox'; +import { PresBox } from './trails/PresBox'; type BindingProps = Without<FieldViewProps, 'fieldKey'>; export interface JsxBindings { @@ -66,7 +64,6 @@ export const ObserverJsxParser: typeof JsxParser = ObserverJsxParser1 as any; interface HTMLtagProps { Document: Doc; - RootDoc: Doc; htmltag: string; onClick?: ScriptField; onInput?: ScriptField; @@ -90,18 +87,18 @@ interface HTMLtagProps { export class HTMLtag extends React.Component<HTMLtagProps> { click = (e: React.MouseEvent) => { const clickScript = (this.props as any).onClick as Opt<ScriptField>; - clickScript?.script.run({ this: this.props.Document, self: this.props.RootDoc, scale: this.props.scaling }); + clickScript?.script.run({ this: this.props.Document, self: 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.RootDoc, value: (e.target as any).textContent }); + onInputScript?.script.run({ this: this.props.Document, self: this.props.Document, value: (e.target as any).textContent }); }; render() { const style: { [key: string]: any } = {}; - const divKeys = OmitKeys(this.props, ['children', 'htmltag', 'RootDoc', 'scaling', 'Document', 'key', 'onInput', 'onClick', '__proto__']).omit; + 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) => { - // bcz: this executes a script to convert a propery expression string: { script } into a value - return (ScriptField.MakeFunction(expr, { self: Doc.name, this: Doc.name, scale: 'number' })?.script.run({ self: this.props.RootDoc, this: this.props.Document, scale: this.props.scaling }).result as 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) => { const p = (this.props as any)[prop] as string; @@ -116,71 +113,72 @@ export class HTMLtag extends React.Component<HTMLtagProps> { } } +export interface DocumentContentsViewProps extends FieldViewProps { + layoutFieldKey: string; +} @observer -export class DocumentContentsView extends React.Component< - DocumentViewProps & { - isSelected: (outsideReaction: boolean) => boolean; - select: (ctrl: boolean) => void; - NativeDimScaling?: () => number; - setHeight?: (height: number) => void; - layout_fieldKey: string; +export class DocumentContentsView extends ObservableReactComponent<DocumentContentsViewProps> { + constructor(props: any) { + super(props); + makeObservable(this); } -> { + @computed get layout(): string { TraceMobx(); - if (this.props.LayoutTemplateString) return this.props.LayoutTemplateString; + if (this._props.LayoutTemplateString) return this._props.LayoutTemplateString; if (!this.layoutDoc) return '<p>awaiting layout</p>'; - if (this.props.layout_fieldKey === 'layout_keyValue') return StrCast(this.props.Document.layout_keyValue, KeyValueBox.LayoutString()); - const layout = Cast(this.layoutDoc[this.layoutDoc === this.props.Document && this.props.layout_fieldKey ? this.props.layout_fieldKey : StrCast(this.layoutDoc.layout_fieldKey, 'layout')], 'string'); - if (layout === undefined) return this.props.Document.data ? "<FieldView {...props} fieldKey='data' />" : KeyValueBox.LayoutString(); + if (this._props.layoutFieldKey === 'layout_keyValue') return StrCast(this._props.Document.layout_keyValue, KeyValueBox.LayoutString()); + const layout = Cast(this.layoutDoc[this.layoutDoc === this._props.Document && this._props.layoutFieldKey ? this._props.layoutFieldKey : StrCast(this.layoutDoc.layout_fieldKey, 'layout')], 'string'); + if (layout === undefined) return this._props.Document.data ? "<FieldView {...props} fieldKey='data' />" : KeyValueBox.LayoutString(); if (typeof layout === 'string') return layout; return '<p>Loading layout</p>'; } - get dataDoc() { - const proto = this.props.DataDoc || Doc.GetProto(this.props.Document); - return proto instanceof Promise ? undefined : proto; - } get layoutDoc() { // bcz: replaced this with below : is it correct? change was made to accommodate passing fieldKey's from a layout script - // const template: Doc = this.props.LayoutTemplate?.() || Doc.Layout(this.props.Document, this.props.layout_fieldKey ? Cast(this.props.Document[this.props.layout_fieldKey], Doc, null) : undefined); + // const template: Doc = this._props.LayoutTemplate?.() || Doc.Layout(this._props.Document, this._props.fieldKey ? Cast(this._props.Document[this._props.fieldKey], Doc, null) : undefined); const template: Doc = - this.props.LayoutTemplate?.() || - (this.props.LayoutTemplateString && this.props.Document) || - (this.props.layout_fieldKey && StrCast(this.props.Document[this.props.layout_fieldKey]) && this.props.Document) || - Doc.Layout(this.props.Document, this.props.layout_fieldKey ? Cast(this.props.Document[this.props.layout_fieldKey], Doc, null) : undefined); - return Doc.expandTemplateLayout(template, this.props.Document); + this._props.LayoutTemplate?.() || + (this._props.LayoutTemplateString && this._props.Document) || + (this._props.layoutFieldKey && StrCast(this._props.Document[this._props.layoutFieldKey]) && this._props.Document) || + Doc.Layout(this._props.Document, this._props.layoutFieldKey ? Cast(this._props.Document[this._props.layoutFieldKey], Doc, null) : undefined); + return Doc.expandTemplateLayout(template, this._props.Document); } CreateBindings(onClick: Opt<ScriptField>, onInput: Opt<ScriptField>): JsxBindings { const docOnlyProps = [ - // these are the properties in DocumentViewProps that need to be removed to pass on only DocumentSharedViewProps to the FieldViews + // these are the properties in DocumentViewProps that need to be removed to pass on only DocumentSharedViewProps to the FieldViews 'hideResizeHandles', 'hideTitle', - 'contentPointerEvents', - 'radialMenu', + 'bringToFront', + 'childContentPointerEvents', 'LayoutTemplateString', 'LayoutTemplate', + 'layoutFieldKey', 'dontCenter', 'contextMenuItems', - 'onClick', - 'onDoubleClick', - 'onPointerDown', - 'onPointerUp', + //'onClick', // don't need to omit this since it will be set + 'onDoubleClickScript', + 'onPointerDownScript', + 'onPointerUpScript', ]; - const list = { - ...OmitKeys(this.props, [...docOnlyProps], '').omit, - RootDoc: Cast(this.layoutDoc?.rootDocument, Doc, null) || this.layoutDoc, - Document: this.layoutDoc, - DataDoc: this.dataDoc, - onClick: onClick, - onInput: onInput, + const templateDataDoc = this._props.TemplateDataDocument ?? (this.layoutDoc !== this._props.Document ? this._props.Document[DocData] : undefined); + const list: BindingProps & React.DetailedHTMLProps<React.HtmlHTMLAttributes<HTMLDivElement>, HTMLDivElement> = { + ...this._props, + Document: this.layoutDoc ?? this._props.Document, + TemplateDataDocument: templateDataDoc instanceof Promise ? undefined : templateDataDoc, + onClick: onClick as any as React.MouseEventHandler, // pass onClick script as if it were a real function -- it will be interpreted properly in the HTMLtag + onInput: onInput as any as React.FormEventHandler, + }; + return { + props: { + ...OmitKeys(list, [...docOnlyProps], '').omit, + }, }; - return { props: list }; } // componentWillUpdate(oldProps: any, newState: any) { - // // console.log("willupdate", oldProps, this.props); // bcz: if you get a message saying something invalidated because reactive props changed, then this method allows you to figure out which prop changed + // // console.log("willupdate", oldProps, this._props); // bcz: if you get a message saying something invalidated because reactive props changed, then this method allows you to figure out which prop changed // } @computed get renderData() { @@ -189,13 +187,13 @@ export class DocumentContentsView extends React.Component< // replace code content with a script >{content}< as in <HTMLdiv>{this.title}</HTMLdiv> const replacer = (match: any, prefix: string, expr: string, postfix: string, offset: any, string: any) => { - return prefix + ((ScriptField.MakeFunction(expr, { self: Doc.name, this: Doc.name })?.script.run({ this: this.props.Document }).result as string) || '') + postfix; + return prefix + ((ScriptField.MakeFunction(expr, { self: Doc.name, this: Doc.name })?.script.run({ this: this._props.Document }).result as string) || '') + postfix; }; layoutFrame = layoutFrame.replace(/(>[^{]*)[^=]\{([^.'][^<}]+)\}([^}]*<)/g, replacer); // replace HTML<tag> with corresponding HTML tag as in: <HTMLdiv> becomes <HTMLtag Document={props.Document} htmltag='div'> const replacer2 = (match: any, p1: string, offset: any, string: any) => { - return `<HTMLtag RootDoc={props.RootDoc} Document={props.Document} scaling='${this.props.NativeDimScaling?.() || 1}' htmltag='${p1}'`; + return `<HTMLtag Document={props.Document} scaling='${this._props.NativeDimScaling?.() || 1}' htmltag='${p1}'`; }; layoutFrame = layoutFrame.replace(/<HTML([a-zA-Z0-9_-]+)/g, replacer2); @@ -227,7 +225,7 @@ export class DocumentContentsView extends React.Component< TraceMobx(); const { bindings, layoutFrame } = this.renderData; - return this.props.renderDepth > 12 || !layoutFrame || !this.layoutDoc || GetEffectiveAcl(this.layoutDoc) === AclPrivate ? null : ( + return this._props.renderDepth > 12 || !layoutFrame || !this.layoutDoc || GetEffectiveAcl(this.layoutDoc) === AclPrivate ? null : ( <ObserverJsxParser key={42} blacklistedAttrs={emptyPath} @@ -235,7 +233,6 @@ export class DocumentContentsView extends React.Component< components={{ FormattedTextBox, ImageBox, - DirectoryImportBox, FontIconBox, LabelBox, EquationBox, @@ -243,6 +240,7 @@ export class DocumentContentsView extends React.Component< CollectionFreeFormView, CollectionDockingView, CollectionSchemaView, + CollectionCalendarView, CollectionView, WebBox, KeyValueBox, @@ -255,7 +253,6 @@ export class DocumentContentsView extends React.Component< PresElementBox, SearchBox, FunctionPlotBox, - ColorBox, DashWebRTCVideo, LinkAnchorBox, InkingStroke, diff --git a/src/client/views/nodes/DocumentIcon.tsx b/src/client/views/nodes/DocumentIcon.tsx index bccbd66e8..4a22766cc 100644 --- a/src/client/views/nodes/DocumentIcon.tsx +++ b/src/client/views/nodes/DocumentIcon.tsx @@ -1,25 +1,35 @@ +import { Tooltip } from '@mui/material'; +import { action, makeObservable, observable } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; -import { DocumentView } from './DocumentView'; -import { DocumentManager } from '../../util/DocumentManager'; -import { Transformer, ts } from '../../util/Scripting'; +import { factory } from 'typescript'; import { Field } from '../../../fields/Doc'; -import { Tooltip } from '@material-ui/core'; -import { action, observable } from 'mobx'; import { Id } from '../../../fields/FieldSymbols'; -import { factory } from 'typescript'; -import { LightboxView } from '../LightboxView'; +import { DocumentManager } from '../../util/DocumentManager'; +import { Transformer, ts } from '../../util/Scripting'; import { SettingsManager } from '../../util/SettingsManager'; +import { LightboxView } from '../LightboxView'; +import { ObservableReactComponent } from '../ObservableReactComponent'; +import { DocumentView } from './DocumentView'; +interface DocumentIconProps { + view: DocumentView; + index: number; +} @observer -export class DocumentIcon extends React.Component<{ view: DocumentView; index: number }> { +export class DocumentIcon extends ObservableReactComponent<DocumentIconProps> { @observable _hovered = false; + constructor(props: any) { + super(props); + makeObservable(this); + } + static get DocViews() { - return LightboxView.LightboxDoc ? DocumentManager.Instance.DocumentViews.filter(v => LightboxView.IsLightboxDocView(v.props.docViewPath())) : DocumentManager.Instance.DocumentViews; + 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.view; + const { left, top, right, bottom } = view.getBounds || { left: 0, top: 0, right: 0, bottom: 0 }; return ( <div @@ -33,8 +43,8 @@ export class DocumentIcon extends React.Component<{ view: DocumentView; index: n background: SettingsManager.userBackgroundColor, transform: `translate(${(left + right) / 2}px, ${top}px)`, }}> - <Tooltip title={<>{this.props.view.rootDoc.title}</>}> - <p>d{this.props.index}</p> + <Tooltip title={<>{this._props.view.Document.title}</>}> + <p>d{this._props.index}</p> </Tooltip> </div> ); @@ -59,7 +69,7 @@ export class DocumentIconContainer extends React.Component { const match = node.text.match(/d([0-9]+)/); if (match) { const m = parseInt(match[1]); - const doc = DocumentIcon.DocViews[m].rootDoc; + const doc = DocumentIcon.DocViews[m].Document; usedDocuments.add(m); return factory.createIdentifier(`idToDoc("${doc[Id]}")`); } @@ -74,7 +84,7 @@ export class DocumentIconContainer extends React.Component { getVars() { const docs = DocumentIcon.DocViews; const capturedVariables: { [name: string]: Field } = {}; - usedDocuments.forEach(index => (capturedVariables[`d${index}`] = docs.length > index ? docs[index].props.Document : `d${index}`)); + usedDocuments.forEach(index => (capturedVariables[`d${index}`] = docs.length > index ? docs[index].Document : `d${index}`)); return capturedVariables; }, }; diff --git a/src/client/views/nodes/DocumentLinksButton.scss b/src/client/views/nodes/DocumentLinksButton.scss index 6da0b73ba..b32b27e65 100644 --- a/src/client/views/nodes/DocumentLinksButton.scss +++ b/src/client/views/nodes/DocumentLinksButton.scss @@ -1,4 +1,4 @@ -@import '../global/globalCssVariables.scss'; +@import '../global/globalCssVariables.module.scss'; .documentLinksButton-wrapper { transform-origin: top left; diff --git a/src/client/views/nodes/DocumentLinksButton.tsx b/src/client/views/nodes/DocumentLinksButton.tsx index 4db0bf5fa..d1805308d 100644 --- a/src/client/views/nodes/DocumentLinksButton.tsx +++ b/src/client/views/nodes/DocumentLinksButton.tsx @@ -1,22 +1,23 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { Tooltip } from '@material-ui/core'; -import { action, computed, observable, runInAction } from 'mobx'; +import { Tooltip } from '@mui/material'; +import { action, computed, makeObservable, observable, runInAction } from 'mobx'; import { observer } from 'mobx-react'; -import { Doc, Opt } from '../../../fields/Doc'; +import * as React from 'react'; +import { StopEvent, emptyFunction, returnFalse, setupMoveUpEvents } from '../../../Utils'; +import { Doc } from '../../../fields/Doc'; import { StrCast } from '../../../fields/Types'; -import { emptyFunction, returnFalse, setupMoveUpEvents, StopEvent } from '../../../Utils'; import { DocUtils } from '../../documents/Documents'; import { DragManager } from '../../util/DragManager'; import { Hypothesis } from '../../util/HypothesisUtils'; import { LinkManager } from '../../util/LinkManager'; -import { undoable, undoBatch, UndoManager } from '../../util/UndoManager'; +import { UndoManager, undoBatch } from '../../util/UndoManager'; +import { ObservableReactComponent } from '../ObservableReactComponent'; import './DocumentLinksButton.scss'; import { DocumentView } from './DocumentView'; import { LinkDescriptionPopup } from './LinkDescriptionPopup'; import { TaskCompletionBox } from './TaskCompletedBox'; -import React = require('react'); -import _ = require('lodash'); import { PinProps } from './trails'; +import { DocData } from '../../../fields/DocSymbols'; interface DocumentLinksButtonProps { View: DocumentView; @@ -27,26 +28,45 @@ interface DocumentLinksButtonProps { 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 StartLinkView: DocumentView | undefined = undefined; + @observable public AnnotationId: string | undefined = undefined; + @observable public AnnotationUri: string | undefined = undefined; + @observable public LinkEditorDocView: DocumentView | undefined = undefined; + public static _instance: DocButtonState | undefined; + public static get Instance() { + return DocButtonState._instance ?? (DocButtonState._instance = new DocButtonState()); + } + constructor() { + makeObservable(this); + } } @observer -export class DocumentLinksButton extends React.Component<DocumentLinksButtonProps, {}> { +export class DocumentLinksButton extends ObservableReactComponent<DocumentLinksButtonProps> { private _linkButton = React.createRef<HTMLDivElement>(); - @observable public static StartLink: Doc | undefined; //origin's Doc, if defined - @observable public static StartLinkView: DocumentView | undefined; - @observable public static AnnotationId: string | undefined; - @observable public static AnnotationUri: string | undefined; - @observable public static LinkEditorDocView: DocumentView | undefined; + public static get StartLink() { return DocButtonState.Instance.StartLink; } // prettier-ignore + public static set StartLink(value) { runInAction(() => (DocButtonState.Instance.StartLink = value)); } // prettier-ignore + @observable public static StartLinkView: DocumentView | undefined = undefined; + @observable public static AnnotationId: string | undefined = undefined; + @observable public static AnnotationUri: string | undefined = undefined; + constructor(props: any) { + super(props); + makeObservable(this); + } - @action @undoBatch onLinkButtonMoved = (e: PointerEvent) => { - if (this.props.InMenu && this.props.StartLink) { + if (this._props.InMenu && this._props.StartLink) { if (this._linkButton.current !== null) { const linkDrag = UndoManager.StartBatch('Drag Link'); - this.props.View && - DragManager.StartLinkDrag(this._linkButton.current, this.props.View, this.props.View.ComponentView?.getAnchor, e.pageX, e.pageY, { + this._props.View && + DragManager.StartLinkDrag(this._linkButton.current, this._props.View, this._props.View.ComponentView?.getAnchor, e.pageX, e.pageY, { dragComplete: dropEv => { - if (this.props.View && dropEv.linkDocument) { + if (this._props.View && dropEv.linkDocument) { // dropEv.linkDocument equivalent to !dropEve.aborted since linkDocument is only assigned on a completed drop !dropEv.linkDocument.link_relationship && (Doc.GetProto(dropEv.linkDocument).link_relationship = 'hyperlink'); } @@ -68,11 +88,11 @@ export class DocumentLinksButton extends React.Component<DocumentLinksButtonProp this.onLinkButtonMoved, emptyFunction, action((e, doubleTap) => { - doubleTap && DocumentView.showBackLinks(this.props.View.rootDoc); + doubleTap && DocumentView.showBackLinks(this._props.View.Document); }), undefined, undefined, - action(() => (DocumentLinksButton.LinkEditorDocView = this.props.View)) + action(() => (DocButtonState.Instance.LinkEditorDocView = this._props.View)) ); }; @@ -83,33 +103,32 @@ export class DocumentLinksButton extends React.Component<DocumentLinksButtonProp this.onLinkButtonMoved, emptyFunction, action((e, doubleTap) => { - if (doubleTap && this.props.InMenu && this.props.StartLink) { - //action(() => Doc.BrushDoc(this.props.View.Document)); - if (DocumentLinksButton.StartLink === this.props.View.props.Document) { + if (doubleTap && this._props.InMenu && this._props.StartLink) { + //action(() => Doc.BrushDoc(this._props.View.Document)); + if (DocumentLinksButton.StartLink === this._props.View.Document) { DocumentLinksButton.StartLink = undefined; DocumentLinksButton.StartLinkView = undefined; } else { - DocumentLinksButton.StartLink = this.props.View.props.Document; - DocumentLinksButton.StartLinkView = this.props.View; + DocumentLinksButton.StartLink = this._props.View.Document; + DocumentLinksButton.StartLinkView = this._props.View; } } }) ); }; - @action @undoBatch onLinkClick = (e: React.MouseEvent): void => { - if (this.props.InMenu && this.props.StartLink) { + if (this._props.InMenu && this._props.StartLink) { DocumentLinksButton.AnnotationId = undefined; DocumentLinksButton.AnnotationUri = undefined; - if (DocumentLinksButton.StartLink === this.props.View.props.Document) { + if (DocumentLinksButton.StartLink === this._props.View.Document) { DocumentLinksButton.StartLink = undefined; DocumentLinksButton.StartLinkView = undefined; } else { //if this LinkButton's Document is undefined - DocumentLinksButton.StartLink = this.props.View.props.Document; - DocumentLinksButton.StartLinkView = this.props.View; + DocumentLinksButton.StartLink = this._props.View.Document; + DocumentLinksButton.StartLinkView = this._props.View; } } }; @@ -120,7 +139,7 @@ export class DocumentLinksButton extends React.Component<DocumentLinksButtonProp e, returnFalse, emptyFunction, - action(e => DocumentLinksButton.finishLinkClick(e.clientX, e.clientY, DocumentLinksButton.StartLink, this.props.View.props.Document, true, this.props.View)) + action(e => DocumentLinksButton.finishLinkClick(e.clientX, e.clientY, DocumentLinksButton.StartLink, this._props.View.Document, true, this._props.View)) ); }; @@ -132,49 +151,48 @@ export class DocumentLinksButton extends React.Component<DocumentLinksButtonProp DocumentLinksButton.StartLinkView = undefined; DocumentLinksButton.AnnotationId = undefined; DocumentLinksButton.AnnotationUri = undefined; - //!this.props.StartLink + //!this._props.StartLink } else if (startLink !== endLink) { - endLink = endLinkView?.docView?._componentView?.getAnchor?.(true, pinProps) || endLink; - startLink = DocumentLinksButton.StartLinkView?.docView?._componentView?.getAnchor?.(true) || startLink; + endLink = endLinkView?.ComponentView?.getAnchor?.(true, pinProps) || endLink; + startLink = DocumentLinksButton.StartLinkView?.ComponentView?.getAnchor?.(true) || startLink; const linkDoc = DocUtils.MakeLink(startLink, endLink, { link_relationship: DocumentLinksButton.AnnotationId ? 'hypothes.is annotation' : undefined }); LinkManager.currentLink = linkDoc; - if (DocumentLinksButton.AnnotationId && DocumentLinksButton.AnnotationUri) { - // if linking from a Hypothes.is annotation - Doc.GetProto(linkDoc as Doc).linksToAnnotation = true; - Doc.GetProto(linkDoc as Doc).annotationId = DocumentLinksButton.AnnotationId; - Doc.GetProto(linkDoc as Doc).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 - } - 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; TaskCompletionBox.taskCompleted = true; - if (LinkDescriptionPopup.showDescriptions === 'ON' || !LinkDescriptionPopup.showDescriptions) { - LinkDescriptionPopup.popupX = screenX; - LinkDescriptionPopup.popupY = screenY - 100; - LinkDescriptionPopup.descriptionPopup = true; + if (LinkDescriptionPopup.Instance.showDescriptions === 'ON' || !LinkDescriptionPopup.Instance.showDescriptions) { + LinkDescriptionPopup.Instance.popupX = screenX; + LinkDescriptionPopup.Instance.popupY = screenY - 100; + LinkDescriptionPopup.Instance.display = true; } const rect = document.body.getBoundingClientRect(); - if (LinkDescriptionPopup.popupX + 200 > rect.width) { - LinkDescriptionPopup.popupX -= 190; + if (LinkDescriptionPopup.Instance.popupX + 200 > rect.width) { + LinkDescriptionPopup.Instance.popupX -= 190; TaskCompletionBox.popupX -= 40; } - if (LinkDescriptionPopup.popupY + 100 > rect.height) { - LinkDescriptionPopup.popupY -= 40; + if (LinkDescriptionPopup.Instance.popupY + 100 > rect.height) { + LinkDescriptionPopup.Instance.popupY -= 40; TaskCompletionBox.popupY -= 40; } setTimeout( - action(() => { - TaskCompletionBox.taskCompleted = false; - }), + action(() => (TaskCompletionBox.taskCompleted = false)), 2500 ); } @@ -189,8 +207,8 @@ export class DocumentLinksButton extends React.Component<DocumentLinksButtonProp @computed get filteredLinks() { const results = [] as Doc[]; - const filters = this.props.View.props.childFilters(); - Array.from(new Set<Doc>(this.props.View.allLinks)).forEach(link => { + const filters = this._props.View._props.childFilters(); + Array.from(new Set<Doc>(this._props.View.allLinks)).forEach(link => { if (DocUtils.FilterDocs([link], filters, []).length || DocUtils.FilterDocs([link.link_anchor_2 as Doc], filters, []).length || DocUtils.FilterDocs([link.link_anchor_1 as Doc], filters, []).length) { results.push(link); } @@ -205,8 +223,8 @@ export class DocumentLinksButton extends React.Component<DocumentLinksButtonProp */ @computed get linkButtonInner() { const btnDim = 30; - const isActive = DocumentLinksButton.StartLink === this.props.View.props.Document && this.props.StartLink; - const scaling = Math.min(1, this.props.scaling?.() || 1); + const isActive = DocumentLinksButton.StartLink === this._props.View.Document && this._props.StartLink; + const scaling = Math.min(1, this._props.scaling?.() || 1); const showLinkCount = (onHover?: boolean, offset?: boolean) => ( <div className="documentLinksButton-showCount" @@ -220,16 +238,16 @@ export class DocumentLinksButton extends React.Component<DocumentLinksButtonProp <span style={{ width: '100%', display: 'inline-block', textAlign: 'center' }}>{Array.from(this.filteredLinks).length}</span> </div> ); - return this.props.ShowCount ? ( - showLinkCount(this.props.OnHover, this.props.Bottom) + return this._props.ShowCount ? ( + showLinkCount(this._props.OnHover, this._props.Bottom) ) : ( <div className="documentLinksButton-menu"> - {this.props.StartLink ? ( //if link has been started from current node, then set behavior of link button to deactivate linking when clicked again + {this._props.StartLink ? ( //if link has been started from current node, then set behavior of link button to deactivate linking when clicked again <div className={`documentLinksButton ${isActive ? `startLink` : ``}`} ref={this._linkButton} onPointerDown={isActive ? StopEvent : this.onLinkButtonDown} onClick={isActive ? this.clearLinks : this.onLinkClick}> <FontAwesomeIcon className="documentdecorations-icon" icon="link" /> </div> ) : null} - {!this.props.StartLink && DocumentLinksButton.StartLink !== this.props.View.props.Document ? ( //if the origin node is not this node + {!this._props.StartLink && DocumentLinksButton.StartLink !== this._props.View.Document ? ( //if the origin node is not this node <div className={'documentLinksButton-endLink'} ref={this._linkButton} onPointerDown={DocumentLinksButton.StartLink && this.completeLink}> <FontAwesomeIcon className="documentdecorations-icon" icon="link" /> </div> @@ -239,20 +257,21 @@ export class DocumentLinksButton extends React.Component<DocumentLinksButtonProp } render() { - const menuTitle = this.props.StartLink ? 'Drag or tap to start link' : 'Tap to complete link'; + if (this.props.hideCount?.()) return null; + const menuTitle = this._props.StartLink ? 'Drag or tap to start link' : 'Tap to complete link'; const buttonTitle = 'Tap to view links; double tap to open link collection'; - const title = this.props.ShowCount ? buttonTitle : menuTitle; + const title = this._props.ShowCount ? buttonTitle : menuTitle; //render circular tooltip if it isn't set to invisible and show the number of doc links the node has, and render inner-menu link button for starting/stopping links if currently in menu - return !Array.from(this.filteredLinks).length && !this.props.AlwaysOn ? null : ( + return !Array.from(this.filteredLinks).length && !this._props.AlwaysOn ? null : ( <div className="documentLinksButton-wrapper" style={{ - position: this.props.InMenu ? 'relative' : 'absolute', + position: this._props.InMenu ? 'relative' : 'absolute', top: 0, pointerEvents: 'none', }}> - {DocumentLinksButton.LinkEditorDocView ? this.linkButtonInner : <Tooltip title={<div className="dash-tooltip">{title}</div>}>{this.linkButtonInner}</Tooltip>} + {DocButtonState.Instance.LinkEditorDocView ? this.linkButtonInner : <Tooltip title={<div className="dash-tooltip">{title}</div>}>{this.linkButtonInner}</Tooltip>} </div> ); } diff --git a/src/client/views/nodes/DocumentView.scss b/src/client/views/nodes/DocumentView.scss index 931594568..c4dab16fb 100644 --- a/src/client/views/nodes/DocumentView.scss +++ b/src/client/views/nodes/DocumentView.scss @@ -1,4 +1,4 @@ -@import '../global/globalCssVariables'; +@import '../global/globalCssVariables.module.scss'; .documentView-effectsWrapper { border-radius: inherit; @@ -61,7 +61,6 @@ .documentView-htmlOverlayInner { box-shadow: black 0.2vw 0.2vw 0.8vw; background: rgb(255, 255, 255); - overflow: auto; position: relative; margin: auto; padding: 20px; @@ -120,6 +119,10 @@ display: flex; justify-content: center; align-items: center; + position: relative; // allows contents to be positioned relative/below title + > .formattedTextBox { + position: absolute; // position a child text box + } .sharingIndicator { height: 30px; @@ -183,12 +186,15 @@ top: 0; width: 100%; height: 14; - background: rgba(0, 0, 0, 0.4); + opacity: 0.5; text-align: center; text-overflow: ellipsis; white-space: pre; position: absolute; display: flex; // this allows title field dropdown to be inline with editable title + &:hover { + opacity: 1; + } } .documentView-titleWrapper-hover { diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx index 3d6b53ccc..8eb354e1e 100644 --- a/src/client/views/nodes/DocumentView.tsx +++ b/src/client/views/nodes/DocumentView.tsx @@ -1,30 +1,29 @@ import { IconProp } from '@fortawesome/fontawesome-svg-core'; import { Dropdown, DropdownType, Type } from 'browndash-components'; -import { action, computed, IReactionDisposer, observable, reaction, runInAction } from 'mobx'; +import { Howl } from 'howler'; +import { IReactionDisposer, action, computed, makeObservable, observable, reaction, runInAction } from 'mobx'; import { observer } from 'mobx-react'; -import { computedFn } from 'mobx-utils'; -import { Bounce, Fade, Flip, LightSpeed, Roll, Rotate, Zoom } from 'react-reveal'; +import * as React from 'react'; +import { Bounce, Fade, Flip, JackInTheBox, Roll, Rotate, Zoom } from 'react-awesome-reveal'; +import { 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, Width } from '../../../fields/DocSymbols'; +import { AclPrivate, Animation, AudioPlay, DocData, DocViews } from '../../../fields/DocSymbols'; import { Id } from '../../../fields/FieldSymbols'; import { InkTool } from '../../../fields/InkField'; import { List } from '../../../fields/List'; -import { RefField } from '../../../fields/RefField'; import { listSpec } from '../../../fields/Schema'; import { ScriptField } from '../../../fields/ScriptField'; -import { BoolCast, Cast, DocCast, ImageCast, NumCast, ScriptCast, StrCast } from '../../../fields/Types'; +import { BoolCast, Cast, DocCast, NumCast, ScriptCast, StrCast } from '../../../fields/Types'; import { AudioField } from '../../../fields/URLField'; import { GetEffectiveAcl, TraceMobx } from '../../../fields/util'; -import { emptyFunction, isTargetChildOf as isParentOf, lightOrDark, returnEmptyString, returnFalse, returnTrue, returnVal, simulateMouseClick, Utils } from '../../../Utils'; -import { GooglePhotos } from '../../apis/google_docs/GooglePhotosClientUtils'; import { DocServer } from '../../DocServer'; -import { DocOptions, Docs, DocUtils, FInfo } from '../../documents/Documents'; -import { CollectionViewType, DocumentType } from '../../documents/DocumentTypes'; import { Networking } from '../../Network'; +import { GooglePhotos } from '../../apis/google_docs/GooglePhotosClientUtils'; +import { CollectionViewType, DocumentType } from '../../documents/DocumentTypes'; +import { DocOptions, DocUtils, Docs, FInfo } from '../../documents/Documents'; import { DictationManager } from '../../util/DictationManager'; import { DocumentManager } from '../../util/DocumentManager'; import { DragManager, dropActionType } from '../../util/DragManager'; -import { InteractionUtils } from '../../util/InteractionUtils'; import { FollowLinkScript } from '../../util/LinkFollower'; import { LinkManager } from '../../util/LinkManager'; import { ScriptingGlobals } from '../../util/ScriptingGlobals'; @@ -32,38 +31,37 @@ import { SelectionManager } from '../../util/SelectionManager'; import { SettingsManager } from '../../util/SettingsManager'; import { SharingManager } from '../../util/SharingManager'; import { SnappingManager } from '../../util/SnappingManager'; -import { Transform } from '../../util/Transform'; -import { undoBatch, UndoManager } from '../../util/UndoManager'; +import { UndoManager, undoBatch, undoable } from '../../util/UndoManager'; import { ContextMenu } from '../ContextMenu'; import { ContextMenuProps } from '../ContextMenuItem'; -import { DocComponent } from '../DocComponent'; +import { DocComponent, ViewBoxInterface } from '../DocComponent'; import { EditableView } from '../EditableView'; import { GestureOverlay } from '../GestureOverlay'; import { LightboxView } from '../LightboxView'; import { StyleProp } from '../StyleProvider'; -import { UndoStack } from '../UndoStack'; -import { CollectionFreeFormDocumentView } from './CollectionFreeFormDocumentView'; import { DocumentContentsView, ObserverJsxParser } from './DocumentContentsView'; import { DocumentLinksButton } from './DocumentLinksButton'; import './DocumentView.scss'; -import { FieldViewProps } from './FieldView'; -import { FormattedTextBox } from './formattedText/FormattedTextBox'; +import { FieldViewProps, FieldViewSharedProps } from './FieldView'; import { KeyValueBox } from './KeyValueBox'; import { LinkAnchorBox } from './LinkAnchorBox'; +import { FormattedTextBox } from './formattedText/FormattedTextBox'; import { PresEffect, PresEffectDirection } from './trails'; -import { PinProps, PresBox } from './trails/PresBox'; -import React = require('react'); -const { Howl } = require('howler'); - interface Window { MediaRecorder: MediaRecorder; } - declare class MediaRecorder { - // whatever MediaRecorder has - constructor(e: any); + 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', @@ -79,136 +77,13 @@ export enum OpenWhere { inParent = 'inParent', inParentFromScreen = 'inParentFromScreen', overlay = 'overlay', -} -export enum OpenWhereMod { - none = '', - left = 'left', - right = 'right', - top = 'top', - bottom = 'bottom', - rightKeyValue = 'rightKeyValue', + addRightKeyvalue = 'add:right:keyValue', } -export interface DocFocusOptions { - 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 DocFocusFunc = (doc: Doc, options: DocFocusOptions) => Opt<number>; -export type StyleProviderFunc = (doc: Opt<Doc>, props: Opt<DocumentViewProps>, property: string) => any; -export interface DocComponentView { - updateIcon?: () => void; // updates the icon representation of the document - getAnchor?: (addAsAnnotation: boolean, pinData?: PinProps) => Doc; // returns an Anchor Doc that represents the current state of the doc's componentview (e.g., the current playhead location of a an audio/video box) - restoreView?: (viewSpec: Doc) => boolean; - scrollPreview?: (docView: DocumentView, doc: Doc, focusSpeed: number, options: DocFocusOptions) => Opt<number>; // returns the duration of the focus - brushView?: (view: { width: number; height: number; panX: number; panY: number }, transTime: number) => void; // highlight a region of a view (used by freeforms) - getView?: (doc: Doc) => Promise<Opt<DocumentView>>; // returns a nested DocumentView for the specified doc or undefined - addDocTab?: (doc: Doc, where: OpenWhere) => boolean; // determines how to add a document - used in following links to open the target ina local lightbox - addDocument?: (doc: Doc | Doc[], annotationKey?: string) => boolean; // add a document (used only by collections) - reverseNativeScaling?: () => boolean; // DocumentView's setup screenToLocal based on the doc having a nativeWidth/Height. However, some content views (e.g., FreeFormView w/ fitContentsToBox set) may ignore the native dimensions so this flags the DocumentView to not do Nativre scaling. - select?: (ctrlKey: boolean, shiftKey: boolean) => void; - focus?: (textAnchor: Doc, options: DocFocusOptions) => Opt<number>; - menuControls?: () => JSX.Element; // controls to display in the top menu bar when the document is selected. - isAnyChildContentActive?: () => boolean; // is any child content of the document active - onClickScriptDisable?: () => 'never' | 'always'; // disable click scripts : never, always, or undefined = only when selected - getKeyFrameEditing?: () => boolean; // whether the document is in keyframe editing mode (if it is, then all hidden documents that are not active at the keyframe time will still be shown) - setKeyFrameEditing?: (set: boolean) => void; // whether the document is in keyframe editing mode (if it is, then all hidden documents that are not active at the keyframe time will still be shown) - playFrom?: (time: number, endTime?: number) => void; - Pause?: () => void; // pause a media document (eg, audio/video) - IsPlaying?: () => boolean; // is a media document playing - TogglePause?: (keep?: boolean) => void; // toggle media document playing state - setFocus?: () => void; // sets input focus to the componentView - setData?: (data: Field | Promise<RefField | undefined>) => boolean; - componentUI?: (boundsLeft: number, boundsTop: number) => JSX.Element | null; - incrementalRendering?: () => void; - layout_fitWidth?: () => boolean; // whether the component always fits width (eg, KeyValueBox) - overridePointerEvents?: () => 'all' | 'none' | undefined; // if the conmponent overrides the pointer events for the document - fieldKey?: string; - annotationKey?: string; - getTitle?: () => string; - getCenter?: (xf: Transform) => { X: number; Y: number }; - screenBounds?: () => { left: number; top: number; right: number; bottom: number; center?: { X: number; Y: number } }; - ptToScreen?: (pt: { X: number; Y: number }) => { X: number; Y: number }; - ptFromScreen?: (pt: { X: number; Y: number }) => { X: number; Y: number }; - snapPt?: (pt: { X: number; Y: number }, excludeSegs?: number[]) => { nearestPt: { X: number; Y: number }; distance: number }; - search?: (str: string, bwd?: boolean, clear?: boolean) => boolean; -} -// These props are passed to both FieldViews and DocumentViews -export interface DocumentViewSharedProps { - fieldKey?: string; // only used by FieldViews but helpful here to allow styleProviders to access fieldKey of FieldViewProps. In priniciple, passing a fieldKey to a documentView could override or be the default fieldKey for fieldViews - DocumentView?: () => DocumentView; - renderDepth: number; - Document: Doc; - DataDoc?: Doc; - 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 - suppressSetHeight?: boolean; - setContentView?: (view: DocComponentView) => any; - CollectionFreeFormDocumentView?: () => CollectionFreeFormDocumentView; - PanelWidth: () => number; - PanelHeight: () => number; - shouldNotScale?: () => boolean; - docViewPath: () => DocumentView[]; - childHideDecorationTitle?: () => boolean; - childHideResizeHandles?: () => boolean; - childDragAction?: dropActionType; // allows child documents to be dragged out of collection without holding the embedKey or dragging the doc decorations title bar. - dataTransition?: string; // specifies animation transition - used by collectionPile and potentially other layout engines when changing the size of documents so that the change won't be abrupt - styleProvider: Opt<StyleProviderFunc>; - setTitleFocus?: () => void; - focus: DocFocusFunc; - layout_fitWidth?: (doc: Doc) => boolean | undefined; - childFilters: () => string[]; - childFiltersByRanges: () => string[]; - searchFilterDocs: () => Doc[]; - layout_showTitle?: () => string; - whenChildContentsActiveChanged: (isActive: boolean) => void; - rootSelected: (outsideReaction?: boolean) => boolean; // whether the root of a template has been selected - addDocTab: (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; - moveDocument?: (doc: Doc | Doc[], targetCollection: Doc | undefined, addDocument: (document: Doc | Doc[], annotationKey?: string) => boolean) => boolean; - pinToPres: (document: Doc, pinProps: PinProps) => void; - ScreenToLocalTransform: () => Transform; - bringToFront: (doc: Doc, sendToBack?: boolean) => void; - dragAction?: dropActionType; - treeViewDoc?: Doc; - xPadding?: number; - yPadding?: number; - dropAction?: dropActionType; - dontRegisterView?: boolean; - hideLinkButton?: boolean; - hideCaptions?: boolean; - ignoreAutoHeight?: boolean; - forceAutoHeight?: boolean; - disableBrushing?: boolean; // should highlighting for this view be disabled when same document in another view is hovered over. - onClickScriptDisable?: 'never' | 'always'; // undefined = only when selected - waitForDoubleClickToClick?: () => 'never' | 'always' | undefined; - defaultDoubleClick?: () => 'default' | 'ignore' | undefined; - pointerEvents?: () => Opt<string>; - scriptContext?: any; // can be assigned anything and will be passed as 'scriptContext' to any OnClick script that executes on this document - createNewFilterDoc?: () => void; - updateFilterDoc?: (doc: Doc) => void; - dontHideOnDrag?: boolean; +export function returnEmptyDocViewList() { + return [] as DocumentView[]; } - -// these props are specific to DocuentViews -export interface DocumentViewProps extends DocumentViewSharedProps { - // properties specific to DocumentViews but not to FieldView +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 hideTitle?: boolean; // forces suppression of title. e.g, treeView document labels suppress titles in case they are globally active via settings @@ -217,38 +92,35 @@ export interface DocumentViewProps extends DocumentViewSharedProps { hideOpenButton?: boolean; hideDeleteButton?: boolean; hideLinkAnchors?: boolean; - isDocumentActive?: () => boolean | undefined; // whether a document should handle pointer events - isContentActive: () => boolean | undefined; // whether document contents should handle pointer events + hideLinkButton?: boolean; + hideCaptions?: boolean; contentPointerEvents?: 'none' | 'all' | undefined; // pointer events allowed for content of a document view. eg. set to "none" in menuSidebar for sharedDocs so that you can select a document, but not interact with its contents - radialMenu?: String[]; - LayoutTemplateString?: string; dontCenter?: 'x' | 'y' | 'xy'; + childHideDecorationTitle?: boolean; + childHideResizeHandles?: boolean; + childDragAction?: dropActionType; // allows child documents to be dragged out of collection without holding the embedKey or dragging the doc decorations title bar. + dragWhenActive?: boolean; + dontHideOnDrag?: boolean; + suppressSetHeight?: boolean; + onClickScriptDisable?: 'never' | 'always'; // undefined = only when selected NativeWidth?: () => number; NativeHeight?: () => number; - NativeDimScaling?: () => number; // scaling the DocumentView does to transform its contents into its panel & needed by ScreenToLocal NOTE: Must also be added to FieldViewProps - LayoutTemplate?: () => Opt<Doc>; contextMenuItems?: () => { script: ScriptField; filter?: ScriptField; label: string; icon: string }[]; - onClick?: () => ScriptField; - onDoubleClick?: () => ScriptField; - onPointerDown?: () => ScriptField; - onPointerUp?: () => ScriptField; - onBrowseClick?: () => ScriptField | undefined; - onKey?: (e: React.KeyboardEvent, fieldProps: FieldViewProps) => boolean | undefined; -} - -// these props are only available in DocumentViewIntenral -export interface DocumentViewInternalProps extends DocumentViewProps { - NativeWidth: () => number; - NativeHeight: () => number; - isSelected: (outsideReaction?: boolean) => boolean; - select: (ctrlPressed: boolean, shiftPress?: boolean) => void; - DocumentView: () => DocumentView; - viewPath: () => DocumentView[]; + dragStarting?: () => void; + dragEnding?: () => void; } - @observer -export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps>() { +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; + private _disposers: { [name: string]: IReactionDisposer } = {}; private _doubleClickTimeout: NodeJS.Timeout | undefined; private _singleClickFunc: undefined | (() => any); @@ -261,76 +133,53 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps private _mainCont = React.createRef<HTMLDivElement>(); private _titleRef = React.createRef<EditableView>(); private _dropDisposer?: DragManager.DragDropDisposer; - private _holdDisposer?: InteractionUtils.MultiTouchEventDisposer; - protected _multiTouchDisposer?: InteractionUtils.MultiTouchEventDisposer; + constructor(props: FieldViewProps & DocumentViewProps) { + super(props); + makeObservable(this); + } - @observable _componentView: Opt<DocComponentView>; // needs to be accessed from DocumentView wrapper class - @observable _animateScaleTime: Opt<number>; // milliseconds for animating between views. defaults to 300 if not uset + @observable _changingTitleField = false; + @observable _titleDropDownInnerWidth = 0; // width of menu dropdown when setting doc title + @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 _animateScaleTime: Opt<number> = undefined; // milliseconds for animating between views. defaults to 300 if not uset @observable _animateScalingTo = 0; - public get animateScaleTime() { - return this._animateScaleTime ?? 100; - } - public get displayName() { - return 'DocumentView(' + this.props.Document.title + ')'; - } // this makes mobx trace() statements more descriptive - public get ContentDiv() { - return this._mainCont.current; - } - public get LayoutFieldKey() { - return Doc.LayoutFieldKey(this.layoutDoc); - } - @computed get layout_showTitle() { - return this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.ShowTitle) as Opt<string>; - } - @computed get NativeDimScaling() { - return this.props.NativeDimScaling?.() || 1; - } - @computed get thumb() { - return ImageCast(this.layoutDoc['thumb-frozen'], ImageCast(this.layoutDoc.thumb))?.url?.href.replace('.png', '_m.png'); - } - @computed get opacity() { - return this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.Opacity); - } - @computed get boxShadow() { - return this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.BoxShadow); - } - @computed get borderRounding() { - return this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.BorderRounding); - } - @computed get widgetDecorations() { - TraceMobx(); - return this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.Decorations); - } - @computed get backgroundBoxColor() { - return this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.BackgroundColor + ':box'); - } - @computed get docContents() { - return this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.DocContents); - } - @computed get headerMargin() { - return this.props?.styleProvider?.(this.layoutDoc, this.props, StyleProp.HeaderMargin) || 0; - } - @computed get layout_showCaption() { - return this.props?.styleProvider?.(this.layoutDoc, this.props, StyleProp.ShowCaption) || 0; - } - @computed get titleHeight() { - return this.props?.styleProvider?.(this.layoutDoc, this.props, StyleProp.TitleHeight) || 0; - } - @computed get pointerEvents(): 'none' | 'all' | 'visiblePainted' | undefined { - return this.props.styleProvider?.(this.Document, this.props, StyleProp.PointerEvents); + get _contentDiv() { return this._mainCont.current; } // prettier-ignore + get _docView() { return this._props.DocumentView?.(); } // prettier-ignore + + animateScaleTime = () => this._animateScaleTime ?? 100; + style = (doc: Doc, sprop: StyleProp | string) => this._props.styleProvider?.(doc, this._props, sprop); + @computed get layout_showTitle() { return this.style(this.layoutDoc, StyleProp.ShowTitle) as Opt<string>; } // prettier-ignore + @computed get opacity() { return this.style(this.layoutDoc, StyleProp.Opacity); } // prettier-ignore + @computed get boxShadow() { return this.style(this.layoutDoc, StyleProp.BoxShadow); } // prettier-ignore + @computed get borderRounding() { return this.style(this.layoutDoc, StyleProp.BorderRounding); } // prettier-ignore + @computed get widgetDecorations() { return this.style(this.layoutDoc, StyleProp.Decorations); } // prettier-ignore + @computed get backgroundBoxColor() { return this.style(this.layoutDoc, StyleProp.BackgroundColor + ':box'); } // prettier-ignore + @computed get headerMargin() { return this.style(this.layoutDoc, StyleProp.HeaderMargin) ?? 0; } // prettier-ignore + @computed get layout_showCaption() { return this.style(this.layoutDoc, StyleProp.ShowCaption) ?? 0; } // prettier-ignore + @computed get titleHeight() { return this.style(this.layoutDoc, StyleProp.TitleHeight) ?? 0; } // prettier-ignore + @computed get docContents() { return this.style(this.Document, StyleProp.DocContents); } // prettier-ignore + @computed get highlighting() { return this.style(this.Document, StyleProp.Highlighting); } // prettier-ignore + @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)); } - @computed get finalLayoutKey() { - return StrCast(this.Document.layout_fieldKey, 'layout'); + @computed get onDoubleClickHandler() { + return this._props.onDoubleClickScript?.() ?? ScriptCast(this.layoutDoc.onDoubleClick, ScriptCast(this.Document.onDoubleClick)); } - @computed get nativeWidth() { - return this.props.NativeWidth(); + @computed get onPointerDownHandler() { + return this._props.onPointerDownScript?.() ?? ScriptCast(this.layoutDoc.onPointerDown, ScriptCast(this.Document.onPointerDown)); } - @computed get nativeHeight() { - return this.props.NativeHeight(); + @computed get onPointerUpHandler() { + return this._props.onPointerUpScript?.() ?? ScriptCast(this.layoutDoc.onPointerUp, ScriptCast(this.Document.onPointerUp)); } + @computed get disableClickScriptFunc() { - const onScriptDisable = this.props.onClickScriptDisable ?? this._componentView?.onClickScriptDisable?.() ?? this.layoutDoc.onClickScriptDisable; + const onScriptDisable = this._props.onClickScriptDisable ?? this._componentView?.onClickScriptDisable?.() ?? this.layoutDoc.onClickScriptDisable; // prettier-ignore return ( DocumentView.LongPress || @@ -338,116 +187,148 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps (onScriptDisable !== 'never' && (this.rootSelected() || this._componentView?.isAnyChildContentActive?.())) ); } - @computed get onClickHandler() { - return this.props.onClick?.() ?? this.props.onBrowseClick?.() ?? Cast(this.Document.onClick, ScriptField, Cast(this.layoutDoc.onClick, ScriptField, null)); + @computed get _rootSelected() { + return this._props.isSelected() || BoolCast(this._props.TemplateDataDocument && this._props.rootSelected?.()); } - @computed get onDoubleClickHandler() { - return this.props.onDoubleClick?.() ?? Cast(this.layoutDoc.onDoubleClick, ScriptField, null) ?? this.Document.onDoubleClick; + /// disable pointer events on content when there's an enabled onClick script (but not the browse script) and the contents aren't forced active, or if contents are marked inactive + @computed get _contentPointerEvents() { + TraceMobx(); + return this._props.contentPointerEvents ?? + ((!this.disableClickScriptFunc && // + this.onClickHandler && + !this._props.onBrowseClickScript?.() && + this.isContentActive() !== true) || + this.isContentActive() === false) + ? 'none' + : this._pointerEvents; } - @computed get onPointerDownHandler() { - return this.props.onPointerDown?.() ?? ScriptCast(this.Document.onPointerDown); + + // We need to use allrelatedLinks to get not just links to the document as a whole, but links to + // anchors that are not rendered as DocumentViews (marked as 'layout_unrendered' with their 'annotationOn' set to this document). e.g., + // - 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. + @computed get directLinks() { + TraceMobx(); + return LinkManager.Instance.getAllRelatedLinks(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)) || + ((link.link_anchor_1 as Doc)?.layout_unrendered && Doc.AreProtosEqual((link.link_anchor_1 as Doc)?.annotationOn as Doc, this.Document)) || + ((link.link_anchor_2 as Doc)?.layout_unrendered && Doc.AreProtosEqual((link.link_anchor_2 as Doc)?.annotationOn as Doc, this.Document)) + ); } - @computed get onPointerUpHandler() { - return this.props.onPointerUp?.() ?? ScriptCast(this.Document.onPointerUp); + @computed get _allLinks() { + TraceMobx(); + return LinkManager.Instance.getAllRelatedLinks(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); } componentWillUnmount() { this.cleanupHandlers(true); } + componentDidMount() { + runInAction(() => (this._mounted = true)); this.setupHandlers(); + this._disposers.contentActive = reaction( + () => { + // true - if the document has been activated directly or indirectly (by having its children selected) + // false - if its pointer events are explicitly turned off or if it's container tells it that it's inactive + // undefined - it is not active, but it should be responsive to actions that might activate it or its contents (eg clicking) + return this._props.isContentActive() === false || this._props.pointerEvents?.() === 'none' + ? false + : Doc.ActiveTool !== InkTool.None || SnappingManager.CanEmbed || this.rootSelected() || this.Document.forceActive || this._componentView?.isAnyChildContentActive?.() || this._props.isContentActive() + ? true + : undefined; + }, + active => (this._isContentActive = active), + { fireImmediately: true } + ); + this._disposers.pointerevents = reaction( + () => this.style(this.Document, StyleProp.PointerEvents), + pointerevents => (this._pointerEvents = pointerevents), + { fireImmediately: true } + ); } - preDropFunc = (e: Event, de: DragManager.DropEvent) => { - const dropAction = this.layoutDoc.dropAction as dropActionType; - if (de.complete.docDragData && this.isContentActive() && !this.props.treeViewDoc) { - dropAction && (de.complete.docDragData.dropAction = dropAction); + 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; e.stopPropagation(); } }; setupHandlers() { this.cleanupHandlers(false); if (this._mainCont.current) { - this._dropDisposer = DragManager.MakeDropTarget(this._mainCont.current, this.drop.bind(this), this.props.Document, this.preDropFunc); - this._multiTouchDisposer = InteractionUtils.MakeMultiTouchTarget(this._mainCont.current, this.onTouchStart.bind(this)); - this._holdDisposer = InteractionUtils.MakeHoldTouchTarget(this._mainCont.current, this.handle1PointerHoldStart.bind(this)); + this._dropDisposer = DragManager.MakeDropTarget(this._mainCont.current, this.drop.bind(this), this.Document, this.preDrop); } } - @action + cleanupHandlers(unbrush: boolean) { this._dropDisposer?.(); - this._multiTouchDisposer?.(); - this._holdDisposer?.(); - unbrush && Doc.UnBrushDoc(this.props.Document); + unbrush && Doc.UnBrushDoc(this.Document); Object.values(this._disposers).forEach(disposer => disposer?.()); } startDragging(x: number, y: number, dropAction: dropActionType, hideSource = false) { - if (this._mainCont.current) { - const views = SelectionManager.Views().filter(dv => dv.docView?._mainCont.current); - const selected = views.some(dv => dv.rootDoc === this.Document) ? views : [this.props.DocumentView()]; - const dragData = new DragManager.DocumentDragData(selected.map(dv => dv.rootDoc)); - const [left, top] = this.props.ScreenToLocalTransform().scale(this.NativeDimScaling).inverse().transformPoint(0, 0); - dragData.offset = this.props - .ScreenToLocalTransform() - .scale(this.NativeDimScaling) - .transformDirection(x - left, y - top); + const docView = this._docView; + if (this._mainCont.current && docView) { + const views = SelectionManager.Views.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(); + const [left, top] = screenXf.inverse().transformPoint(0, 0); + dragData.offset = screenXf.transformDirection(x - left, y - top); dragData.dropAction = dropAction; - dragData.treeViewDoc = this.props.treeViewDoc; - dragData.removeDocument = this.props.removeDocument; - dragData.moveDocument = this.props.moveDocument; - dragData.draggedViews = [this.props.DocumentView()]; - dragData.canEmbed = this.rootDoc.dragAction ?? this.props.dragAction ? true : false; + dragData.removeDocument = this._props.removeDocument; + dragData.moveDocument = this._props.moveDocument; + dragData.draggedViews = [docView]; + dragData.canEmbed = this.Document.dragAction ?? this._props.dragAction ? true : false; + this._componentView?.dragConfig?.(dragData); DragManager.StartDocumentDrag( - selected.map(dv => dv.docView!._mainCont.current!), + selected.map(dv => dv.ContentDiv!), dragData, x, y, - { hideSource: hideSource || (!dropAction && !this.layoutDoc.onDragStart && !this.props.dontHideOnDrag) } + { hideSource: hideSource || (!dropAction && !this.layoutDoc.onDragStart && !this._props.dontHideOnDrag) } ); // this needs to happen after the drop event is processed. } } - defaultRestoreTargetView = (docView: DocumentView, anchor: Doc, focusSpeed: number, options: DocFocusOptions) => { - const targetMatch = - Doc.AreProtosEqual(anchor, this.rootDoc) || // anchor is this document, so anchor's properties apply to this document - (DocCast(anchor)?.layout_unrendered && Doc.AreProtosEqual(DocCast(anchor.annotationOn), this.rootDoc)) // the anchor is an layout_unrendered annotation on this document, so anchor properties apply to this document - ? true - : false; - return targetMatch && PresBox.restoreTargetDocView(docView, anchor, focusSpeed) ? focusSpeed : undefined; - }; - // switches text input focus to the title bar of the document (and displays the title bar if it hadn't been) setTitleFocus = () => { 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 }; - - public static addDocTabFunc: (doc: Doc, location: OpenWhere) => boolean = returnFalse; - onClick = action((e: React.MouseEvent | React.PointerEvent) => { - if (this.props.isGroupActive?.() === 'child' && !this.props.isDocumentActive?.()) return; - if (!this.Document.ignoreClick && this.props.renderDepth >= 0 && Utils.isClick(e.clientX, e.clientY, this._downX, this._downY, this._downTime)) { + if (this._props.isGroupActive?.() === 'child' && !this._props.isDocumentActive?.()) return; + 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)) { let stopPropagate = true; let preventDefault = true; - !this.rootDoc._keepZWhenDragged && this.props.bringToFront(this.rootDoc); + !this.layoutDoc._keepZWhenDragged && this._props.bringToFront?.(this.Document); if (this._doubleTap) { - const defaultDblclick = this.props.defaultDoubleClick?.() || this.Document.defaultDoubleClick; + const defaultDblclick = this._props.defaultDoubleClick?.() || this.Document.defaultDoubleClick; if (this.onDoubleClickHandler?.script) { const { clientX, clientY, shiftKey, altKey, ctrlKey } = e; // or we could call e.persist() to capture variables // prettier-ignore const func = () => this.onDoubleClickHandler.script.run( { - this: this.layoutDoc, - self: this.rootDoc, - scriptContext: this.props.scriptContext, - documentView: this.props.DocumentView(), + this: this.Document, + scriptContext: this._props.scriptContext, + documentView, clientX, clientY, altKey, shiftKey, ctrlKey, value: undefined, }, console.log ); - UndoManager.RunInBatch(() => (func().result?.select === true ? this.props.select(false) : ''), 'on double click'); - } else if (!Doc.IsSystem(this.rootDoc) && (defaultDblclick === undefined || defaultDblclick === 'default')) { - UndoManager.RunInBatch(() => LightboxView.AddDocTab(this.rootDoc, OpenWhere.lightbox), 'double tap'); + UndoManager.RunInBatch(() => (func().result?.select === true ? this._props.select(false) : ''), 'on double click'); + } else if (!Doc.IsSystem(this.Document) && (defaultDblclick === undefined || defaultDblclick === 'default')) { + UndoManager.RunInBatch(() => LightboxView.Instance.AddDocTab(this.Document, OpenWhere.lightbox), 'double tap'); SelectionManager.DeselectAll(); - Doc.UnBrushDoc(this.props.Document); + Doc.UnBrushDoc(this.Document); } else { this._singleClickFunc?.(); } @@ -464,14 +345,13 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps // e.g., if this document is part of a labeled 'lightbox' container, then documents will be shown in place // instead of in the global lightbox const oldFunc = DocumentViewInternal.addDocTabFunc; - DocumentViewInternal.addDocTabFunc = this.props.addDocTab; + DocumentViewInternal.addDocTabFunc = this._props.addDocTab; this.onClickHandler?.script.run( { - this: this.layoutDoc, - self: this.rootDoc, + this: this.Document, _readOnly_: false, - scriptContext: this.props.scriptContext, - documentView: this.props.DocumentView(), + scriptContext: this._props.scriptContext, + documentView, clientX, clientY, shiftKey, @@ -480,14 +360,14 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps }, console.log ).result?.select === true - ? this.props.select(false) + ? this._props.select(false) : ''; DocumentViewInternal.addDocTabFunc = oldFunc; }; - clickFunc = () => UndoManager.RunInBatch(func, 'click ' + this.rootDoc.title); + clickFunc = () => UndoManager.RunInBatch(func, 'click ' + this.Document.title); } else { // onDragStart implies a button doc that we don't want to select when clicking. RootDocument & isTemplateForField implies we're clicking on part of a template instance and we want to select the whole template, not the part - if ((this.layoutDoc.onDragStart || this.props.Document.rootDocument) && !(e.ctrlKey || e.button > 0)) { + if ((this.layoutDoc.onDragStart || this._props.TemplateDataDocument) && !(e.ctrlKey || e.button > 0)) { stopPropagate = false; // don't stop propagation for field templates -- want the selection to propagate up to the root document of the template } preventDefault = false; @@ -495,10 +375,10 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps const sendToBack = e.altKey; this._singleClickFunc = // prettier-ignore - clickFunc ?? (() => (sendToBack ? this.props.DocumentView().props.bringToFront(this.rootDoc, true) : + clickFunc ?? (() => (sendToBack ? documentView._props.bringToFront?.(this.Document, true) : this._componentView?.select?.(e.ctrlKey || e.metaKey, e.shiftKey) ?? - this.props.select(e.ctrlKey||e.shiftKey, e.metaKey))); - const waitFordblclick = this.props.waitForDoubleClickToClick?.() ?? this.Document.waitForDoubleClickToClick; + this._props.select(e.ctrlKey||e.shiftKey, e.metaKey))); + const waitFordblclick = this._props.waitForDoubleClickToClick?.() ?? this.Document.waitForDoubleClickToClick; if ((clickFunc && waitFordblclick !== 'never') || waitFordblclick === 'always') { this._doubleClickTimeout && clearTimeout(this._doubleClickTimeout); this._doubleClickTimeout = setTimeout(this._singleClickFunc, 300); @@ -512,40 +392,31 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps } }); - @action onPointerDown = (e: React.PointerEvent): void => { - if (this.props.isGroupActive?.() === 'child' && !this.props.isDocumentActive?.()) return; - this._longPressSelector = setTimeout(() => { - if (DocumentView.LongPress) { - if (this.rootDoc.undoIgnoreFields) { - runInAction(() => (UndoStack.HideInline = !UndoStack.HideInline)); - } else { - this.props.select(false); - } - } - }, 1000); - if (!GestureOverlay.DownDocView) GestureOverlay.DownDocView = this.props.DocumentView(); + 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; this._downX = e.clientX; this._downY = e.clientY; this._downTime = Date.now(); - if ((Doc.ActiveTool === InkTool.None || this.props.addDocTab === returnFalse) && !(this.props.Document.rootDocument && !(e.ctrlKey || e.button > 0))) { + if ((Doc.ActiveTool === InkTool.None || this._props.addDocTab === returnFalse) && !(this._props.TemplateDataDocument && !(e.ctrlKey || e.button > 0))) { // click events stop here if the document is active and no modes are overriding it // if this is part of a template, let the event go up to the template root unless right/ctrl clicking if ( // prettier-ignore - (this.props.isDocumentActive?.() || this.props.isContentActive?.()) && - !this.props.onBrowseClick?.() && + (this._props.isDocumentActive?.() || this._props.isContentActive?.()) && + !this._props.onBrowseClickScript?.() && !this.Document.ignoreClick && e.button === 0 && !Doc.IsInMyOverlay(this.layoutDoc) ) { e.stopPropagation(); // don't preventDefault anymore. Goldenlayout, PDF text selection and RTF text selection all need it to go though - //if (this.props.isSelected(true) && this.rootDoc.type !== DocumentType.PDF && this.layoutDoc._type_collection !== CollectionViewType.Docking) e.preventDefault(); + //if (this._props.isSelected(true) && this.Document.type !== DocumentType.PDF && this.layoutDoc._type_collection !== CollectionViewType.Docking) e.preventDefault(); // listen to move events if document content isn't active or document is draggable - if (!this.layoutDoc._lockedPosition && (!this.isContentActive() || BoolCast(this.rootDoc._dragWhenActive))) { + if (!this.layoutDoc._lockedPosition && (!this.isContentActive() || BoolCast(this.layoutDoc._dragWhenActive, this._props.dragWhenActive))) { document.addEventListener('pointermove', this.onPointerMove); } } @@ -553,74 +424,69 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps } }; - @action onPointerMove = (e: PointerEvent): void => { if (e.buttons !== 1 || [InkTool.Highlighter, InkTool.Pen, InkTool.Write].includes(Doc.ActiveTool)) return; if (!Utils.isClick(e.clientX, e.clientY, this._downX, this._downY, Date.now())) { this.cleanupPointerEvents(); this._longPressSelector && clearTimeout(this._longPressSelector); - this.startDragging(this._downX, this._downY, ((e.ctrlKey || e.altKey) && 'embed') || ((this.Document.dragAction || this.props.dragAction || undefined) as dropActionType)); + this.startDragging(this._downX, this._downY, ((e.ctrlKey || e.altKey) && 'embed') || ((this.Document.dragAction || this._props.dragAction || undefined) as dropActionType)); } }; cleanupPointerEvents = () => { - this.cleanUpInteractions(); document.removeEventListener('pointermove', this.onPointerMove); document.removeEventListener('pointerup', this.onPointerUp); }; - @action onPointerUp = (e: PointerEvent): void => { this.cleanupPointerEvents(); this._longPressSelector && clearTimeout(this._longPressSelector); if (this.onPointerUpHandler?.script) { - this.onPointerUpHandler.script.run({ self: this.rootDoc, this: this.layoutDoc }, console.log); + 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.rootDoc.defaultDoubleClick !== 'ignore') && Date.now() - this._lastTap < Utils.CLICK_TIME; + this._doubleTap = (this.onDoubleClickHandler?.script || this.Document.defaultDoubleClick !== 'ignore') && Date.now() - this._lastTap < Utils.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 } if (DocumentView.LongPress) e.preventDefault(); }; - @undoBatch - @action - toggleFollowLink = (zoom?: boolean, setTargetToggle?: boolean): void => { - const hadOnClick = this.rootDoc.onClick; + toggleFollowLink = undoable((zoom?: boolean, setTargetToggle?: boolean): void => { + const hadOnClick = this.Document.onClick; this.noOnClick(); this.Document.onClick = hadOnClick ? undefined : FollowLinkScript(); this.Document.waitForDoubleClickToClick = hadOnClick ? undefined : 'never'; - }; - @undoBatch - @action - followLinkOnClick = (): void => { + }, 'toggle follow link'); + + followLinkOnClick = undoable(() => { this.Document.ignoreClick = false; this.Document.onClick = FollowLinkScript(); this.Document.followLinkToggle = false; this.Document.followLinkZoom = false; this.Document.followLinkLocation = undefined; - }; - @undoBatch - noOnClick = (): void => { - this.Document.ignoreClick = false; - this.Document.onClick = Doc.GetProto(this.Document).onClick = undefined; - }; + }, 'follow link on click'); - @undoBatch deleteClicked = () => this.props.removeDocument?.(this.props.Document); - @undoBatch setToggleDetail = () => - (this.Document.onClick = ScriptField.MakeScript( - `toggleDetail(documentView, "${StrCast(this.Document.layout_fieldKey) - .replace('layout_', '') - .replace(/^layout$/, 'detail')}")`, - { documentView: 'any' } - )); - - @undoBatch - @action - drop = (e: Event, de: DragManager.DropEvent) => { - if (this.props.dontRegisterView || this.props.LayoutTemplateString?.includes(LinkAnchorBox.name)) return false; - if (this.props.Document === Doc.ActiveDashboard) { + noOnClick = undoable(() => { + this.Document.ignoreClick = false; + this.Document.onClick = this.Document[DocData].onClick = undefined; + }, 'default on click'); + + deleteClicked = undoable(() => this._props.removeDocument?.(this.Document), 'delete doc'); + setToggleDetail = undoable( + () => + (this.Document.onClick = 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.Document === Doc.ActiveDashboard) { e.stopPropagation(); e.preventDefault(); alert( @@ -633,16 +499,16 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps const linkdrag = de.complete.annoDragData ?? de.complete.linkDragData; if (linkdrag) { linkdrag.linkSourceDoc = linkdrag.linkSourceGetAnchor(); - if (linkdrag.linkSourceDoc && linkdrag.linkSourceDoc !== this.rootDoc) { + if (linkdrag.linkSourceDoc && linkdrag.linkSourceDoc !== this.Document) { if (de.complete.annoDragData && !de.complete.annoDragData.dropDocument) { de.complete.annoDragData.dropDocument = de.complete.annoDragData.dropDocCreator(undefined); } - if (de.complete.annoDragData || this.rootDoc !== linkdrag.linkSourceDoc.embedContainer) { - const dropDoc = de.complete.annoDragData?.dropDocument ?? this._componentView?.getAnchor?.(true) ?? this.rootDoc; + if (de.complete.annoDragData || this.Document !== linkdrag.linkSourceDoc.embedContainer) { + const dropDoc = de.complete.annoDragData?.dropDocument ?? this._componentView?.getAnchor?.(true) ?? this.Document; de.complete.linkDocument = DocUtils.MakeLink(linkdrag.linkSourceDoc, dropDoc, {}, undefined, [de.x, de.y - 50]); if (de.complete.linkDocument) { de.complete.linkDocument.layout_isSvg = true; - this.props.CollectionFreeFormDocumentView?.().props.CollectionFreeFormView.addDocument(de.complete.linkDocument); + this._docView?.CollectionFreeFormView?.addDocument(de.complete.linkDocument); } } e.stopPropagation(); @@ -650,22 +516,26 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps } } return false; - }; + }, 'drop doc'); - @undoBatch - @action - makeIntoPortal = () => { - const portalLink = this.allLinks.find(d => d.link_anchor_1 === this.props.Document && d.link_relationship === 'portal to:portal from'); + makeIntoPortal = undoable(() => { + const portalLink = this._allLinks.find(d => d.link_anchor_1 === this.Document && d.link_relationship === 'portal to:portal from'); if (!portalLink) { DocUtils.MakeLink( - this.props.Document, - Docs.Create.FreeformDocument([], { _width: NumCast(this.layoutDoc._width) + 10, _height: NumCast(this.layoutDoc._height), _isLightbox: true, _layout_fitWidth: true, title: StrCast(this.props.Document.title) + ' [Portal]' }), + this.Document, + Docs.Create.FreeformDocument([], { + _width: NumCast(this.layoutDoc._width) + 10, + _height: Math.max(NumCast(this.layoutDoc._height), NumCast(this.layoutDoc._width) + 10), + _isLightbox: true, + _layout_fitWidth: true, + title: StrCast(this.Document.title) + ' [Portal]', + }), { link_relationship: 'portal to:portal from' } ); } this.Document.followLinkLocation = OpenWhere.lightbox; this.Document.onClick = FollowLinkScript(); - }; + }, 'make into portal'); importDocument = () => { const input = document.createElement('input'); @@ -676,7 +546,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps const batch = UndoManager.StartBatch('importing'); Doc.importDocument(input.files[0]).then(doc => { if (doc instanceof Doc) { - this.props.addDocTab(doc, OpenWhere.addRight); + this._props.addDocTab(doc, OpenWhere.addRight); batch.end(); } }); @@ -685,12 +555,11 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps input.click(); }; - @action onContextMenu = (e?: React.MouseEvent, pageX?: number, pageY?: number) => { - if (e && this.rootDoc._layout_hideContextMenu && Doc.noviceMode) { + if (e && this.layoutDoc._layout_hideContextMenu && Doc.noviceMode) { e.preventDefault(); e.stopPropagation(); - //!this.props.isSelected(true) && SelectionManager.SelectView(this.props.DocumentView(), false); + //!this._props.isSelected(true) && SelectionManager.SelectView(this.DocumentView(), false); } // the touch onContextMenu is button 0, the pointer onContextMenu is button 2 if (e) { @@ -708,11 +577,11 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps } const cm = ContextMenu.Instance; - if (!cm || (e as any)?.nativeEvent?.SchemaHandled || DocumentView.ExploreMode) return; + if (!cm || (e as any)?.nativeEvent?.SchemaHandled || SnappingManager.ExploreMode) return; if (e && !(e.nativeEvent as any).dash) { const onDisplay = () => { - if (this.rootDoc.type !== DocumentType.MAP) DocumentViewInternal.SelectAfterContextMenu && !this.props.isSelected(true) && SelectionManager.SelectView(this.props.DocumentView(), false); // on a mac, the context menu is triggered on mouse down, but a YouTube video becaomes interactive when selected which means that the context menu won't show up. by delaying the selection until hopefully after the pointer up, the context menu will appear. + if (this.Document.type !== DocumentType.MAP) DocumentViewInternal.SelectAfterContextMenu && this._props.select(false); // on a mac, the context menu is triggered on mouse down, but a YouTube video becaomes interactive when selected which means that the context menu won't show up. by delaying the selection until hopefully after the pointer up, the context menu will appear. setTimeout(() => simulateMouseClick(document.elementFromPoint(e.clientX, e.clientY), e.clientX, e.clientY, e.screenX, e.screenY)); }; if (navigator.userAgent.includes('Macintosh')) { @@ -723,59 +592,56 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps return; } - const customScripts = Cast(this.props.Document.contextMenuScripts, listSpec(ScriptField), []); + const customScripts = Cast(this.Document.contextMenuScripts, listSpec(ScriptField), []); StrListCast(this.Document.contextMenuLabels).forEach((label, i) => - cm.addItem({ description: label, event: () => customScripts[i]?.script.run({ documentView: this, this: this.layoutDoc, scriptContext: this.props.scriptContext, self: this.rootDoc }), icon: 'sticky-note' }) + cm.addItem({ description: label, event: () => customScripts[i]?.script.run({ documentView: this, this: this.Document, scriptContext: this._props.scriptContext }), icon: 'sticky-note' }) ); - this.props - .contextMenuItems?.() - .forEach(item => item.label && cm.addItem({ description: item.label, event: () => item.script.script.run({ this: this.layoutDoc, scriptContext: this.props.scriptContext, self: this.rootDoc }), icon: item.icon as IconProp })); + this._props.contextMenuItems?.().forEach(item => item.label && cm.addItem({ description: item.label, event: () => item.script.script.run({ this: this.Document, scriptContext: this._props.scriptContext }), icon: item.icon as IconProp })); - if (!this.props.Document.isFolder) { - const templateDoc = Cast(this.props.Document[StrCast(this.props.Document.layout_fieldKey)], Doc, null); - const appearance = cm.findByDescription('UI Controls...'); + if (!this.Document.isFolder) { + const templateDoc = Cast(this.Document[StrCast(this.Document.layout_fieldKey)], Doc, null); + const appearance = cm.findByDescription('Appearance...'); const appearanceItems: ContextMenuProps[] = appearance && 'subitems' in appearance ? appearance.subitems : []; - if (this.props.renderDepth === 0) { - appearanceItems.push({ description: 'Open in Lightbox', event: () => LightboxView.SetLightboxDoc(this.rootDoc), icon: 'hand-point-right' }); + if (this._props.renderDepth === 0) { + appearanceItems.splice(0, 0, { description: 'Open in Lightbox', event: () => LightboxView.Instance.SetLightboxDoc(this.Document), icon: 'external-link-alt' }); + } + appearanceItems.push({ description: 'Pin', event: () => this._props.pinToPres(this.Document, {}), icon: 'eye' }); + !Doc.noviceMode && templateDoc && appearanceItems.push({ description: 'Open Template ', event: () => this._props.addDocTab(templateDoc, OpenWhere.addRight), icon: 'eye' }); + !appearance && appearanceItems.length && cm.addItem({ description: 'Appearance...', subitems: appearanceItems, icon: 'compass' }); + + 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: !this.layoutDoc._keepZDragged ? 'Keep ZIndex when dragged' : 'Allow ZIndex to change when dragged', + 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' }); } - !Doc.noviceMode && templateDoc && appearanceItems.push({ description: 'Open Template ', event: () => this.props.addDocTab(templateDoc, OpenWhere.addRight), icon: 'eye' }); - !appearance && appearanceItems.length && cm.addItem({ description: 'UI Controls...', subitems: appearanceItems, icon: 'compass' }); - if (!Doc.IsSystem(this.rootDoc) && this.rootDoc.type !== DocumentType.PRES && ![CollectionViewType.Docking, CollectionViewType.Tree].includes(this.rootDoc._type_collection as any)) { + if (!Doc.IsSystem(this.Document) && !this.Document.hideClickBehaviors && !this._props.hideClickBehaviors) { const existingOnClick = cm.findByDescription('OnClick...'); const onClicks: ContextMenuProps[] = existingOnClick && 'subitems' in existingOnClick ? existingOnClick.subitems : []; - if (this.props.bringToFront !== emptyFunction) { - const zorders = cm.findByDescription('ZOrder...'); - const zorderItems: ContextMenuProps[] = zorders && 'subitems' in zorders ? zorders.subitems : []; - zorderItems.push({ description: 'Bring to Front', event: () => SelectionManager.Views().forEach(dv => dv.props.bringToFront(dv.rootDoc, false)), icon: 'arrow-up' }); - zorderItems.push({ description: 'Send to Back', event: () => SelectionManager.Views().forEach(dv => dv.props.bringToFront(dv.rootDoc, true)), icon: 'arrow-down' }); - zorderItems.push({ - description: !this.rootDoc._keepZDragged ? 'Keep ZIndex when dragged' : 'Allow ZIndex to change when dragged', - event: undoBatch(action(() => (this.rootDoc._keepZWhenDragged = !this.rootDoc._keepZWhenDragged))), - icon: 'hand-point-up', - }); - !zorders && cm.addItem({ description: 'Z Order...', addDivider: true, noexpand: true, subitems: zorderItems, icon: 'layer-group' }); - } - onClicks.push({ description: 'Enter Portal', event: this.makeIntoPortal, icon: 'window-restore' }); !Doc.noviceMode && onClicks.push({ description: 'Toggle Detail', event: this.setToggleDetail, icon: 'concierge-bell' }); - if (!this.props.treeViewDoc) { - if (!this.Document.annotationOn) { - const options = cm.findByDescription('Options...'); - const optionItems: ContextMenuProps[] = options && 'subitems' in options ? options.subitems : []; - !options && cm.addItem({ description: 'Options...', subitems: optionItems, icon: 'compass' }); - - onClicks.push({ description: this.onClickHandler ? 'Remove Click Behavior' : 'Follow Link', event: () => this.toggleFollowLink(false, false), icon: 'link' }); - !Doc.noviceMode && onClicks.push({ description: 'Edit onClick Script', event: () => UndoManager.RunInBatch(() => DocUtils.makeCustomViewClicked(this.props.Document, undefined, 'onClick'), 'edit onClick'), icon: 'terminal' }); - !existingOnClick && cm.addItem({ description: 'OnClick...', noexpand: true, subitems: onClicks, icon: 'mouse-pointer' }); - } else if (LinkManager.Links(this.Document).length) { - onClicks.push({ description: 'Restore On Click default', event: () => this.noOnClick(), icon: 'link' }); - onClicks.push({ description: 'Follow Link on Click', event: () => this.followLinkOnClick(), icon: 'link' }); - !existingOnClick && cm.addItem({ description: 'OnClick...', subitems: onClicks, icon: 'mouse-pointer' }); - } + if (!this.Document.annotationOn) { + const options = cm.findByDescription('Options...'); + const optionItems: ContextMenuProps[] = options && 'subitems' in options ? options.subitems : []; + !options && cm.addItem({ description: 'Options...', subitems: optionItems, icon: 'compass' }); + + onClicks.push({ description: this.onClickHandler ? 'Remove Click Behavior' : 'Follow Link', event: () => this.toggleFollowLink(false, false), icon: 'link' }); + !Doc.noviceMode && onClicks.push({ description: 'Edit onClick Script', event: () => UndoManager.RunInBatch(() => DocUtils.makeCustomViewClicked(this.Document, undefined, 'onClick'), 'edit onClick'), icon: 'terminal' }); + !existingOnClick && cm.addItem({ description: 'OnClick...', noexpand: true, subitems: onClicks, icon: 'mouse-pointer' }); + } else if (LinkManager.Links(this.Document).length) { + onClicks.push({ description: 'Restore On Click default', event: () => this.noOnClick(), icon: 'link' }); + onClicks.push({ description: 'Follow Link on Click', event: () => this.followLinkOnClick(), icon: 'link' }); + !existingOnClick && cm.addItem({ description: 'OnClick...', subitems: onClicks, icon: 'mouse-pointer' }); } } @@ -789,43 +655,43 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps const more = cm.findByDescription('More...'); const moreItems = more && 'subitems' in more ? more.subitems : []; - if (!Doc.IsSystem(this.rootDoc)) { + if (!Doc.IsSystem(this.Document)) { if (!Doc.noviceMode) { - moreItems.push({ description: 'Make View of Metadata Field', event: () => Doc.MakeMetadataFieldTemplate(this.props.Document, this.props.DataDoc), icon: 'concierge-bell' }); + 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.props.Document).data, listSpec(Doc))) { - moreItems.push({ description: 'Export to Google Photos Album', event: () => GooglePhotos.Export.CollectionToAlbum({ collection: this.props.Document }).then(console.log), icon: 'caret-square-right' }); - moreItems.push({ description: 'Tag Child Images via Google Photos', event: () => GooglePhotos.Query.TagChildImages(this.props.Document), icon: 'caret-square-right' }); - moreItems.push({ description: 'Write Back Link to Album', event: () => GooglePhotos.Transactions.AddTextEnrichment(this.props.Document), icon: 'caret-square-right' }); + if (Cast(Doc.GetProto(this.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.props.Document)), icon: 'fingerprint' }); + moreItems.push({ description: 'Copy ID', event: () => Utils.CopyText(Doc.globalServerPath(this.Document)), icon: 'fingerprint' }); } } !more && moreItems.length && cm.addItem({ description: 'More...', subitems: moreItems, icon: 'compass' }); } const constantItems: ContextMenuProps[] = []; - if (!Doc.IsSystem(this.rootDoc) && this.rootDoc._type_collection !== CollectionViewType.Docking) { - constantItems.push({ description: 'Zip Export', icon: 'download', event: async () => Doc.Zip(this.props.Document) }); - (this.rootDoc._type_collection !== CollectionViewType.Docking || !Doc.noviceMode) && constantItems.push({ description: 'Share', event: () => SharingManager.Instance.open(this.props.DocumentView()), icon: 'users' }); - if (this.props.removeDocument && Doc.ActiveDashboard !== this.props.Document) { + 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' }); + 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' }); } } - constantItems.push({ description: 'Show Metadata', event: () => this.props.addDocTab(this.props.Document, (OpenWhere.addRight.toString() + 'KeyValue') as OpenWhere), icon: 'table-columns' }); + constantItems.push({ description: 'Show Metadata', event: () => this._props.addDocTab(this.Document, OpenWhere.addRightKeyvalue), icon: 'table-columns' }); cm.addItem({ description: 'General...', noexpand: false, subitems: constantItems, icon: 'question' }); const help = cm.findByDescription('Help...'); const helpItems: ContextMenuProps[] = help && 'subitems' in help ? help.subitems : []; - !Doc.noviceMode && helpItems.push({ description: 'Text Shortcuts Ctrl+/', event: () => this.props.addDocTab(Docs.Create.PdfDocument('/assets/cheat-sheet.pdf', { _width: 300, _height: 300 }), OpenWhere.addRight), icon: 'keyboard' }); - !Doc.noviceMode && helpItems.push({ description: 'Print Document in Console', event: () => console.log(this.props.Document), icon: 'hand-point-right' }); - !Doc.noviceMode && helpItems.push({ description: 'Print DataDoc in Console', event: () => console.log(this.props.Document[DocData]), icon: 'hand-point-right' }); + !Doc.noviceMode && helpItems.push({ description: 'Text Shortcuts Ctrl+/', event: () => this._props.addDocTab(Docs.Create.PdfDocument('/assets/cheat-sheet.pdf', { _width: 300, _height: 300 }), OpenWhere.addRight), icon: 'keyboard' }); + !Doc.noviceMode && helpItems.push({ description: 'Print Document in Console', event: () => console.log(this.Document), icon: 'hand-point-right' }); + !Doc.noviceMode && helpItems.push({ description: 'Print DataDoc in Console', event: () => console.log(this.dataDoc), icon: 'hand-point-right' }); let documentationDescription: string | undefined = undefined; let documentationLink: string | undefined = undefined; - switch (this.props.Document.type) { + switch (this.Document.type) { case DocumentType.COL: documentationDescription = 'See collection documentation'; documentationLink = 'https://brown-dash.github.io/Dash-Documentation/views/'; @@ -859,8 +725,8 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps documentationLink = 'https://brown-dash.github.io/Dash-Documentation/documents/dataViz/'; break; } - // Add link to help documentation - if (!this.props.treeViewDoc && documentationDescription && documentationLink) { + // 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) { helpItems.push({ description: documentationDescription, event: () => window.open(documentationLink, '_blank'), @@ -871,119 +737,47 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps else cm.moveAfter(help); e?.stopPropagation(); // DocumentViews should stop propagation of this event - cm.displayMenu((e?.pageX || pageX || 0) - 15, (e?.pageY || pageY || 0) - 15); + cm.displayMenu((e?.pageX || pageX || 0) - 15, (e?.pageY || pageY || 0) - 15, undefined, undefined, undefined); }; - @computed get _rootSelected() { - return this.props.isSelected(false) || (this.props.Document.rootDocument && this.props.rootSelected?.(false)) || false; - } - rootSelected = (outsideReaction?: boolean) => this._rootSelected; - panelHeight = () => this.props.PanelHeight() - this.headerMargin; - screenToLocal = () => this.props.ScreenToLocalTransform().translate(0, -this.headerMargin); - onClickFunc: any = () => (this.disableClickScriptFunc ? undefined : this.onClickHandler); - setHeight = (height: number) => (this.layoutDoc._height = height); - setContentView = action((view: { getAnchor?: (addAsAnnotation: boolean) => Doc; forward?: () => boolean; back?: () => boolean }) => (this._componentView = view)); - @computed get _isContentActive() { - // true - if the document has been activated directly or indirectly (by having its children selected) - // false - if its pointer events are explicitly turned off or if it's container tells it that it's inactive - // undefined - it is not active, but it should be responsive to actions that might active it or its contents (eg clicking) - return this.props.isContentActive() === false || this.props.pointerEvents?.() === 'none' - ? false - : Doc.ActiveTool !== InkTool.None || SnappingManager.GetIsDragging() || this.rootSelected() || this.rootDoc.forceActive || this._componentView?.isAnyChildContentActive?.() || this.props.isContentActive() - ? true - : undefined; - } + rootSelected = () => this._rootSelected; + 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 = height); + setContentView = action((view: ViewBoxInterface) => (this._componentView = view)); isContentActive = (): boolean | undefined => this._isContentActive; - childFilters = () => [...this.props.childFilters(), ...StrListCast(this.layoutDoc.childFilters)]; + childFilters = () => [...this._props.childFilters(), ...StrListCast(this.layoutDoc.childFilters)]; - /// disable pointer events on content when there's an enabled onClick script (but not the browse script) and the contents aren't forced active, or if contents are marked inactive - @computed get _contentPointerEvents() { - if (this.props.contentPointerEvents) return this.props.contentPointerEvents; - return (!this.disableClickScriptFunc && this.onClickHandler && !this.props.onBrowseClick?.() && this.isContentActive() !== true) || this.isContentActive() === false ? 'none' : this.pointerEvents; - } contentPointerEvents = () => this._contentPointerEvents; - @computed get contents() { - TraceMobx(); - const isInk = this.layoutDoc._layout_isSvg && !this.props.LayoutTemplateString; - const noBackground = this.rootDoc._isGroup && (!this.rootDoc.backgroundColor || this.rootDoc.backgroundColor === 'transparent'); - return ( - <div - className="documentView-contentsView" - style={{ - pointerEvents: (isInk || noBackground ? 'none' : this.contentPointerEvents()) ?? 'all', - height: this.headerMargin ? `calc(100% - ${this.headerMargin}px)` : undefined, - }}> - <DocumentContentsView - key={1} - {...this.props} - pointerEvents={this.contentPointerEvents} - docViewPath={this.props.viewPath} - setContentView={this.setContentView} - childFilters={this.childFilters} - NativeDimScaling={this.props.NativeDimScaling} - PanelHeight={this.panelHeight} - setHeight={!this.props.suppressSetHeight ? this.setHeight : undefined} - isContentActive={this.isContentActive} - ScreenToLocalTransform={this.screenToLocal} - rootSelected={this.rootSelected} - onClick={this.onClickFunc} - focus={this.props.focus} - setTitleFocus={this.setTitleFocus} - layout_fieldKey={this.finalLayoutKey} - /> - {this.layoutDoc.layout_hideAllLinks ? null : this.allLinkEndpoints} - </div> - ); - } - anchorPanelWidth = () => this.props.PanelWidth() || 1; - anchorPanelHeight = () => this.props.PanelHeight() || 1; - anchorStyleProvider = (doc: Opt<Doc>, props: Opt<DocumentViewProps>, property: string): any => { + anchorPanelWidth = () => this._props.PanelWidth() || 1; + anchorPanelHeight = () => this._props.PanelHeight() || 1; + anchorStyleProvider = (doc: Opt<Doc>, props: Opt<FieldViewProps>, property: string): any => { // prettier-ignore switch (property.split(':')[0]) { case StyleProp.ShowTitle: return ''; case StyleProp.PointerEvents: return 'none'; case StyleProp.Highlighting: return undefined; case StyleProp.Opacity: { - const filtered = DocUtils.FilterDocs(this.directLinks, this.props.childFilters?.() ?? [], []).filter(d => d.link_displayLine || Doc.UserDoc().showLinkLines); + const filtered = DocUtils.FilterDocs(this.directLinks, this._props.childFilters?.() ?? [], []).filter(d => d.link_displayLine || Doc.UserDoc().showLinkLines); return filtered.some(link => link._link_displayArrow) ? 0 : undefined; } } - return this.props.styleProvider?.(doc, props, property); + return this._props.styleProvider?.(doc, props, property); }; - // We need to use allrelatedLinks to get not just links to the document as a whole, but links to - // anchors that are not rendered as DocumentViews (marked as 'layout_unrendered' with their 'annotationOn' set to this document). e.g., - // - 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. - @computed get directLinks() { - TraceMobx(); - return LinkManager.Instance.getAllRelatedLinks(this.rootDoc).filter( - link => - (link.link_matchEmbeddings ? link.link_anchor_1 === this.rootDoc : Doc.AreProtosEqual(link.link_anchor_1 as Doc, this.rootDoc)) || - (link.link_matchEmbeddings ? link.link_anchor_2 === this.rootDoc : Doc.AreProtosEqual(link.link_anchor_2 as Doc, this.rootDoc)) || - ((link.link_anchor_1 as Doc)?.layout_unrendered && Doc.AreProtosEqual((link.link_anchor_1 as Doc)?.annotationOn as Doc, this.rootDoc)) || - ((link.link_anchor_2 as Doc)?.layout_unrendered && Doc.AreProtosEqual((link.link_anchor_2 as Doc)?.annotationOn as Doc, this.rootDoc)) - ); - } - @computed get allLinks() { - TraceMobx(); - return LinkManager.Instance.getAllRelatedLinks(this.rootDoc); - } - hideLink = computedFn((link: Doc) => () => (link.link_displayLine = false)); - @computed get allLinkEndpoints() { + + removeLinkByHiding = (link: Doc) => () => (link.link_displayLine = false); + allLinkEndpoints = () => { // the small blue dots that mark the endpoints of links - TraceMobx(); - if (this._componentView instanceof KeyValueBox || this.props.hideLinkAnchors || this.layoutDoc.layout_hideLinkAnchors || this.props.dontRegisterView || this.layoutDoc.layout_unrendered) return null; - const filtered = DocUtils.FilterDocs(this.directLinks, this.props.childFilters?.() ?? [], []).filter(d => d.link_displayLine || Doc.UserDoc().showLinkLines); - return filtered.map(link => ( + 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} + {...this._props} isContentActive={returnFalse} Document={link} - docViewPath={this.props.viewPath} + containerViewPath={this._props.docViewPath} PanelWidth={this.anchorPanelWidth} PanelHeight={this.anchorPanelHeight} dontRegisterView={false} @@ -991,107 +785,55 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps hideCaptions={true} hideLinkAnchors={true} layout_fitWidth={returnTrue} - removeDocument={this.hideLink(link)} + removeDocument={this.removeLinkByHiding(link)} styleProvider={this.anchorStyleProvider} LayoutTemplate={undefined} - LayoutTemplateString={LinkAnchorBox.LayoutString(`link_anchor_${Doc.LinkEndpoint(link, this.rootDoc)}`)} + LayoutTemplateString={LinkAnchorBox.LayoutString(`link_anchor_${LinkManager.anchorIndex(link, this.Document)}`)} /> </div> )); - } - - 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 === undefined) { - dataDoc[field + '_audioAnnotations'] = new List([audioField]); - } else { - audioAnnos.push(audioField); - } - } - }; - //runInAction(() => (dataDoc.audioAnnoState = 'recording')); - recorder.start(); - const stopFunc = () => { - recorder.stop(); - DictationManager.Controls.stop(false); - runInAction(() => (dataDoc.audioAnnoState = 'stopped')); - gumStream.getAudioTracks()[0].stop(); - }; - if (onRecording) onRecording(stopFunc); - else setTimeout(stopFunc, 5000); - }); - } - playAnnotation = () => { - const self = this; - const audioAnnoState = this.dataDoc.audioAnnoState ?? 'stopped'; - const audioAnnos = Cast(this.dataDoc[this.LayoutFieldKey + '_audioAnnotations'], listSpec(AudioField), null); - const anno = audioAnnos?.lastElement(); - if (anno instanceof AudioField) { - switch (audioAnnoState) { - case 'stopped': - this.dataDoc[AudioPlay] = new Howl({ - src: [anno.url.href], - format: ['mp3'], - autoplay: true, - loop: false, - volume: 0.5, - onend: action(() => (self.dataDoc.audioAnnoState = 'stopped')), - }); - this.dataDoc.audioAnnoState = 'playing'; - break; - case 'playing': - this.dataDoc[AudioPlay]?.stop(); - this.dataDoc.audioAnnoState = 'stopped'; - break; - } - } + 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'); + return ( + <div + className="documentView-contentsView" + style={{ + pointerEvents: (isInk || noBackground ? 'none' : this.contentPointerEvents()) ?? (this._mounted ? 'all' : 'none'), + height: this.headerMargin ? `calc(100% - ${this.headerMargin}px)` : undefined, + }}> + <DocumentContentsView + {...this._props} + layoutFieldKey={StrCast(this.Document.layout_fieldKey, 'layout')} + pointerEvents={this.contentPointerEvents} + setContentViewBox={this.setContentView} + childFilters={this.childFilters} + PanelHeight={this.panelHeight} + setHeight={this.setHeight} + isContentActive={this.isContentActive} + ScreenToLocalTransform={this.screenToLocalContent} + rootSelected={this.rootSelected} + onClickScript={this.onClickFunc} + setTitleFocus={this.setTitleFocus} + hideClickBehaviors={BoolCast(this.layoutDoc.hideClickBehaviors)} + /> + {this.layoutDoc.layout_hideAllLinks ? null : this.allLinkEndpoints()} + </div> + ); }; - captionStyleProvider = (doc: Opt<Doc>, props: Opt<DocumentViewProps>, property: string) => this.props?.styleProvider?.(doc, props, property + ':caption'); - @observable _changingTitleField = false; - @observable _dropDownInnerWidth = 0; - fieldsDropdown = (inputOptions: string[], dropdownWidth: number, placeholder: string, onChange: (val: string | number) => void, onClose: () => void) => { - const filteredOptions = new Set(inputOptions); - const scaling = this.titleHeight / 30; /* height of Dropdown */ - Object.entries(DocOptions) - .filter(opts => opts[1].filterable) - .forEach((pair: [string, FInfo]) => filteredOptions.add(pair[0])); - filteredOptions.add(StrCast(this.layoutDoc.layout_showTitle)); - const options = Array.from(filteredOptions) - .filter(f => f) - .map(facet => ({ val: facet, text: facet })); + captionStyleProvider = (doc: Opt<Doc>, props: Opt<FieldViewProps>, property: string) => this._props?.styleProvider?.(doc, props, property + ':caption'); + fieldsDropdown = (reqdFields: string[], dropdownWidth: number, placeholder: string, onChange: (val: string | number) => void, onClose: () => void) => { + const filteredFields = Object.entries(DocOptions).reduce((set, [field, opts]) => (opts.filterable ? set.add(field) : set), new Set(reqdFields)); return ( <div style={{ width: dropdownWidth }}> <div - ref={action((r: any) => r && (this._dropDownInnerWidth = Number(getComputedStyle(r).width.replace('px', ''))))} + ref={action((r: any) => r && (this._titleDropDownInnerWidth = Number(getComputedStyle(r).width.replace('px', ''))))} onPointerDown={action(e => (this._changingTitleField = true))} - style={{ width: 'max-content', transformOrigin: 'left', transform: `scale(${scaling})` }}> + style={{ width: 'max-content', transformOrigin: 'left', transform: `scale(${this.titleHeight / 30 /* height of Dropdown */})` }}> <Dropdown activeChanged={action(isOpen => !isOpen && (this._changingTitleField = false))} selectedVal={placeholder} @@ -1101,7 +843,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps type={Type.TERT} closeOnSelect={true} dropdownType={DropdownType.SELECT} - items={options} + items={Array.from(filteredFields).map(facet => ({ val: facet, text: facet }))} width={100} fillWidth /> @@ -1109,64 +851,46 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps </div> ); }; - @computed get innards() { - TraceMobx(); + /** + * 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[;field2[...][:hover]] + * from the UI, this is done by clicking the title field and prefixin the format with '#'. eg., #field1[;field2;...][:hover] + **/ + titleView = () => { const showTitle = this.layout_showTitle?.split(':')[0]; const showTitleHover = this.layout_showTitle?.includes(':hover'); - const captionView = !this.layout_showCaption ? null : ( - <div - className="documentView-captionWrapper" - style={{ - pointerEvents: this.rootDoc.ignoreClick ? 'none' : this.isContentActive() || this.props.isDocumentActive?.() ? 'all' : undefined, - background: StrCast(this.layoutDoc._backgroundColor, 'rgba(0,0,0,0.2)'), - color: lightOrDark(StrCast(this.layoutDoc._backgroundColor, 'black')), - }}> - <FormattedTextBox - {...this.props} - yPadding={10} - xPadding={10} - fieldKey={this.layout_showCaption} - styleProvider={this.captionStyleProvider} - dontRegisterView={true} - noSidebar={true} - dontScale={true} - renderDepth={this.props.renderDepth} - isContentActive={this.isContentActive} - /> - </div> - ); - const targetDoc = showTitle?.startsWith('_') ? this.layoutDoc : this.rootDoc; + + 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(this.layoutDoc.layout_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, SettingsManager.userBackgroundColor)) ); - const dropdownWidth = this._titleRef.current?._editing || this._changingTitleField ? Math.max(10, (this._dropDownInnerWidth * this.titleHeight) / 30) : 0; + 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('%', ''); - // 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[;field2[...][:hover]] - // from the UI, this is done by clicking the title field and prefixin the format with '#'. eg., #field1[;field2;...][:hover] - const titleView = !showTitle ? null : ( + + return !showTitle ? null : ( <div className={`documentView-titleWrapper${showTitleHover ? '-hover' : ''}`} key="title" style={{ + zIndex: 1, position: this.headerMargin ? 'relative' : 'absolute', height: this.titleHeight, width: 100 - sidebarWidthPercent + '%', color: background === 'transparent' ? SettingsManager.userColor : lightOrDark(background), background, - pointerEvents: (!this.disableClickScriptFunc && this.onClickHandler) || this.Document.ignoreClick ? 'none' : this.isContentActive() || this.props.isDocumentActive?.() ? 'all' : undefined, + pointerEvents: (!this.disableClickScriptFunc && this.onClickHandler) || this.Document.ignoreClick ? 'none' : this.isContentActive() || this._props.isDocumentActive?.() ? 'all' : undefined, }}> {!dropdownWidth ? null : this.fieldsDropdown( - [], + [StrCast(this.layoutDoc.layout_showTitle)], dropdownWidth, StrCast(this.layoutDoc.layout_showTitle).split(':')[0], action((field: string | number) => { - if (this.rootDoc.layout_showTitle) { - this.rootDoc._layout_showTitle = field; - } else if (!this.props.layout_showTitle) { + if (this.layoutDoc.layout_showTitle) { + this.layoutDoc._layout_showTitle = field; + } else if (!this._props.layout_showTitle) { Doc.UserDoc().layout_showTitle = field; } this._changingTitleField = false; @@ -1189,12 +913,12 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps display="block" oneLine={true} fontSize={(this.titleHeight / 15) * 10} - GetValue={() => (showTitle.split(';').length !== 1 ? '#' + showTitle : Field.toKeyValueString(this.rootDoc, showTitle.split(';')[0]))} + GetValue={() => (showTitle.split(';').length !== 1 ? '#' + showTitle : Field.toKeyValueString(this.Document, showTitle.split(';')[0]))} SetValue={undoBatch((input: string) => { if (input?.startsWith('#')) { - if (this.rootDoc.layout_showTitle) { - this.rootDoc._layout_showTitle = input?.substring(1); - } else if (!this.props.layout_showTitle) { + if (this.layoutDoc.layout_showTitle) { + this.layoutDoc._layout_showTitle = input?.substring(1); + } else if (!this._props.layout_showTitle) { Doc.UserDoc().layout_showTitle = input?.substring(1) ?? 'author_date'; } } else if (showTitle && !showTitle.includes('Date') && showTitle !== 'author') { @@ -1206,22 +930,37 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps </div> </div> ); - return this.props.hideTitle || (!showTitle && !this.layout_showCaption) ? ( - this.contents - ) : ( - <div className="documentView-styleWrapper"> - {' '} - {!this.headerMargin ? this.contents : titleView} - {!this.headerMargin ? titleView : this.contents} - {' ' /* */} - {captionView} + }; + + captionView = () => { + return !this.layout_showCaption ? null : ( + <div + className="documentView-captionWrapper" + style={{ + pointerEvents: this.Document.ignoreClick ? 'none' : this.isContentActive() || this._props.isDocumentActive?.() ? 'all' : undefined, + background: StrCast(this.layoutDoc._backgroundColor, 'rgba(0,0,0,0.2)'), + color: lightOrDark(StrCast(this.layoutDoc._backgroundColor, 'black')), + }}> + <FormattedTextBox + {...this._props} + yPadding={10} + xPadding={10} + fieldKey={this.layout_showCaption} + styleProvider={this.captionStyleProvider} + dontRegisterView={true} + noSidebar={true} + dontScale={true} + renderDepth={this._props.renderDepth} + isContentActive={this.isContentActive} + /> </div> ); - } + }; renderDoc = (style: object) => { TraceMobx(); - return !DocCast(this.Document) || GetEffectiveAcl(this.Document[DocData]) === AclPrivate + const showTitle = this.layout_showTitle?.split(':')[0]; + return !DocCast(this.Document) || GetEffectiveAcl(this.dataDoc) === AclPrivate ? null : this.docContents ?? ( <div @@ -1236,14 +975,61 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps fontFamily: StrCast(this.Document._text_fontFamily, 'inherit'), fontSize: Cast(this.Document._text_fontSize, 'string', null), transform: this._animateScalingTo ? `scale(${this._animateScalingTo})` : undefined, - transition: !this._animateScalingTo ? StrCast(this.Document.dataTransition) : `transform ${this.animateScaleTime / 1000}s ease-${this._animateScalingTo < 1 ? 'in' : 'out'}`, + transition: !this._animateScalingTo ? StrCast(this.Document.dataTransition) : `transform ${this.animateScaleTime() / 1000}s ease-${this._animateScalingTo < 1 ? 'in' : 'out'}`, }}> - {this.innards} + {this._props.hideTitle || (!showTitle && !this.layout_showCaption) ? ( + this.viewBoxContents() + ) : ( + <div className="documentView-styleWrapper"> + {this.titleView()} + {this.viewBoxContents()} + {this.captionView()} + </div> + )} {this.widgetDecorations ?? null} </div> ); }; + render() { + TraceMobx(); + const highlighting = this.highlighting; + const borderPath = this.borderPath; + const boxShadow = !highlighting + ? this.boxShadow + : highlighting && this.borderRounding && highlighting.highlightStyle !== 'dashed' + ? `0 0 0 ${highlighting.highlightIndex}px ${highlighting.highlightColor}` + : this.boxShadow || (this.Document.isTemplateForField ? 'black 0.2vw 0.2vw 0.8vw' : undefined); + const renderDoc = this.renderDoc({ + borderRadius: this.borderRounding, + outline: highlighting && !this.borderRounding && !highlighting.highlightStroke ? `${highlighting.highlightColor} ${highlighting.highlightStyle} ${highlighting.highlightIndex}px` : 'solid 0px', + border: highlighting && this.borderRounding && highlighting.highlightStyle === 'dashed' ? `${highlighting.highlightStyle} ${highlighting.highlightColor} ${highlighting.highlightIndex}px` : undefined, + boxShadow, + clipPath: borderPath?.clipPath, + }); + + return ( + <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)} + 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} + </> + </div> + ); + } + /** * returns an entrance animation effect function to wrap a JSX element * @param presEffectDoc presentation effects document that specifies the animation effect parameters @@ -1270,250 +1056,221 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps case PresEffect.Rotate: return <Rotate {...effectProps}>{renderDoc}</Rotate>; case PresEffect.Bounce: return <Bounce {...effectProps}>{renderDoc}</Bounce>; case PresEffect.Roll: return <Roll {...effectProps}>{renderDoc}</Roll>; - case PresEffect.Lightspeed: return <LightSpeed {...effectProps}>{renderDoc}</LightSpeed>; + case PresEffect.Lightspeed: return <JackInTheBox {...effectProps}>{renderDoc}</JackInTheBox>; } } - @computed get highlighting() { - return this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.Highlighting); - } - @computed get borderPath() { - return this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.BorderPath); - } - render() { - TraceMobx(); - const highlighting = this.highlighting; - const borderPath = this.borderPath; - const boxShadow = - this.props.treeViewDoc || !highlighting - ? this.boxShadow - : highlighting && this.borderRounding && highlighting.highlightStyle !== 'dashed' - ? `0 0 0 ${highlighting.highlightIndex}px ${highlighting.highlightColor}` - : this.boxShadow || (this.rootDoc.isTemplateForField ? 'black 0.2vw 0.2vw 0.8vw' : undefined); - const renderDoc = this.renderDoc({ - borderRadius: this.borderRounding, - outline: highlighting && !this.borderRounding && !highlighting.highlightStroke ? `${highlighting.highlightColor} ${highlighting.highlightStyle} ${highlighting.highlightIndex}px` : 'solid 0px', - border: highlighting && this.borderRounding && highlighting.highlightStyle === 'dashed' ? `${highlighting.highlightStyle} ${highlighting.highlightColor} ${highlighting.highlightIndex}px` : undefined, - boxShadow, - clipPath: borderPath?.clipPath, - }); + 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?.(); + }); - return ( - <div - className={`${DocumentView.ROOT_DIV} docView-hack`} - ref={this._mainCont} - onContextMenu={this.onContextMenu} - onPointerDown={this.onPointerDown} - onClick={this.onClick} - onPointerEnter={e => (!SnappingManager.GetIsDragging() || DragManager.CanEmbed) && Doc.BrushDoc(this.rootDoc)} - onPointerOver={e => (!SnappingManager.GetIsDragging() || DragManager.CanEmbed) && Doc.BrushDoc(this.rootDoc)} - onPointerLeave={e => !isParentOf(this.ContentDiv, document.elementFromPoint(e.nativeEvent.x, e.nativeEvent.y)) && Doc.UnBrushDoc(this.rootDoc)} - 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.rootDoc[Animation], this.rootDoc)} - {borderPath?.jsx} - </> - </div> - ); + 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 === undefined) { + dataDoc[field + '_audioAnnotations'] = new List([audioField]); + } else { + audioAnnos.push(audioField); + } + } + }; + //runInAction(() => (dataDoc.audioAnnoState = 'recording')); + recorder.start(); + const stopFunc = () => { + recorder.stop(); + DictationManager.Controls.stop(false); + runInAction(() => (dataDoc.audioAnnoState = 'stopped')); + gumStream.getAudioTracks()[0].stop(); + }; + if (onRecording) onRecording(stopFunc); + else setTimeout(stopFunc, 5000); + }); } } @observer -export class DocumentView extends React.Component<DocumentViewProps> { +export class DocumentView extends DocComponent<DocumentViewProps>() { public static ROOT_DIV = 'documentView-effectsWrapper'; - @observable public static Interacting = false; - @observable public static LongPress = false; - @observable public static ExploreMode = false; - @observable public static LastPressedSidebarBtn: Opt<Doc>; // bcz: this is a hack to handle highlighting buttons in the leftpanel menu .. need to find a cleaner approach - @computed public static get exploreMode() { - return () => (DocumentView.ExploreMode ? ScriptField.MakeScript('CollectionBrowseClick(documentView, clientX, clientY)', { documentView: 'any', clientX: 'number', clientY: 'number' })! : undefined); - } - @observable public docView: DocumentViewInternal | undefined | null; - @observable public textHtmlOverlay: Opt<string>; - @observable private _isHovering = false; - - public htmlOverlayEffect = ''; - public get displayName() { - return 'DocumentView(' + this.props.Document?.title + ')'; - } // this makes mobx trace() statements more descriptive + public get displayName() { return 'DocumentView(' + this.Document?.title + ')'; } // prettier-ignore public ContentRef = React.createRef<HTMLDivElement>(); - public ViewTimer: NodeJS.Timeout | undefined; // timer for res - public AnimEffectTimer: NodeJS.Timeout | undefined; // timer for res + private _htmlOverlayEffect: Opt<Doc>; private _disposers: { [name: string]: IReactionDisposer } = {}; - public clearViewTransition = () => { - this.ViewTimer && clearTimeout(this.ViewTimer); - this.rootDoc._viewTransition = undefined; - }; - public startDragging = (x: number, y: number, dropAction: dropActionType, hideSource = false) => this.docView?.startDragging(x, y, dropAction, hideSource); + private _viewTimer: NodeJS.Timeout | undefined; + private _animEffectTimer: NodeJS.Timeout | undefined; - public showContextMenu = (pageX: number, pageY: number) => this.docView?.onContextMenu(undefined, pageX, pageY); - - public setAnimEffect = (presEffect: Doc, timeInMs: number, afterTrans?: () => void) => { - this.AnimEffectTimer && clearTimeout(this.AnimEffectTimer); - this.rootDoc[Animation] = presEffect; - this.AnimEffectTimer = setTimeout(() => (this.rootDoc[Animation] = undefined), timeInMs); - }; - public setViewTransition = (transProp: string, timeInMs: number, afterTrans?: () => void, dataTrans = false) => { - this.rootDoc._viewTransition = `${transProp} ${timeInMs}ms`; - if (dataTrans) this.rootDoc._dataTransition = `${transProp} ${timeInMs}ms`; - this.ViewTimer && clearTimeout(this.ViewTimer); - return (this.ViewTimer = setTimeout(() => { - this.rootDoc._viewTransition = undefined; - this.rootDoc._dataTransition = 'inherit'; - afterTrans?.(); - }, timeInMs + 10)); - }; - public static SetViewTransition(docs: Doc[], transProp: string, timeInMs: number, afterTrans?: () => void, dataTrans = false) { - docs.forEach(doc => { - doc._viewTransition = `${transProp} ${timeInMs}ms`; - dataTrans && (doc.dataTransition = `${transProp} ${timeInMs}ms`); - }); - return setTimeout( - () => - docs.forEach(doc => { - doc._viewTransition = undefined; - dataTrans && (doc.dataTransition = 'inherit'); - afterTrans?.(); - }), - timeInMs + 10 - ); + @computed public static get exploreMode() { + return () => (SnappingManager.ExploreMode ? ScriptField.MakeScript('CollectionBrowseClick(documentView, clientX, clientY)', { documentView: 'any', clientX: 'number', clientY: 'number' })! : undefined); } - // 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'; - // prettier-ignore - DocServer.GetRefField(docId).then(docx => - LightboxView.SetLightboxDoc( - (docx as Doc) ?? // reuse existing pivot view of documents, or else create a new collection - Docs.Create.StackingDocument([], { title: linkAnchor.title + '-pivot', _width: 500, _height: 500, target: linkAnchor, updateContentsScript: ScriptField.MakeScript('updateLinkCollection(self, self.target)') }, docId) - ) - ); + constructor(props: DocumentViewProps) { + super(props); + makeObservable(this); } - get Document() { - return this.props.Document; - } - get topMost() { - return this.props.renderDepth === 0; - } - get rootDoc() { - return this.docView?.rootDoc ?? this.Document; - } - get dataDoc() { - return this.docView?.dataDoc ?? this.Document; - } - get ContentDiv() { - return this.docView?.ContentDiv; - } - get ComponentView() { - return this.docView?._componentView; - } - get allLinks() { - return (this.docView?.allLinks || []).filter(link => !link.link_matchEmbeddings || link.link_anchor_1 === this.rootDoc || link.link_anchor_2 === this.rootDoc); - } - get LayoutFieldKey() { - return this.docView?.LayoutFieldKey || 'layout'; - } - @computed get layout_fitWidth() { - return this.docView?._componentView?.layout_fitWidth?.() ?? this.props.layout_fitWidth?.(this.rootDoc) ?? this.layoutDoc?.layout_fitWidth; - } - @computed get anchorViewDoc() { - return this.props.LayoutTemplateString?.includes('link_anchor_2') ? DocCast(this.rootDoc['link_anchor_2']) : this.props.LayoutTemplateString?.includes('link_anchor_1') ? DocCast(this.rootDoc['link_anchor_1']) : undefined; - } - @computed get hideLinkButton() { - return this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.HideLinkBtn + (this.isSelected() ? ':selected' : '')); - } - @computed get linkCountView() { - const hideCount = this.props.renderDepth === -1 || SnappingManager.GetIsDragging() || (this.isSelected() && this.props.renderDepth) || !this._isHovering || this.hideLinkButton; - return hideCount ? null : <DocumentLinksButton View={this} scaling={this.scaleToScreenSpace} OnHover={true} Bottom={this.topMost} ShowCount={true} />; - } - @computed get docViewPath(): DocumentView[] { - return this.props.docViewPath ? [...this.props.docViewPath(), this] : [this]; - } - @computed get layoutDoc() { - return Doc.Layout(this.Document, this.props.LayoutTemplate?.()); - } - @computed get nativeWidth() { - return this.docView?._componentView?.reverseNativeScaling?.() ? 0 : returnVal(this.props.NativeWidth?.(), Doc.NativeWidth(this.layoutDoc, this.props.DataDoc, !this.layout_fitWidth)); - } - @computed get nativeHeight() { - return this.docView?._componentView?.reverseNativeScaling?.() ? 0 : returnVal(this.props.NativeHeight?.(), Doc.NativeHeight(this.layoutDoc, this.props.DataDoc, !this.layout_fitWidth)); - } - @computed get shouldNotScale() { - return this.props.shouldNotScale?.() || (this.layout_fitWidth && !this.nativeWidth) || [CollectionViewType.Docking].includes(this.Document._type_collection as any); + // want the htmloverlay to be able to fade in but we also want it to be display 'none' until it is needed. + // unfortunately, CSS can't transition animate any properties for something that is display 'none'. + // so we need to first activate the div, then, after a render timeout, start the opacity transition. + @observable private _enableHtmlOverlayTransitions: boolean = false; + @observable private _docViewInternal: DocumentViewInternal | undefined | null = undefined; + @observable private _htmlOverlayText: Opt<string> = undefined; + @observable private _isHovering = false; + @observable private _selected = false; + @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); } - @computed get effectiveNativeWidth() { + @computed private get effectiveNativeWidth() { return this.shouldNotScale ? 0 : this.nativeWidth || NumCast(this.layoutDoc.width); } - @computed get effectiveNativeHeight() { + @computed private get effectiveNativeHeight() { return this.shouldNotScale ? 0 : this.nativeHeight || NumCast(this.layoutDoc.height); } - @computed get nativeScaling() { + @computed private get nativeScaling() { if (this.shouldNotScale) return 1; const minTextScale = this.Document.type === DocumentType.RTF ? 0.1 : 0; - if (this.layout_fitWidth || this.props.PanelHeight() / (this.effectiveNativeHeight || 1) > this.props.PanelWidth() / (this.effectiveNativeWidth || 1)) { - return Math.max(minTextScale, this.props.PanelWidth() / (this.effectiveNativeWidth || 1)); // width-limited or layout_fitWidth + if (this.layout_fitWidth || this._props.PanelHeight() / (this.effectiveNativeHeight || 1) > this._props.PanelWidth() / (this.effectiveNativeWidth || 1)) { + return Math.max(minTextScale, this._props.PanelWidth() / (this.effectiveNativeWidth || 1)); // width-limited or layout_fitWidth } - return Math.max(minTextScale, this.props.PanelHeight() / (this.effectiveNativeHeight || 1)); // height-limited or unscaled + return Math.max(minTextScale, this._props.PanelHeight() / (this.effectiveNativeHeight || 1)); // height-limited or unscaled } - @computed get panelWidth() { - return this.effectiveNativeWidth ? this.effectiveNativeWidth * this.nativeScaling : this.props.PanelWidth(); + @computed private get panelWidth() { + return this.effectiveNativeWidth ? this.effectiveNativeWidth * this.nativeScaling : this._props.PanelWidth(); } - @computed get panelHeight() { - if (this.effectiveNativeHeight && (!this.layout_fitWidth || !this.layoutDoc.nativeHeightUnfrozen)) { - return Math.min(this.props.PanelHeight(), this.effectiveNativeHeight * this.nativeScaling); + @computed private get panelHeight() { + if (this.effectiveNativeHeight && (!this.layout_fitWidth || !this.layoutDoc.layout_reflowVertical)) { + return Math.min(this._props.PanelHeight(), this.effectiveNativeHeight * this.nativeScaling); } - return this.props.PanelHeight(); + return this._props.PanelHeight(); } - @computed get Xshift() { - return this.effectiveNativeWidth ? Math.max(0, (this.props.PanelWidth() - this.effectiveNativeWidth * this.nativeScaling) / 2) : 0; + @computed private get Xshift() { + return this.effectiveNativeWidth ? Math.max(0, (this._props.PanelWidth() - this.effectiveNativeWidth * this.nativeScaling) / 2) : 0; } - @computed get Yshift() { + @computed private get Yshift() { return this.effectiveNativeWidth && this.effectiveNativeHeight && Math.abs(this.Xshift) < 0.001 && - (!this.layoutDoc.nativeHeightUnfrozen || (!this.layout_fitWidth && this.effectiveNativeHeight * this.nativeScaling <= this.props.PanelHeight())) - ? Math.max(0, (this.props.PanelHeight() - this.effectiveNativeHeight * this.nativeScaling) / 2) + (!this.layoutDoc.layout_reflowVertical || (!this.layout_fitWidth && this.effectiveNativeHeight * this.nativeScaling <= this._props.PanelHeight())) + ? Math.max(0, (this._props.PanelHeight() - this.effectiveNativeHeight * this.nativeScaling) / 2) : 0; } - @computed get centeringX() { - return this.props.dontCenter?.includes('x') ? 0 : this.Xshift; + @computed private get hideLinkButton() { + return ( + this._props.hideLinkButton || + this._props.renderDepth === -1 || // + (this.IsSelected && this._props.renderDepth) || + !this._isHovering || + (!this.IsSelected && this.layoutDoc.layout_hideLinkButton) || + SnappingManager.IsDragging || + SnappingManager.IsResizing + ); } - @computed get centeringY() { - return this.props.dontCenter?.includes('y') ? 0 : this.Yshift; + + 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); + } + + componentWillUnmount() { + runInAction(() => this.Document[DocViews].delete(this)); + Object.values(this._disposers).forEach(disposer => disposer?.()); + !BoolCast(this.Document.dontRegisterView, this._props.dontRegisterView) && DocumentManager.Instance.RemoveView(this); } - public toggleNativeDimensions = () => this.docView && this.rootDoc.type !== DocumentType.INK && Doc.toggleNativeDimensions(this.layoutDoc, this.docView.NativeDimScaling, this.props.PanelWidth(), this.props.PanelHeight()); - public getBounds = () => { - if (!this.docView?.ContentDiv || this.props.treeViewDoc || Doc.AreProtosEqual(this.props.Document, Doc.UserDoc())) { + 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 + public get ComponentView() { return this._docViewInternal?._componentView; } // prettier-ignore + public get allLinks() { return this._docViewInternal?._allLinks ?? []; } // prettier-ignore + + get LayoutFieldKey() { + return Doc.LayoutFieldKey(this.Document, this._props.LayoutTemplateString); + } + + @computed get layout_fitWidth() { + return this._props.layout_fitWidth?.(this.layoutDoc) ?? this.layoutDoc?.layout_fitWidth; + } + @computed get anchorViewDoc() { + return this._props.LayoutTemplateString?.includes('link_anchor_2') ? DocCast(this.Document['link_anchor_2']) : this._props.LayoutTemplateString?.includes('link_anchor_1') ? DocCast(this.Document['link_anchor_1']) : undefined; + } + + @computed get getBounds() { + if (!this._docViewInternal?._contentDiv || Doc.AreProtosEqual(this.Document, Doc.UserDoc())) { return undefined; } - if (this.docView._componentView?.screenBounds) { - return this.docView._componentView.screenBounds(); + if (this._docViewInternal._componentView?.screenBounds?.()) { + return this._docViewInternal._componentView.screenBounds(); } - const xf = this.docView.props - .ScreenToLocalTransform() - .scale(this.trueNativeWidth() ? this.nativeScaling : 1) - .inverse(); + 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.docView.props.LayoutTemplateString?.includes(LinkAnchorBox.name)) { - const docuBox = this.docView.ContentDiv.getElementsByClassName('linkAnchorBox-cont'); + if (this._props.LayoutTemplateString?.includes(LinkAnchorBox.name)) { + const docuBox = this._docViewInternal._contentDiv.getElementsByClassName('linkAnchorBox-cont'); if (docuBox.length) return { ...docuBox[0].getBoundingClientRect(), center: undefined }; } - return { left, top, right, bottom, center: this.ComponentView?.getCenter?.(xf) }; + return { left, top, right, bottom }; + } + + @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)); + } + @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)); + } + @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 + + /** + * 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 clearViewTransition = () => { + this._viewTimer && clearTimeout(this._viewTimer); + this.layoutDoc._viewTransition = undefined; }; + public noOnClick = () => this._docViewInternal?.noOnClick(); + public makeIntoPortal = () => this._docViewInternal?.makeIntoPortal(); + public toggleFollowLink = (zoom?: boolean, setTargetToggle?: boolean): void => this._docViewInternal?.toggleFollowLink(zoom, setTargetToggle); + public setToggleDetail = () => this._docViewInternal?.setToggleDetail(); + public onContextMenu = (e?: React.MouseEvent, pageX?: number, pageY?: number) => this._docViewInternal?.onContextMenu?.(e, pageX, pageY); + public cleanupPointerEvents = () => this._docViewInternal?.cleanupPointerEvents(); + public startDragging = (x: number, y: number, dropAction: dropActionType, hideSource = false) => this._docViewInternal?.startDragging(x, y, dropAction, hideSource); + public showContextMenu = (pageX: number, pageY: number) => this._docViewInternal?.onContextMenu(undefined, pageX, pageY); + + public toggleNativeDimensions = () => this._docViewInternal && this.Document.type !== DocumentType.INK && Doc.toggleNativeDimensions(this.layoutDoc, this.NativeDimScaling() ?? 1, this._props.PanelWidth(), this._props.PanelHeight()); public iconify(finished?: () => void, animateTime?: number) { this.ComponentView?.updateIcon?.(); - const animTime = this.docView?._animateScaleTime; - runInAction(() => this.docView && animateTime !== undefined && (this.docView._animateScaleTime = animateTime)); + const animTime = this._docViewInternal?.animateScaleTime(); + runInAction(() => this._docViewInternal && animateTime !== undefined && (this._docViewInternal._animateScaleTime = animateTime)); const finalFinished = action(() => { finished?.(); - this.docView && (this.docView._animateScaleTime = animTime); + this._docViewInternal && (this._docViewInternal._animateScaleTime = animTime); }); const layout_fieldKey = Cast(this.Document.layout_fieldKey, 'string', null); if (layout_fieldKey !== 'layout_icon') { @@ -1523,123 +1280,169 @@ export class DocumentView extends React.Component<DocumentViewProps> { const deiconifyLayout = Cast(this.Document.deiconifyLayout, 'string', null); this.switchViews(deiconifyLayout ? true : false, deiconifyLayout, finalFinished); this.Document.deiconifyLayout = undefined; - this.props.bringToFront(this.rootDoc); + this._props.bringToFront?.(this.Document); } } - @undoBatch - @action - setCustomView = (custom: boolean, layout: string): void => { - Doc.setNativeView(this.props.Document); - custom && DocUtils.makeCustomViewClicked(this.props.Document, Docs.Create.StackingDocument, layout, undefined); + + public playAnnotation = () => { + const self = this; + const audioAnnoState = this.dataDoc.audioAnnoState ?? 'stopped'; + const audioAnnos = Cast(this.dataDoc[this.LayoutFieldKey + '_audioAnnotations'], listSpec(AudioField), null); + const anno = audioAnnos?.lastElement(); + if (anno instanceof AudioField) { + switch (audioAnnoState) { + case 'stopped': + this.dataDoc[AudioPlay] = new Howl({ + src: [anno.url.href], + format: ['mp3'], + autoplay: true, + loop: false, + volume: 0.5, + onend: action(() => (self.dataDoc.audioAnnoState = 'stopped')), + }); + this.dataDoc.audioAnnoState = 'playing'; + break; + case 'playing': + this.dataDoc[AudioPlay]?.stop(); + this.dataDoc.audioAnnoState = 'stopped'; + break; + } + } }; - @action - switchViews = (custom: boolean, view: string, finished?: () => void, useExistingLayout = false) => { - this.docView && (this.docView._animateScalingTo = 0.1); // shrink doc + + public setTextHtmlOverlay = action((text: string | undefined, effect?: Doc) => { + this._htmlOverlayText = text; + this._htmlOverlayEffect = effect; + }); + public setAnimateScaling = action((scale: number, time?: number) => { + if (this._docViewInternal) { + this._docViewInternal._animateScalingTo = scale; + this._docViewInternal._animateScaleTime = time; + } + }); + 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); + }; + public setViewTransition = (transProp: string, timeInMs: number, afterTrans?: () => void, dataTrans = false) => { + this._viewTimer = DocumentView.SetViewTransition([this.layoutDoc], transProp, timeInMs, this._viewTimer, afterTrans, dataTrans); + }; + + public setCustomView = undoable((custom: boolean, layout: string): void => { + Doc.setNativeView(this.Document); + custom && DocUtils.makeCustomViewClicked(this.Document, Docs.Create.StackingDocument, layout, undefined); + }, 'set custom view'); + + public switchViews = (custom: boolean, view: string, finished?: () => void, useExistingLayout = false) => { + runInAction(() => this._docViewInternal && (this._docViewInternal._animateScalingTo = 0.1)); // shrink doc setTimeout( action(() => { - if (useExistingLayout && custom && this.rootDoc['layout_' + view]) { - this.rootDoc.layout_fieldKey = 'layout_' + view; + if (useExistingLayout && custom && this.Document['layout_' + view]) { + this.Document.layout_fieldKey = 'layout_' + view; } else { this.setCustomView(custom, view); } - this.docView && (this.docView._animateScalingTo = 1); // expand it + this._docViewInternal && (this._docViewInternal._animateScalingTo = 1); // expand it setTimeout( action(() => { - this.docView && (this.docView._animateScalingTo = 0); + this._docViewInternal && (this._docViewInternal._animateScalingTo = 0); finished?.(); }), - this.docView ? Math.max(0, this.docView.animateScaleTime - 10) : 0 + Math.max(0, (this._docViewInternal?.animateScaleTime() ?? 0) - 10) ); }), - this.docView ? Math.max(0, this.docView?.animateScaleTime - 10) : 0 + Math.max(0, (this._docViewInternal?.animateScaleTime() ?? 0) - 10) ); }; + /** + * @returns a hierarchy path through the nested DocumentViews that display this view. The last element of the path is this view. + */ + public docViewPath = () => (this.containerViewPath ? [...this.containerViewPath(), this] : [this]); - scaleToScreenSpace = () => (1 / (this.props.NativeDimScaling?.() || 1)) * this.screenToLocalTransform().Scale; - docViewPathFunc = () => this.docViewPath; - isSelected = (outsideReaction?: boolean) => SelectionManager.IsSelected(this, outsideReaction); + layout_fitWidthFunc = (doc: Doc) => BoolCast(this.layout_fitWidth); + screenToLocalScale = () => this._props.ScreenToLocalTransform().Scale; + isSelected = () => this.IsSelected; select = (extendSelection: boolean, focusSelection?: boolean) => { - SelectionManager.SelectView(this, extendSelection); - if (focusSelection) { - DocumentManager.Instance.showDocument(this.rootDoc, { - willZoomCentered: true, - zoomScale: 0.9, - zoomTime: 500, - }); + if (this.IsSelected && SelectionManager.Views.length > 1) SelectionManager.DeselectView(this); + else { + SelectionManager.SelectView(this, extendSelection); + if (focusSelection) { + DocumentManager.Instance.showDocument(this.Document, { + willZoomCentered: true, + zoomScale: 0.9, + zoomTime: 500, + }); + } } }; + ShouldNotScale = () => this.shouldNotScale; NativeWidth = () => this.effectiveNativeWidth; NativeHeight = () => this.effectiveNativeHeight; PanelWidth = () => this.panelWidth; PanelHeight = () => this.panelHeight; NativeDimScaling = () => this.nativeScaling; + hideLinkCount = () => (this.hideLinkButton ? true : false); selfView = () => this; - trueNativeWidth = () => returnVal(this.props.NativeWidth?.(), Doc.NativeWidth(this.layoutDoc, this.props.DataDoc, false)); - screenToLocalTransform = () => - this.props + /** + * @returns Transform to the document view (in the coordinate system of whatever contains the DocumentView) + */ + screenToViewTransform = () => this._props.ScreenToLocalTransform(); + /** + * @returns Transform to the coordinate system of the contents of the document view (includes native dimension scaling and centering) + */ + screenToContentsTransform = () => + this._props .ScreenToLocalTransform() .translate(-this.centeringX, -this.centeringY) - .scale(this.trueNativeWidth() ? 1 / this.nativeScaling : 1); - componentDidMount() { - this._disposers.updateContentsScript = reaction( - () => ScriptCast(this.rootDoc.updateContentsScript)?.script?.run({ this: this.props.Document, self: Cast(this.rootDoc, Doc, null) || this.props.Document }).result, - output => output - ); - this._disposers.height = reaction( - // increase max auto height if document has been resized to be greater than current max - () => NumCast(this.layoutDoc._height), - action(height => { - const docMax = NumCast(this.layoutDoc.layout_maxAutoHeight); - if (docMax && docMax < height) this.layoutDoc.layout_maxAutoHeight = height; - }) - ); - !BoolCast(this.props.Document.dontRegisterView, this.props.dontRegisterView) && DocumentManager.Instance.AddView(this); - } - componentWillUnmount() { - Object.values(this._disposers).forEach(disposer => disposer?.()); - !BoolCast(this.props.Document.dontRegisterView, this.props.dontRegisterView) && DocumentManager.Instance.RemoveView(this); - } - @computed get htmlOverlay() { - return !this.textHtmlOverlay ? null : ( - <div className="documentView-htmlOverlay"> - <div className="documentView-htmlOverlayInner"> - <Fade delay={0} duration={500}> - {DocumentViewInternal.AnimationEffect( - <div className="webBox-textHighlight"> - <ObserverJsxParser autoCloseVoidElements={true} key={42} onError={(e: any) => console.log('PARSE error', e)} renderInWrapper={false} jsx={StrCast(this.textHtmlOverlay)} /> - </div>, - { presentation_effect: this.htmlOverlayEffect ?? 'Zoom' } as any as Doc, - this.rootDoc - )}{' '} - </Fade> + .scale(1 / this.nativeScaling); + + htmlOverlay = () => { + const effect = StrCast(this._htmlOverlayEffect?.presentation_effect, StrCast(this._htmlOverlayEffect?.followLinkAnimEffect)); + return ( + <div + className="documentView-htmlOverlay" + 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))); + } + }} + 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)} /> + </div>, + { ...(this._htmlOverlayEffect ?? {}), presentation_effect: effect ?? PresEffect.Zoom } as any as Doc, + this.Document + )} </div> </div> ); - } + }; render() { TraceMobx(); - const xshift = Math.abs(this.Xshift) <= 0.001 ? this.props.PanelWidth() : undefined; - const yshift = Math.abs(this.Yshift) <= 0.001 ? this.props.PanelHeight() : undefined; + const xshift = Math.abs(this.Xshift) <= 0.001 ? this._props.PanelWidth() : undefined; + const yshift = Math.abs(this.Yshift) <= 0.001 ? this._props.PanelHeight() : undefined; return ( <div className="contentFittingDocumentView" onPointerEnter={action(() => (this._isHovering = true))} onPointerLeave={action(() => (this._isHovering = false))}> - {!this.props.Document || !this.props.PanelWidth() ? null : ( + {!this.Document || !this._props.PanelWidth() ? null : ( <div className="contentFittingDocumentView-previewDoc" ref={this.ContentRef} style={{ - transition: this.props.dataTransition, transform: `translate(${this.centeringX}px, ${this.centeringY}px)`, - width: xshift ?? `${(100 * (this.props.PanelWidth() - this.Xshift * 2)) / this.props.PanelWidth()}%`, - height: this.props.forceAutoHeight - ? undefined - : yshift ?? (this.layout_fitWidth ? `${this.panelHeight}px` : `${(((100 * this.effectiveNativeHeight) / this.effectiveNativeWidth) * this.props.PanelWidth()) / this.props.PanelHeight()}%`), + width: xshift ?? `${this._props.PanelWidth() - this.Xshift * 2}px`, + height: this._props.forceAutoHeight ? undefined : yshift ?? (this.layout_fitWidth ? `${this.panelHeight}px` : `${(this.effectiveNativeHeight / this.effectiveNativeWidth) * this._props.PanelWidth()}px`), }}> <DocumentViewInternal - {...this.props} + {...this._props} + fieldKey={this.LayoutFieldKey} DocumentView={this.selfView} - viewPath={this.docViewPathFunc} + docViewPath={this.docViewPath} PanelWidth={this.PanelWidth} PanelHeight={this.PanelHeight} NativeWidth={this.NativeWidth} @@ -1647,18 +1450,49 @@ export class DocumentView extends React.Component<DocumentViewProps> { NativeDimScaling={this.NativeDimScaling} isSelected={this.isSelected} select={this.select} - ScreenToLocalTransform={this.screenToLocalTransform} - focus={this.props.focus || emptyFunction} - ref={action((r: DocumentViewInternal | null) => r && (this.docView = r))} + layout_fitWidth={this.layout_fitWidthFunc} + ScreenToLocalTransform={this.screenToContentsTransform} + focus={this._props.focus || emptyFunction} + ref={action((r: DocumentViewInternal | null) => r && (this._docViewInternal = r))} /> - {this.htmlOverlay} + {this.htmlOverlay()} + {this.ComponentView?.infoUI?.()} </div> )} - - {this.linkCountView} + {/* display link count button */} + <DocumentLinksButton hideCount={this.hideLinkCount} View={this} scaling={this.screenToLocalScale} OnHover={true} Bottom={this.topMost} ShowCount={true} /> </div> ); } + + public static SetViewTransition(docs: Doc[], transProp: string, timeInMs: number, timer?: NodeJS.Timeout | undefined, afterTrans?: () => void, dataTrans = false) { + docs.forEach(doc => { + doc._viewTransition = `${transProp} ${timeInMs}ms`; + dataTrans && (doc.dataTransition = `${transProp} ${timeInMs}ms`); + }); + timer && clearTimeout(timer); + return setTimeout( + () => + docs.forEach(doc => { + doc._viewTransition = undefined; + dataTrans && (doc.dataTransition = 'inherit'); + afterTrans?.(); + }), + timeInMs + 10 + ); + } + + // 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'; + // prettier-ignore + DocServer.GetRefField(docId).then(docx => + LightboxView.Instance.SetLightboxDoc( + (docx as Doc) ?? // reuse existing pivot view of documents, or else create a new collection + Docs.Create.StackingDocument([], { title: linkAnchor.title + '-pivot', _width: 500, _height: 500, target: linkAnchor, onViewMounted: ScriptField.MakeScript('updateLinkCollection(this, this.target)') }, docId) + ) + ); + } } ScriptingGlobals.add(function deiconifyView(documentView: DocumentView) { @@ -1667,8 +1501,7 @@ ScriptingGlobals.add(function deiconifyView(documentView: DocumentView) { }); ScriptingGlobals.add(function deiconifyViewToLightbox(documentView: DocumentView) { - //documentView.iconify(() => - LightboxView.AddDocTab(documentView.rootDoc, OpenWhere.lightbox, 'layout'); //, 0); + LightboxView.Instance.AddDocTab(documentView.Document, OpenWhere.lightbox, 'layout'); //, 0); }); ScriptingGlobals.add(function toggleDetail(dv: DocumentView, detailLayoutKeySuffix: string) { @@ -1678,7 +1511,7 @@ ScriptingGlobals.add(function toggleDetail(dv: DocumentView, detailLayoutKeySuff ScriptingGlobals.add(function updateLinkCollection(linkCollection: Doc, linkSource: Doc) { const collectedLinks = DocListCast(Doc.GetProto(linkCollection).data); - let wid = linkSource[Width](); + let wid = NumCast(linkSource._width); let embedding: Doc | undefined; const links = LinkManager.Links(linkSource); links.forEach(link => { @@ -1689,7 +1522,7 @@ ScriptingGlobals.add(function updateLinkCollection(linkCollection: Doc, linkSour embedding.x = wid; embedding.y = 0; embedding._lockedPosition = false; - wid += otherdoc[Width](); + wid += NumCast(otherdoc._width); Doc.AddDocToList(Doc.GetProto(linkCollection), 'data', embedding); } }); diff --git a/src/client/views/nodes/EquationBox.scss b/src/client/views/nodes/EquationBox.scss index f5871db22..5009ec7a7 100644 --- a/src/client/views/nodes/EquationBox.scss +++ b/src/client/views/nodes/EquationBox.scss @@ -1,4 +1,4 @@ -@import '../global/globalCssVariables.scss'; +@import '../global/globalCssVariables.module.scss'; .equationBox-cont { transform-origin: center; diff --git a/src/client/views/nodes/EquationBox.tsx b/src/client/views/nodes/EquationBox.tsx index a77e4bdd1..2e03a766a 100644 --- a/src/client/views/nodes/EquationBox.tsx +++ b/src/client/views/nodes/EquationBox.tsx @@ -1,8 +1,6 @@ -import EquationEditor from 'equation-editor-react'; -import { action, reaction } from 'mobx'; +import { action, makeObservable, reaction } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; -import { Width } from '../../../fields/DocSymbols'; import { Id } from '../../../fields/FieldSymbols'; import { NumCast, StrCast } from '../../../fields/Types'; import { TraceMobx } from '../../../fields/util'; @@ -12,6 +10,7 @@ import { ViewBoxBaseComponent } from '../DocComponent'; import { LightboxView } from '../LightboxView'; import './EquationBox.scss'; import { FieldView, FieldViewProps } from './FieldView'; +import EquationEditor from './formattedText/EquationEditor'; @observer export class EquationBox extends ViewBoxBaseComponent<FieldViewProps>() { @@ -20,13 +19,19 @@ export class EquationBox extends ViewBoxBaseComponent<FieldViewProps>() { } public static SelectOnLoad: string = ''; _ref: React.RefObject<EquationEditor> = React.createRef(); + + constructor(props: FieldViewProps) { + super(props); + makeObservable(this); + } + componentDidMount() { - this.props.setContentView?.(this); - if (EquationBox.SelectOnLoad === this.rootDoc[Id] && (!LightboxView.LightboxDoc || LightboxView.IsLightboxDocView(this.props.docViewPath()))) { - this.props.select(false); + this._props.setContentViewBox?.(this); + if (EquationBox.SelectOnLoad === this.Document[Id] && (!LightboxView.LightboxDoc || LightboxView.Contains(this.DocumentView?.()))) { + this._props.select(false); this._ref.current!.mathField.focus(); - this.rootDoc.text === 'x' && this._ref.current!.mathField.select(); + this.dataDoc.text === 'x' && this._ref.current!.mathField.select(); EquationBox.SelectOnLoad = ''; } reaction( @@ -39,7 +44,7 @@ export class EquationBox extends ViewBoxBaseComponent<FieldViewProps>() { //{ fireImmediately: true } ); reaction( - () => this.props.isSelected(), + () => this._props.isSelected(), selected => { if (this._ref.current) { if (selected) this._ref.current.element.current.children[0].addEventListener('keydown', this.keyPressed, true); @@ -63,21 +68,21 @@ export class EquationBox extends ViewBoxBaseComponent<FieldViewProps>() { y: NumCast(this.layoutDoc.y) + _height + 10, }); EquationBox.SelectOnLoad = nextEq[Id]; - this.props.addDocument?.(nextEq); + this._props.addDocument?.(nextEq); e.stopPropagation(); } if (e.key === 'Tab') { - const graph = Docs.Create.FunctionPlotDocument([this.rootDoc], { - x: NumCast(this.layoutDoc.x) + this.layoutDoc[Width](), + const graph = Docs.Create.FunctionPlotDocument([this.Document], { + x: NumCast(this.layoutDoc.x) + NumCast(this.layoutDoc._width), y: NumCast(this.layoutDoc.y), _width: 400, _height: 300, backgroundColor: 'white', }); - this.props.addDocument?.(graph); + this._props.addDocument?.(graph); e.stopPropagation(); } - if (e.key === 'Backspace' && !this.dataDoc.text) this.props.removeDocument?.(this.rootDoc); + if (e.key === 'Backspace' && !this.dataDoc.text) this._props.removeDocument?.(this.Document); }; @undoBatch onChange = (str: string) => (this.dataDoc.text = str); @@ -85,13 +90,23 @@ export class EquationBox extends ViewBoxBaseComponent<FieldViewProps>() { updateSize = () => { const style = this._ref.current && getComputedStyle(this._ref.current.element.current); if (style?.width.endsWith('px') && style?.height.endsWith('px')) { - this.layoutDoc._width = Math.max(35, Number(style.width.replace('px', ''))); - this.layoutDoc._height = Math.max(25, Number(style.height.replace('px', ''))); + if (this.layoutDoc._nativeWidth) { + // if equation has been scaled then editing the expression must also edit the native dimensions to keep the aspect ratio + const prevNwidth = NumCast(this.layoutDoc._nativeWidth); + const prevNheight = NumCast(this.layoutDoc._nativeHeight); + this.layoutDoc._nativeWidth = Math.max(35, Number(style.width.replace('px', ''))); + this.layoutDoc._nativeHeight = Math.max(25, Number(style.height.replace('px', ''))); + this.layoutDoc._width = (NumCast(this.layoutDoc._width) * NumCast(this.layoutDoc._nativeWidth)) / prevNwidth; + this.layoutDoc._height = (NumCast(this.layoutDoc._height) * NumCast(this.layoutDoc._nativeHeight)) / prevNheight; + } else { + this.layoutDoc._width = Math.max(35, Number(style.width.replace('px', ''))); + this.layoutDoc._height = Math.max(25, Number(style.height.replace('px', ''))); + } } }; render() { TraceMobx(); - const scale = (this.props.NativeDimScaling?.() || 1) * NumCast(this.layoutDoc._freeform_scale, 1); + const scale = (this._props.NativeDimScaling?.() || 1) * NumCast(this.layoutDoc._freeform_scale, 1); return ( <div ref={r => this.updateSize()} @@ -101,8 +116,8 @@ export class EquationBox extends ViewBoxBaseComponent<FieldViewProps>() { transform: `scale(${scale})`, width: 'fit-content', // `${100 / scale}%`, height: `${100 / scale}%`, - pointerEvents: !this.props.isSelected() ? 'none' : undefined, - fontSize: StrCast(this.rootDoc._text_fontSize), + pointerEvents: !this._props.isSelected() ? 'none' : undefined, + 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" /> diff --git a/src/client/views/nodes/FaceRectangle.tsx b/src/client/views/nodes/FaceRectangle.tsx index 20afa4565..46bc6eb03 100644 --- a/src/client/views/nodes/FaceRectangle.tsx +++ b/src/client/views/nodes/FaceRectangle.tsx @@ -1,14 +1,14 @@ -import React = require("react"); -import { observer } from "mobx-react"; -import { observable, runInAction } from "mobx"; -import { RectangleTemplate } from "./FaceRectangles"; +import { observable, runInAction } from 'mobx'; +import { observer } from 'mobx-react'; +import * as React from 'react'; +import { RectangleTemplate } from './FaceRectangles'; @observer export default class FaceRectangle extends React.Component<{ rectangle: RectangleTemplate }> { @observable private opacity = 0; componentDidMount() { - setTimeout(() => runInAction(() => this.opacity = 1), 500); + setTimeout(() => runInAction(() => (this.opacity = 1)), 500); } render() { @@ -18,12 +18,11 @@ export default class FaceRectangle extends React.Component<{ rectangle: Rectangl style={{ ...rectangle.style, opacity: this.opacity, - transition: "1s ease opacity", - position: "absolute", - borderRadius: 5 + transition: '1s ease opacity', + position: 'absolute', + borderRadius: 5, }} /> ); } - -}
\ No newline at end of file +} diff --git a/src/client/views/nodes/FaceRectangles.tsx b/src/client/views/nodes/FaceRectangles.tsx index 0d1e063af..ade4225d9 100644 --- a/src/client/views/nodes/FaceRectangles.tsx +++ b/src/client/views/nodes/FaceRectangles.tsx @@ -1,9 +1,9 @@ -import React = require("react"); -import { Doc, DocListCast } from "../../../fields/Doc"; -import { Cast, NumCast } from "../../../fields/Types"; -import { observer } from "mobx-react"; -import { Id } from "../../../fields/FieldSymbols"; -import FaceRectangle from "./FaceRectangle"; +import { observer } from 'mobx-react'; +import * as React from 'react'; +import { Doc, DocListCast } from '../../../fields/Doc'; +import { Id } from '../../../fields/FieldSymbols'; +import { Cast, NumCast } from '../../../fields/Types'; +import FaceRectangle from './FaceRectangle'; interface FaceRectanglesProps { document: Doc; @@ -18,7 +18,6 @@ export interface RectangleTemplate { @observer export class FaceRectangles extends React.Component<FaceRectanglesProps> { - render() { const faces = DocListCast(this.props.document.faces); const templates: RectangleTemplate[] = faces.map(faceDoc => { @@ -33,14 +32,15 @@ export class FaceRectangles extends React.Component<FaceRectanglesProps> { } as React.CSSProperties; return { id: rectangle[Id], - style: style + style: style, }; }); return ( <div> - {templates.map(rectangle => <FaceRectangle key={rectangle.id} rectangle={rectangle} />)} + {templates.map(rectangle => ( + <FaceRectangle key={rectangle.id} rectangle={rectangle} /> + ))} </div> ); } - -}
\ No newline at end of file +} diff --git a/src/client/views/nodes/FieldView.tsx b/src/client/views/nodes/FieldView.tsx index f014f842e..8a49b4757 100644 --- a/src/client/views/nodes/FieldView.tsx +++ b/src/client/views/nodes/FieldView.tsx @@ -1,44 +1,119 @@ -import React = require('react'); -import { computed } from 'mobx'; import { observer } from 'mobx-react'; +import * as React from 'react'; import { DateField } from '../../../fields/DateField'; -import { Doc, Field, FieldResult, Opt } from '../../../fields/Doc'; +import { Doc, Field, Opt } from '../../../fields/Doc'; import { List } from '../../../fields/List'; import { ScriptField } from '../../../fields/ScriptField'; import { WebField } from '../../../fields/URLField'; -import { DocumentViewSharedProps } from './DocumentView'; +import { dropActionType } from '../../util/DragManager'; +import { Transform } from '../../util/Transform'; +import { ViewBoxInterface } from '../DocComponent'; +import { CollectionFreeFormDocumentView } from './CollectionFreeFormDocumentView'; +import { DocumentView, OpenWhere } from './DocumentView'; +import { PinProps } from './trails'; +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>; +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. // However, that only happens because the properties are "defined" in the markup for the field view. // See the LayoutString method on each field view : ImageBox, FormattedTextBox, etc. // -export interface FieldViewProps extends DocumentViewSharedProps { - // FieldView specific props that are not part of DocumentView props - fieldKey: string; - scrollOverflow?: boolean; // bcz: would like to think this can be avoided -- need to look at further - - select: (isCtrlPressed: boolean) => void; - isContentActive: (outsideReaction?: boolean) => boolean | undefined; - isDocumentActive?: () => boolean | undefined; - isSelected: (outsideReaction?: boolean) => boolean; - setHeight?: (height: number) => void; - NativeDimScaling?: () => number; // scaling the DocumentView does to transform its contents into its panel & needed by ScreenToLocal NOTE: Must also be added to DocumentViewInternalsProps - onBrowseClick?: () => ScriptField | undefined; +export interface FieldViewSharedProps { + Document: Doc; + TemplateDataDocument?: Doc; + LayoutTemplateString?: string; + LayoutTemplate?: () => Opt<Doc>; + renderDepth: number; + scriptContext?: any; // can be assigned anything and will be passed as 'scriptContext' to any OnClick script that executes on this document + xPadding?: number; + yPadding?: number; + dontRegisterView?: boolean; + dropAction?: dropActionType; + dragAction?: dropActionType; + forceAutoHeight?: boolean; + ignoreAutoHeight?: boolean; + 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 + CollectionFreeFormDocumentView?: () => CollectionFreeFormDocumentView; + 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 + PanelWidth: () => number; + PanelHeight: () => number; + isDocumentActive?: () => boolean | undefined; // whether a document should handle pointer events + isContentActive: () => boolean | undefined; // whether document contents should handle pointer events + childFilters: () => string[]; + childFiltersByRanges: () => string[]; + styleProvider: Opt<StyleProviderFuncType>; + setTitleFocus?: () => void; + focus: FocusFuncType; + onClickScript?: () => ScriptField; + onDoubleClickScript?: () => ScriptField; + onPointerDownScript?: () => ScriptField; + onPointerUpScript?: () => ScriptField; + onBrowseClickScript?: () => ScriptField | undefined; 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; + 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; + moveDocument?: (doc: Doc | Doc[], targetCollection: Doc | undefined, addDocument: (document: Doc | Doc[], annotationKey?: string) => boolean) => boolean; + pinToPres: (document: Doc, pinProps: PinProps) => void; + ScreenToLocalTransform: () => Transform; + bringToFront?: (doc: Doc, sendToBack?: boolean) => void; + waitForDoubleClickToClick?: () => 'never' | 'always' | undefined; + defaultDoubleClick?: () => 'default' | 'ignore' | undefined; pointerEvents?: () => Opt<string>; +} + +/** + * FieldView specific props that are not shared with DocumentView props + * */ +export interface FieldViewProps extends FieldViewSharedProps { + DocumentView?: () => DocumentView; + fieldKey: string; + isSelected: () => boolean; + select: (ctrlPressed: boolean, shiftPress?: boolean) => void; + docViewPath: () => DocumentView[]; + setHeight?: (height: number) => void; + NativeDimScaling?: () => number; // scaling the DocumentView does to transform its contents into its panel & needed by ScreenToLocal // properties intended to be used from within layout strings (otherwise use the function equivalents that work more efficiently with React) // See currentUserUtils headerTemplate for examples of creating text boxes from html which set some of these fields // Also, see InkingStroke for examples of creating text boxes from render() methods which set some of these fields backgroundColor?: string; - treeViewDoc?: Doc; color?: string; height?: number; width?: number; + dontSelectOnLoad?: boolean; // suppress selecting (e.g.,. text box) when loaded (and mark as not being associated with scrollTop document field) noSidebar?: boolean; dontScale?: boolean; - dontSelectOnLoad?: boolean; // suppress selecting (e.g.,. text box) when loaded (and mark as not being associated with scrollTop document field) } @observer @@ -47,13 +122,8 @@ export class FieldView extends React.Component<FieldViewProps> { return `<${fieldType.name} {...props} fieldKey={'${fieldStr}'}/>`; //e.g., "<ImageBox {...props} fieldKey={'data'} />" } - @computed - get field(): FieldResult { - const { Document, fieldKey: fieldKey } = this.props; - return Document[fieldKey]; - } render() { - const field = this.field; + const field = this.props.Document[this.props.fieldKey]; // prettier-ignore if (field instanceof Doc) return <p> <b>{field.title?.toString()}</b></p>; if (field === undefined) return <p>{'<null>'}</p>; diff --git a/src/client/views/nodes/FontIconBox/ButtonInterface.ts b/src/client/views/nodes/FontIconBox/ButtonInterface.ts index 0aa2ac8e1..1c034bfbe 100644 --- a/src/client/views/nodes/FontIconBox/ButtonInterface.ts +++ b/src/client/views/nodes/FontIconBox/ButtonInterface.ts @@ -1,12 +1,12 @@ -import { Doc } from "../../../../fields/Doc"; -import { IconProp } from "@fortawesome/fontawesome-svg-core"; -import { ButtonType } from "./FontIconBox"; +import { Doc } from '../../../../fields/Doc'; +import { IconProp } from '@fortawesome/fontawesome-svg-core'; +import { ButtonType } from './FontIconBox'; export interface IButtonProps { type: string | ButtonType; - rootDoc: Doc; + Document: Doc; label: any; icon: IconProp; color: string; backgroundColor: string; -}
\ No newline at end of file +} diff --git a/src/client/views/nodes/FontIconBox/FontIconBox.scss b/src/client/views/nodes/FontIconBox/FontIconBox.scss index 9d9fa26b0..db2ffa756 100644 --- a/src/client/views/nodes/FontIconBox/FontIconBox.scss +++ b/src/client/views/nodes/FontIconBox/FontIconBox.scss @@ -1,4 +1,4 @@ -@import '../../global/globalCssVariables'; +@import '../../global/globalCssVariables.module.scss'; .menuButton { height: 100%; diff --git a/src/client/views/nodes/FontIconBox/FontIconBox.tsx b/src/client/views/nodes/FontIconBox/FontIconBox.tsx index d2e1293da..cf07d98be 100644 --- a/src/client/views/nodes/FontIconBox/FontIconBox.tsx +++ b/src/client/views/nodes/FontIconBox/FontIconBox.tsx @@ -1,7 +1,7 @@ import { IconProp } from '@fortawesome/fontawesome-svg-core'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { Button, ColorPicker, Dropdown, DropdownType, EditableText, IconButton, IListItemProps, MultiToggle, NumberDropdown, NumberDropdownType, Popup, Size, Toggle, ToggleType, Type } from 'browndash-components'; -import { action, computed, observable } from 'mobx'; +import { computed, makeObservable, observable } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; import { Doc, DocListCast, StrListCast } from '../../../../fields/Doc'; @@ -13,7 +13,7 @@ import { SelectionManager } from '../../../util/SelectionManager'; import { SettingsManager } from '../../../util/SettingsManager'; import { undoable, UndoManager } from '../../../util/UndoManager'; import { ContextMenu } from '../../ContextMenu'; -import { DocComponent } from '../../DocComponent'; +import { ViewBoxBaseComponent } from '../../DocComponent'; import { EditableView } from '../../EditableView'; import { SelectedDocView } from '../../selectedDoc'; import { StyleProp } from '../../StyleProvider'; @@ -26,7 +26,6 @@ export enum ButtonType { TextButton = 'textBtn', MenuButton = 'menuBtn', DropdownList = 'dropdownList', - DropdownButton = 'dropdownBtn', ClickButton = 'clickBtn', ToggleButton = 'toggleBtn', ColorButton = 'colorBtn', @@ -42,14 +41,29 @@ export interface ButtonProps extends FieldViewProps { type?: ButtonType; } @observer -export class FontIconBox extends DocComponent<ButtonProps>() { +export class FontIconBox extends ViewBoxBaseComponent<ButtonProps>() { public static LayoutString(fieldKey: string) { return FieldView.LayoutString(FontIconBox, fieldKey); } + + constructor(props: 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; showTemplate = (): void => { const dragFactory = Cast(this.layoutDoc.dragFactory, Doc, null); - dragFactory && this.props.addDocTab(dragFactory, OpenWhere.addRight); + dragFactory && this._props.addDocTab(dragFactory, OpenWhere.addRight); }; dragAsTemplate = (): void => { this.layoutDoc.onDragStart = ScriptField.MakeFunction('getCopy(this.dragFactory, true)'); @@ -69,7 +83,7 @@ export class FontIconBox extends DocComponent<ButtonProps>() { // Determining UI Specs @computed get label() { - return StrCast(this.rootDoc.icon_label, StrCast(this.rootDoc.title)); + return StrCast(this.dataDoc.icon_label, StrCast(this.Document.title)); } Icon = (color: string, iconFalse?: boolean) => { let icon; @@ -82,13 +96,13 @@ export class FontIconBox extends DocComponent<ButtonProps>() { return !icon ? null : icon === 'pres-trail' ? TrailsIcon(color) : <FontAwesomeIcon className={`fontIconBox-icon-${this.type}`} icon={icon} color={color} />; }; @computed get dropdown() { - return BoolCast(this.rootDoc.dropDownOpen); + return BoolCast(this.Document.dropDownOpen); } @computed get buttonList() { - return StrListCast(this.rootDoc.btnList); + return StrListCast(this.Document.btnList); } @computed get type() { - return StrCast(this.rootDoc.btnType); + return StrCast(this.Document.btnType); } /** @@ -122,8 +136,8 @@ export class FontIconBox extends DocComponent<ButtonProps>() { type = 'slider'; break; } - const numScript = (value?: number) => ScriptCast(this.rootDoc.script).script.run({ this: this.layoutDoc, self: this.rootDoc, value, _readOnly_: value === undefined }); - const color = this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.Color); + const numScript = (value?: number) => ScriptCast(this.Document.script).script.run({ this: this.Document, self: this.Document, value, _readOnly_: value === undefined }); + const color = this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.Color); // Script for checking the outcome of the toggle const checkResult = Number(Number(numScript().result ?? 0).toPrecision(NumCast(this.dataDoc.numPrecision, 3))); @@ -135,52 +149,21 @@ export class FontIconBox extends DocComponent<ButtonProps>() { showPlusMinus={false} tooltip={this.label} type={Type.PRIM} - min={NumCast(this.rootDoc.numBtnMin, 0)} - max={NumCast(this.rootDoc.numBtnMax, 100)} + min={NumCast(this.dataDoc.numBtnMin, 0)} + max={NumCast(this.dataDoc.numBtnMax, 100)} number={checkResult} - setNumber={undoable(value => numScript(value), `${this.rootDoc.title} button set from list`)} + setNumber={undoable(value => numScript(value), `${this.Document.title} button set from list`)} fillWidth /> ); } - /** - * Dropdown button - */ - @computed get dropdownButton() { - const active: string = StrCast(this.rootDoc.dropDownOpen); - const color = this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.Color); - const backgroundColor = this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.BackgroundColor); - return ( - <div - className={`menuButton ${this.type} ${active}`} - style={{ color, backgroundColor, borderBottomLeftRadius: this.dropdown ? 0 : undefined }} - onClick={action(() => { - this.rootDoc.dropDownOpen = !this.rootDoc.dropDownOpen; - this.noTooltip = this.rootDoc.dropDownOpen; - Doc.UnBrushAllDocs(); - })}> - {this.Icon(color)} - {!this.label || !Doc.GetShowIconLabels() ? null : ( - <div className="fontIconBox-label" style={{ color: color, backgroundColor: backgroundColor }}> - {' '} - {this.label}{' '} - </div> - )} - <div className="menuButton-dropdown" style={{ borderBottomRightRadius: this.dropdown ? 0 : undefined }}> - <FontAwesomeIcon icon={'caret-down'} color={color} size="sm" /> - </div> - {this.rootDoc.dropDownOpen ? <div className="menuButton-dropdownBox">{/* DROPDOWN BOX CONTENTS */}</div> : null} - </div> - ); - } - dropdownItemDown = (e: React.PointerEvent, value: string | number) => { setupMoveUpEvents( this, e, (e: PointerEvent) => { - return ScriptCast(this.rootDoc.onDragScript)?.script.run({ this: this.layoutDoc, self: this.rootDoc, value: { doc: value, e } }).result; + return ScriptCast(this.Document.onDragScript)?.script.run({ this: this.Document, self: this.Document, value: { doc: value, e } }).result; }, emptyFunction, emptyFunction @@ -192,7 +175,7 @@ export class FontIconBox extends DocComponent<ButtonProps>() { * Dropdown list */ @computed get dropdownListButton() { - const script = ScriptCast(this.rootDoc.script); + const script = ScriptCast(this.Document.script); let noviceList: string[] = []; let text: string | undefined; @@ -200,7 +183,7 @@ export class FontIconBox extends DocComponent<ButtonProps>() { let icon: IconProp = 'caret-down'; const isViewDropdown = script?.script.originalScript.startsWith('setView'); if (isViewDropdown) { - const selected = SelectionManager.Docs(); + const selected = SelectionManager.Docs; if (selected.lastElement()) { if (StrCast(selected.lastElement().type) === DocumentType.COL) { text = StrCast(selected.lastElement()._type_collection); @@ -228,7 +211,7 @@ export class FontIconBox extends DocComponent<ButtonProps>() { } noviceList = [CollectionViewType.Freeform, CollectionViewType.Schema, CollectionViewType.Carousel3D, CollectionViewType.Stacking, CollectionViewType.NoteTaking]; } else { - text = script?.script.run({ this: this.layoutDoc, self: this.rootDoc, value: '', _readOnly_: true }).result; + text = script?.script.run({ this: this.Document, self: this.Document, value: '', _readOnly_: true }).result; // text = StrCast((RichTextMenu.Instance?.TextView?.EditorView ? RichTextMenu.Instance : Doc.UserDoc()).fontFamily); getStyle = (val: string) => ({ fontFamily: val }); } @@ -246,7 +229,7 @@ export class FontIconBox extends DocComponent<ButtonProps>() { return ( <Dropdown selectedVal={text} - setSelectedVal={undoable(value => script.script.run({ this: this.layoutDoc, self: this.rootDoc, value }), `dropdown select ${this.label}`)} + setSelectedVal={undoable(value => script.script.run({ this: this.Document, self: this.Document, value }), `dropdown select ${this.label}`)} color={SettingsManager.userColor} background={SettingsManager.userVariantColor} type={Type.TERT} @@ -261,7 +244,7 @@ export class FontIconBox extends DocComponent<ButtonProps>() { } @computed get colorScript() { - return ScriptCast(this.rootDoc.script); + return ScriptCast(this.Document.script); } colorBatch: UndoManager.Batch | undefined; @@ -269,18 +252,18 @@ export class FontIconBox extends DocComponent<ButtonProps>() { * Color button */ @computed get colorButton() { - const color = this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.Color); - const curColor = this.colorScript?.script.run({ this: this.layoutDoc, self: this.rootDoc, value: undefined, _readOnly_: true }).result ?? 'transparent'; - const tooltip: string = StrCast(this.rootDoc.toolTip); + const color = this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.Color); + const curColor = this.colorScript?.script.run({ this: this.Document, self: this.Document, value: undefined, _readOnly_: true }).result ?? 'transparent'; + const tooltip: string = StrCast(this.Document.toolTip); return ( <ColorPicker setSelectedColor={value => { if (!this.colorBatch) this.colorBatch = UndoManager.StartBatch(`Set ${tooltip} color`); - this.colorScript?.script.run({ this: this.layoutDoc, self: this.rootDoc, value: value, _readOnly_: false }); + this.colorScript?.script.run({ this: this.Document, self: this.Document, value: value, _readOnly_: false }); }} setFinalColor={value => { - this.colorScript?.script.run({ this: this.layoutDoc, self: this.rootDoc, value: value, _readOnly_: false }); + this.colorScript?.script.run({ this: this.Document, self: this.Document, value: value, _readOnly_: false }); this.colorBatch?.end(); this.colorBatch = undefined; }} @@ -297,13 +280,13 @@ export class FontIconBox extends DocComponent<ButtonProps>() { } @computed get multiToggleButton() { // Determine the type of toggle button - const tooltip: string = StrCast(this.rootDoc.toolTip); + const tooltip: string = StrCast(this.Document.toolTip); - const script = ScriptCast(this.rootDoc.onClick); - const toggleStatus = script ? script.script.run({ this: this.layoutDoc, self: this.rootDoc, value: undefined, _readOnly_: true }).result : false; + const script = ScriptCast(this.Document.onClick); + const toggleStatus = script ? script.script.run({ this: this.Document, self: this.Document, value: undefined, _readOnly_: true }).result : false; // Colors - const color = this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.Color); - const items = DocListCast(this.rootDoc.data); + const color = this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.Color); + const items = DocListCast(this.dataDoc.data); return ( <MultiToggle tooltip={`Toggle ${tooltip}`} @@ -311,7 +294,7 @@ export class FontIconBox extends DocComponent<ButtonProps>() { color={color} background={SettingsManager.userBackgroundColor} label={this.label} - items={DocListCast(this.rootDoc.data).map(item => ({ + 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), @@ -327,14 +310,14 @@ export class FontIconBox extends DocComponent<ButtonProps>() { @computed get toggleButton() { // Determine the type of toggle button - const buttonText: string = StrCast(this.rootDoc.buttonText); - const tooltip: string = StrCast(this.rootDoc.toolTip); + const buttonText = StrCast(this.dataDoc.buttonText); + const tooltip = StrCast(this.Document.toolTip); - const script = ScriptCast(this.rootDoc.onClick); - const toggleStatus = script ? script.script.run({ this: this.layoutDoc, self: this.rootDoc, value: undefined, _readOnly_: true }).result : false; + const script = ScriptCast(this.Document.onClick); + const toggleStatus = script ? script.script.run({ this: this.Document, self: this.Document, value: undefined, _readOnly_: true }).result : false; // Colors - const color = this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.Color); - const backgroundColor = this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.BackgroundColor); + const color = this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.Color); + const backgroundColor = this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.BackgroundColor); return ( <Toggle @@ -347,7 +330,7 @@ export class FontIconBox extends DocComponent<ButtonProps>() { //background={SettingsManager.userBackgroundColor} icon={this.Icon(color)!} label={this.label} - onPointerDown={() => script.script.run({ this: this.layoutDoc, self: this.rootDoc, value: !toggleStatus, _readOnly_: false })} + onPointerDown={() => script.script.run({ this: this.Document, self: this.Document, value: !toggleStatus, _readOnly_: false })} /> ); } @@ -356,20 +339,20 @@ export class FontIconBox extends DocComponent<ButtonProps>() { * Default */ @computed get defaultButton() { - const color = this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.Color); - const backgroundColor = this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.BackgroundColor); - const tooltip: string = StrCast(this.rootDoc.toolTip); + 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.rootDoc.script); + const script = ScriptCast(this.Document.script); // Function to run the script - const checkResult = script?.script.run({ this: this.layoutDoc, self: this.rootDoc, value: '', _readOnly_: true }).result; + const checkResult = script?.script.run({ this: this.Document, self: this.Document, value: '', _readOnly_: true }).result; - const setValue = (value: string, shiftDown?: boolean): boolean => script?.script.run({ this: this.layoutDoc, self: this.rootDoc, value, _readOnly_: false }).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) => {}} />; @@ -377,16 +360,16 @@ export class FontIconBox extends DocComponent<ButtonProps>() { <div className="menuButton editableText"> <FontAwesomeIcon className={`fontIconBox-icon-${this.type}`} icon={'lock'} /> <div style={{ width: 'calc(100% - .875em)', paddingLeft: '4px' }}> - <EditableView GetValue={() => script?.script.run({ this: this.layoutDoc, self: this.rootDoc, value: '', _readOnly_: true }).result} SetValue={setValue} oneLine={true} contents={checkResult} /> + <EditableView GetValue={() => script?.script.run({ this: this.Document, self: this.Document, value: '', _readOnly_: true }).result} SetValue={setValue} oneLine={true} contents={checkResult} /> </div> </div> ); } render() { - const color = this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.Color); - const tooltip = StrCast(this.rootDoc.toolTip); - const scriptFunc = () => ScriptCast(this.rootDoc.onClick)?.script.run({ this: this.layoutDoc, self: this.rootDoc, _readOnly_: false }); + const color = this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.Color); + const tooltip = StrCast(this.Document.toolTip); + const scriptFunc = () => ScriptCast(this.Document.onClick)?.script.run({ this: this.Document, self: this.Document, _readOnly_: false }); const btnProps = { tooltip, icon: this.Icon(color)!, label: this.label }; // prettier-ignore switch (this.type) { @@ -396,15 +379,14 @@ export class FontIconBox extends DocComponent<ButtonProps>() { case ButtonType.EditableText: return this.editableText; case ButtonType.DropdownList: return this.dropdownListButton; case ButtonType.ColorButton: return this.colorButton; - case ButtonType.DropdownButton: return this.dropdownButton; case ButtonType.MultiToggleButton: return this.multiToggleButton; case ButtonType.ToggleButton: return this.toggleButton; case ButtonType.ClickButton: 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.rootDoc.buttonText)}/>; + background={SettingsManager.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={SettingsManager.userBackgroundColor} size={Size.LARGE} tooltipPlacement='right' onPointerDown={scriptFunc} />; } return this.defaultButton; } diff --git a/src/client/views/nodes/FunctionPlotBox.tsx b/src/client/views/nodes/FunctionPlotBox.tsx index 40f48dafe..2e7a2120e 100644 --- a/src/client/views/nodes/FunctionPlotBox.tsx +++ b/src/client/views/nodes/FunctionPlotBox.tsx @@ -1,27 +1,19 @@ import functionPlot from 'function-plot'; -import { action, computed, reaction } from 'mobx'; +import { computed, makeObservable, reaction } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; import { Doc, DocListCast } from '../../../fields/Doc'; -import { documentSchema } from '../../../fields/documentSchemas'; -import { Id } from '../../../fields/FieldSymbols'; import { List } from '../../../fields/List'; -import { createSchema, listSpec, makeInterface } from '../../../fields/Schema'; +import { listSpec } from '../../../fields/Schema'; import { Cast, StrCast } from '../../../fields/Types'; import { TraceMobx } from '../../../fields/util'; import { Docs } from '../../documents/Documents'; import { DragManager } from '../../util/DragManager'; import { undoBatch } from '../../util/UndoManager'; import { ViewBoxAnnotatableComponent } from '../DocComponent'; -import { DocFocusOptions, DocumentView } from './DocumentView'; import { FieldView, FieldViewProps } from './FieldView'; import { PinProps, PresBox } from './trails'; -const EquationSchema = createSchema({}); - -type EquationDocument = makeInterface<[typeof EquationSchema, typeof documentSchema]>; -const EquationDocument = makeInterface(EquationSchema, documentSchema); - @observer export class FunctionPlotBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { public static LayoutString(fieldKey: string) { @@ -31,23 +23,23 @@ export class FunctionPlotBox extends ViewBoxAnnotatableComponent<FieldViewProps> _plot: any; _plotId = ''; _plotEle: any; + constructor(props: any) { super(props); + makeObservable(this); this._plotId = 'graph' + FunctionPlotBox.GraphCount++; } + componentDidMount() { - this.props.setContentView?.(this); + this._props.setContentViewBox?.(this); reaction( - () => [DocListCast(this.dataDoc[this.fieldKey]).map(doc => doc?.text), this.layoutDoc.width, this.layoutDoc.height, this.rootDoc.xRange, this.rootDoc.yRange], + () => [DocListCast(this.dataDoc[this.fieldKey]).map(doc => doc?.text), this.layoutDoc.width, this.layoutDoc.height, this.layoutDoc.xRange, this.layoutDoc.yRange], () => this.createGraph() ); } getAnchor = (addAsAnnotation: boolean, pinProps?: PinProps) => { - const anchor = Docs.Create.ConfigDocument({ - // - annotationOn: this.rootDoc, - }); - PresBox.pinDocView(anchor, { pinDocLayout: pinProps?.pinDocLayout, pinData: { ...(pinProps?.pinData ?? {}), datarange: true } }, this.rootDoc); + const anchor = Docs.Create.ConfigDocument({ annotationOn: this.Document }); + PresBox.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); @@ -55,8 +47,8 @@ export class FunctionPlotBox extends ViewBoxAnnotatableComponent<FieldViewProps> }; createGraph = (ele?: HTMLDivElement) => { this._plotEle = ele || this._plotEle; - const width = this.props.PanelWidth(); - const height = this.props.PanelHeight(); + const width = this._props.PanelWidth(); + const height = this._props.PanelHeight(); const fns = DocListCast(this.dataDoc.data).map(doc => StrCast(doc.text, 'x^2').replace(/\\frac\{(.*)\}\{(.*)\}/, '($1/$2)')); try { this._plotEle.children.length && this._plotEle.removeChild(this._plotEle.children[0]); @@ -64,8 +56,8 @@ export class FunctionPlotBox extends ViewBoxAnnotatableComponent<FieldViewProps> target: '#' + this._plotEle.id, width, height, - xAxis: { domain: Cast(this.rootDoc.xRange, listSpec('number'), [-10, 10]) }, - yAxis: { domain: Cast(this.rootDoc.yRange, listSpec('number'), [-1, 9]) }, + xAxis: { domain: Cast(this.layoutDoc.xRange, listSpec('number'), [-10, 10]) }, + yAxis: { domain: Cast(this.layoutDoc.yRange, listSpec('number'), [-1, 9]) }, grid: true, data: fns.map(fn => ({ fn, @@ -80,7 +72,7 @@ export class FunctionPlotBox extends ViewBoxAnnotatableComponent<FieldViewProps> @undoBatch drop = (e: Event, de: DragManager.DropEvent) => { if (de.complete.docDragData?.droppedDocuments.length) { - const added = de.complete.docDragData.droppedDocuments.reduce((res, doc) => res && Doc.AddDocToList(this.dataDoc, this.props.fieldKey, doc), true); + const added = de.complete.docDragData.droppedDocuments.reduce((res, doc) => res && Doc.AddDocToList(this.dataDoc, this._props.fieldKey, doc), true); !added && e.preventDefault(); e.stopPropagation(); // prevent parent Doc from registering new position so that it snaps back into place return added; @@ -105,14 +97,14 @@ export class FunctionPlotBox extends ViewBoxAnnotatableComponent<FieldViewProps> <div ref={this.createDropTarget} style={{ - pointerEvents: !this.props.isContentActive() ? 'all' : undefined, - width: this.props.PanelWidth(), - height: this.props.PanelHeight(), + pointerEvents: !this._props.isContentActive() ? 'all' : undefined, + width: this._props.PanelWidth(), + height: this._props.PanelHeight(), }}> {this.theGraph} <div style={{ - display: this.props.isSelected() ? 'none' : undefined, + display: this._props.isSelected() ? 'none' : undefined, position: 'absolute', width: '100%', height: '100%', diff --git a/src/client/views/nodes/ImageBox.scss b/src/client/views/nodes/ImageBox.scss index 29943e156..3ffda5a35 100644 --- a/src/client/views/nodes/ImageBox.scss +++ b/src/client/views/nodes/ImageBox.scss @@ -128,6 +128,7 @@ right: 0; bottom: 0; z-index: 2; + cursor: default; } .imageBox-fader img { diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx index 2f4f788d4..923aead64 100644 --- a/src/client/views/nodes/ImageBox.tsx +++ b/src/client/views/nodes/ImageBox.tsx @@ -1,64 +1,45 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { Tooltip } from '@material-ui/core'; -import { action, computed, IReactionDisposer, observable, ObservableMap, reaction, runInAction } from 'mobx'; +import { Tooltip } from '@mui/material'; +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, DocListCast, Opt } from '../../../fields/Doc'; -import { DocData, Width } from '../../../fields/DocSymbols'; +import { DocData } from '../../../fields/DocSymbols'; import { Id } from '../../../fields/FieldSymbols'; import { InkTool } from '../../../fields/InkField'; -import { List } from '../../../fields/List'; import { ObjectField } from '../../../fields/ObjectField'; -import { createSchema } from '../../../fields/Schema'; -import { ComputedField } from '../../../fields/ScriptField'; import { Cast, NumCast, StrCast } from '../../../fields/Types'; import { ImageField } from '../../../fields/URLField'; import { TraceMobx } from '../../../fields/util'; import { DashColor, emptyFunction, returnEmptyString, returnFalse, returnOne, returnZero, setupMoveUpEvents, Utils } from '../../../Utils'; -import { GooglePhotos } from '../../apis/google_docs/GooglePhotosClientUtils'; -import { CognitiveServices, Confidence, Service, Tag } from '../../cognitive_services/CognitiveServices'; import { Docs, DocUtils } from '../../documents/Documents'; import { DocumentType } from '../../documents/DocumentTypes'; -import { Networking } from '../../Network'; import { DocumentManager } from '../../util/DocumentManager'; import { DragManager } from '../../util/DragManager'; import { undoBatch } from '../../util/UndoManager'; import { ContextMenu } from '../../views/ContextMenu'; import { CollectionFreeFormView } from '../collections/collectionFreeForm/CollectionFreeFormView'; import { ContextMenuProps } from '../ContextMenuItem'; -import { ViewBoxAnnotatableComponent, ViewBoxAnnotatableProps } from '../DocComponent'; +import { ViewBoxAnnotatableComponent, ViewBoxInterface } from '../DocComponent'; import { MarqueeAnnotator } from '../MarqueeAnnotator'; import { AnchorMenu } from '../pdf/AnchorMenu'; import { StyleProp } from '../StyleProvider'; -import { DocFocusOptions, OpenWhere } from './DocumentView'; -import { FaceRectangles } from './FaceRectangles'; -import { FieldView, FieldViewProps } from './FieldView'; +import { OpenWhere } from './DocumentView'; +import { FocusViewOptions, FieldView, FieldViewProps } from './FieldView'; import './ImageBox.scss'; import { PinProps, PresBox } from './trails'; -import React = require('react'); - -export const pageSchema = createSchema({ - googlePhotosUrl: 'string', - googlePhotosTags: 'string', -}); -const uploadIcons = { - idle: 'downarrow.png', - loading: 'loading.gif', - success: 'greencheck.png', - failure: 'redx.png', -}; @observer -export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps & FieldViewProps>() { - protected _multiTouchDisposer?: import('../../util/InteractionUtils').InteractionUtils.MultiTouchEventDisposer | undefined; +export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implements ViewBoxInterface { public static LayoutString(fieldKey: string) { return FieldView.LayoutString(ImageBox, fieldKey); } - @observable public static imageRootDoc: Doc | undefined; + @observable public static imageRootDoc: Doc | undefined = undefined; @observable public static imageEditorOpen: boolean = false; @observable public static imageEditorSource: string = ''; - @observable public static addDoc: ((doc: Doc | Doc[], annotationKey?: string) => boolean) | undefined; + @observable public static addDoc: ((doc: Doc | Doc[], annotationKey?: string) => boolean) | undefined = undefined; @action public static setImageEditorOpen(open: boolean) { ImageBox.imageEditorOpen = open; } @@ -71,52 +52,53 @@ export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp private _disposers: { [name: string]: IReactionDisposer } = {}; private _getAnchor: (savedAnnotations: Opt<ObservableMap<number, HTMLDivElement[]>>, addAsAnnotation: boolean) => Opt<Doc> = () => undefined; private _overlayIconRef = React.createRef<HTMLDivElement>(); + private _marqueeref = React.createRef<MarqueeAnnotator>(); @observable _curSuffix = ''; - @observable _uploadIcon = uploadIcons.idle; - constructor(props: any) { + constructor(props: FieldViewProps) { super(props); - this.props.setContentView?.(this); + makeObservable(this); + this._props.setContentViewBox?.(this); } protected createDropTarget = (ele: HTMLDivElement) => { this._dropDisposer?.(); - ele && (this._dropDisposer = DragManager.MakeDropTarget(ele, this.drop.bind(this), this.props.Document)); + ele && (this._dropDisposer = DragManager.MakeDropTarget(ele, this.drop.bind(this), this.Document)); }; getAnchor = (addAsAnnotation: boolean, pinProps?: PinProps) => { - const visibleAnchor = this._getAnchor?.(this._savedAnnotations, false); // use marquee anchor, otherwise, save zoom/pan as anchor + const visibleAnchor = this._getAnchor?.(this._savedAnnotations, true); // use marquee anchor, otherwise, save zoom/pan as anchor const anchor = visibleAnchor ?? Docs.Create.ConfigDocument({ - title: 'ImgAnchor:' + this.rootDoc.title, + title: 'ImgAnchor:' + this.Document.title, config_panX: NumCast(this.layoutDoc._freeform_panX), config_panY: NumCast(this.layoutDoc._freeform_panY), config_viewScale: Cast(this.layoutDoc._freeform_scale, 'number', null), - annotationOn: this.rootDoc, + annotationOn: this.Document, }); 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.rootDoc); + PresBox.pinDocView(anchor, { pinDocLayout: pinProps?.pinDocLayout, pinData: { ...(pinProps?.pinData ?? {}), pannable: visibleAnchor ? false : true } }, this.Document); return anchor; } - return this.rootDoc; + return this.Document; }; componentDidMount() { this._disposers.sizer = reaction( () => ({ - forceFull: this.props.renderDepth < 1 || this.layoutDoc._showFullRes, - scrSize: (this.props.ScreenToLocalTransform().inverse().transformDirection(this.nativeSize.nativeWidth, this.nativeSize.nativeHeight)[0] / this.nativeSize.nativeWidth) * NumCast(this.rootDoc._freeform_scale, 1), - selected: this.props.isSelected(), + forceFull: this._props.renderDepth < 1 || this.layoutDoc._showFullRes, + 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'), { fireImmediately: true, delay: 1000 } ); const layoutDoc = this.layoutDoc; this._disposers.path = reaction( - () => ({ nativeSize: this.nativeSize, width: this.layoutDoc[Width]() }), + () => ({ nativeSize: this.nativeSize, width: NumCast(this.layoutDoc._width) }), ({ nativeSize, width }) => { if (layoutDoc === this.layoutDoc || !this.layoutDoc._height) { this.layoutDoc._height = (width * nativeSize.nativeHeight) / nativeSize.nativeWidth; @@ -141,7 +123,6 @@ export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp } @undoBatch - @action drop = (e: Event, de: DragManager.DropEvent) => { if (de.complete.docDragData) { let added: boolean | undefined = undefined; @@ -152,7 +133,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp }; if (de.metaKey || targetIsBullseye(e.target as HTMLElement)) { added = de.complete.docDragData.droppedDocuments.reduce((last: boolean, drop: Doc) => { - this.rootDoc[this.fieldKey + '_usePath'] = 'alternate:hover'; + this.layoutDoc[this.fieldKey + '_usePath'] = 'alternate:hover'; return last && Doc.AddDocToList(this.dataDoc, this.fieldKey + '-alternates', drop); }, true); } else if (de.altKey || !this.dataDoc[this.fieldKey]) { @@ -177,13 +158,13 @@ export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp @undoBatch setNativeSize = action(() => { - const scaling = (this.props.DocumentView?.().props.ScreenToLocalTransform().Scale || 1) / NumCast(this.rootDoc._freeform_scale, 1); - const nscale = NumCast(this.props.PanelWidth()) / scaling; + const scaling = (this.DocumentView?.().screenToViewTransform().Scale || 1) / NumCast(this.layoutDoc._freeform_scale, 1); + const nscale = NumCast(this._props.PanelWidth()) / scaling; const nw = nscale / NumCast(this.dataDoc[this.fieldKey + '_nativeWidth']); this.dataDoc[this.fieldKey + '_nativeHeight'] = NumCast(this.dataDoc[this.fieldKey + '_nativeHeight']) * nw; this.dataDoc[this.fieldKey + '_nativeWidth'] = NumCast(this.dataDoc[this.fieldKey + '_nativeWidth']) * nw; - this.rootDoc._freeform_panX = nw * NumCast(this.rootDoc._freeform_panX); - this.rootDoc._freeform_panY = nw * NumCast(this.rootDoc._freeform_panY); + this.layoutDoc._freeform_panX = nw * NumCast(this.layoutDoc._freeform_panX); + this.layoutDoc._freeform_panY = nw * NumCast(this.layoutDoc._freeform_panY); this.dataDoc._freeform_panXMax = this.dataDoc._freeform_panXMax ? nw * NumCast(this.dataDoc._freeform_panXMax) : undefined; this.dataDoc._freeform_panXMin = this.dataDoc._freeform_panXMin ? nw * NumCast(this.dataDoc._freeform_panXMin) : undefined; this.dataDoc._freeform_panYMax = this.dataDoc._freeform_panYMax ? nw * NumCast(this.dataDoc._freeform_panYMax) : undefined; @@ -205,29 +186,30 @@ export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp crop = (region: Doc | undefined, addCrop?: boolean) => { if (!region) return; const cropping = Doc.MakeCopy(region, true); - Doc.GetProto(region).lockedPosition = true; - Doc.GetProto(region).title = 'region:' + this.rootDoc.title; - Doc.GetProto(region).followLinkToggle = true; + const regionData = region[DocData]; + regionData.lockedPosition = true; + regionData.title = 'region:' + this.Document.title; + regionData.followLinkToggle = true; this.addDocument(region); const anchx = NumCast(cropping.x); const anchy = NumCast(cropping.y); const anchw = NumCast(cropping._width); const anchh = NumCast(cropping._height); - const viewScale = NumCast(this.rootDoc[this.fieldKey + '_nativeWidth']) / anchw; - cropping.title = 'crop: ' + this.rootDoc.title; - cropping.x = NumCast(this.rootDoc.x) + NumCast(this.rootDoc._width); - cropping.y = NumCast(this.rootDoc.y); - cropping._width = anchw * (this.props.NativeDimScaling?.() || 1); - cropping._height = anchh * (this.props.NativeDimScaling?.() || 1); + const viewScale = NumCast(this.dataDoc[this.fieldKey + '_nativeWidth']) / anchw; + cropping.title = 'crop: ' + this.Document.title; + cropping.x = NumCast(this.Document.x) + NumCast(this.layoutDoc._width); + cropping.y = NumCast(this.Document.y); + cropping._width = anchw * (this._props.NativeDimScaling?.() || 1); + cropping._height = anchh * (this._props.NativeDimScaling?.() || 1); cropping.onClick = undefined; - const croppingProto = Doc.GetProto(cropping); + const croppingProto = cropping[DocData]; croppingProto.annotationOn = undefined; croppingProto.isDataDoc = true; croppingProto.backgroundColor = undefined; - croppingProto.proto = Cast(this.rootDoc.proto, Doc, null)?.proto; // set proto of cropping's data doc to be IMAGE_PROTO + 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 = ObjectField.MakeCopy(this.rootDoc[this.fieldKey] as ObjectField); + croppingProto.data = ObjectField.MakeCopy(this.dataDoc[this.fieldKey] as ObjectField); croppingProto['data_nativeWidth'] = anchw; croppingProto['data_nativeHeight'] = anchh; croppingProto.freeform_scale = viewScale; @@ -240,12 +222,12 @@ export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp croppingProto.freeform_panY_max = anchh / viewScale; if (addCrop) { DocUtils.MakeLink(region, cropping, { link_relationship: 'cropped image' }); - cropping.x = NumCast(this.rootDoc.x) + this.rootDoc[Width](); - cropping.y = NumCast(this.rootDoc.y); - this.props.addDocTab(cropping, OpenWhere.inParent); + cropping.x = NumCast(this.Document.x) + NumCast(this.layoutDoc._width); + cropping.y = NumCast(this.Document.y); + this._props.addDocTab(cropping, OpenWhere.inParent); } DocumentManager.Instance.AddViewRenderedCb(cropping, dv => setTimeout(() => (dv.ComponentView as ImageBox).setNativeSize(), 200)); - this.props.bringToFront(cropping); + this._props.bringToFront?.(cropping); return cropping; }; @@ -262,55 +244,15 @@ export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp event: action(() => { ImageBox.setImageEditorOpen(true); ImageBox.setImageEditorSource(this.choosePath(field.url)); - ImageBox.addDoc = this.props.addDocument; - ImageBox.imageRootDoc = this.rootDoc; + ImageBox.addDoc = this._props.addDocument; + ImageBox.imageRootDoc = this.Document; }), icon: 'pencil-alt', }); - if (!Doc.noviceMode) { - funcs.push({ description: 'Export to Google Photos', event: () => GooglePhotos.Transactions.UploadImages([this.props.Document]), icon: 'caret-square-right' }); - - const existingAnalyze = ContextMenu.Instance?.findByDescription('Analyzers...'); - const modes: ContextMenuProps[] = existingAnalyze && 'subitems' in existingAnalyze ? existingAnalyze.subitems : []; - modes.push({ description: 'Generate Tags', event: this.generateMetadata, icon: 'tag' }); - modes.push({ description: 'Find Faces', event: this.extractFaces, icon: 'camera' }); - //modes.push({ description: "Recommend", event: this.extractText, icon: "brain" }); - !existingAnalyze && ContextMenu.Instance?.addItem({ description: 'Analyzers...', subitems: modes, icon: 'hand-point-right' }); - } - ContextMenu.Instance?.addItem({ description: 'Options...', subitems: funcs, icon: 'asterisk' }); } }; - extractFaces = () => { - const converter = (results: any) => { - return results.map((face: CognitiveServices.Image.Face) => Doc.Get.FromJson({ data: face, title: `Face: ${face.faceId}` })!); - }; - this.url && CognitiveServices.Image.Appliers.ProcessImage(this.dataDoc, [this.fieldKey + '-faces'], this.url, Service.Face, converter); - }; - - generateMetadata = (threshold: Confidence = Confidence.Excellent) => { - const converter = (results: any) => { - const tagDoc = new Doc(); - const tagsList = new List(); - results.tags.map((tag: Tag) => { - tagsList.push(tag.name); - const sanitized = tag.name.replace(' ', '_'); - tagDoc[sanitized] = ComputedField.MakeFunction(`(${tag.confidence} >= this.confidence) ? ${tag.confidence} : "${ComputedField.undefined}"`); - }); - this.dataDoc[this.fieldKey + '-generatedTags'] = tagsList; - tagDoc.title = 'Generated Tags Doc'; - tagDoc.confidence = threshold; - return tagDoc; - }; - this.url && CognitiveServices.Image.Appliers.ProcessImage(this.dataDoc, [this.fieldKey + '-generatedTagsDoc'], this.url, Service.ComputerVision, converter); - }; - - @computed private get url() { - const data = Cast(this.dataDoc[this.fieldKey], ImageField); - return data ? data.url?.href : undefined; - } - choosePath(url: URL) { if (!url?.href) return ''; const lower = url.href.toLowerCase(); @@ -319,62 +261,9 @@ export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp if (!/\.(png|jpg|jpeg|gif|webp)$/.test(lower)) return `/assets/unknown-file-icon-hi.png`; const ext = extname(url.href); - return url.href.replace(ext, this._curSuffix + ext); - } - - considerGooglePhotosLink = () => { - const remoteUrl = StrCast(this.dataDoc.googlePhotosUrl); // bcz: StrCast or URLCast??? - return !remoteUrl ? null : <img draggable={false} style={{ transformOrigin: 'bottom right' }} id={'google-photos'} src={'/assets/google_photos.png'} onClick={() => window.open(remoteUrl)} />; - }; - - considerGooglePhotosTags = () => { - const tags = this.dataDoc.googlePhotosTags; - return !tags ? null : <img id={'google-tags'} src={'/assets/google_tags.png'} />; - }; - - getScrollHeight = () => (this.props.layout_fitWidth?.(this.rootDoc) !== false && NumCast(this.rootDoc._freeform_scale, 1) === NumCast(this.rootDoc._freeform_scaleMin, 1) ? this.nativeSize.nativeHeight : undefined); - - @computed - private get considerDownloadIcon() { - const data = this.dataDoc[this.fieldKey]; - if (!(data instanceof ImageField)) { - return null; - } - const primary = data.url?.href; - if (primary.includes(window.location.origin)) { - return null; - } - return ( - <img - id={'upload-icon'} - draggable={false} - style={{ transformOrigin: 'bottom right' }} - src={`/assets/${this._uploadIcon}`} - onClick={async () => { - const { dataDoc } = this; - const { success, failure, idle, loading } = uploadIcons; - runInAction(() => (this._uploadIcon = loading)); - const [{ accessPaths }] = await Networking.PostToServer('/uploadRemoteImage', { sources: [primary] }); - dataDoc[this.props.fieldKey + '-originalUrl'] = primary; - let succeeded = true; - let data: ImageField | undefined; - try { - data = new ImageField(accessPaths.agnostic.client); - } catch { - succeeded = false; - } - runInAction(() => (this._uploadIcon = succeeded ? success : failure)); - setTimeout( - action(() => { - this._uploadIcon = idle; - data && (dataDoc[this.fieldKey] = data); - }), - 2000 - ); - }} - /> - ); + 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(); @@ -384,7 +273,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp return { nativeWidth, nativeHeight, nativeOrientation }; } @computed get overlayImageIcon() { - const usePath = this.rootDoc[`_${this.fieldKey}_usePath`]; + const usePath = this.layoutDoc[`_${this.fieldKey}_usePath`]; return ( <Tooltip title={ @@ -405,9 +294,9 @@ export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp <div className="imageBox-alternateDropTarget" ref={this._overlayIconRef} - onPointerDown={e => setupMoveUpEvents(e.target, e, returnFalse, emptyFunction, e => (this.rootDoc[`_${this.fieldKey}_usePath`] = usePath === undefined ? 'alternate' : usePath === 'alternate' ? 'alternate:hover' : undefined))} + onPointerDown={e => setupMoveUpEvents(e.target, e, returnFalse, emptyFunction, e => (this.layoutDoc[`_${this.fieldKey}_usePath`] = usePath === undefined ? 'alternate' : usePath === 'alternate' ? 'alternate:hover' : undefined))} style={{ - display: (this.props.isContentActive() !== false && DragManager.DocDragData?.canEmbed) || DocListCast(this.dataDoc[this.fieldKey + '-alternates']).length ? 'block' : 'none', + display: (this._props.isContentActive() !== false && DragManager.DocDragData?.canEmbed) || DocListCast(this.dataDoc[this.fieldKey + '-alternates']).length ? 'block' : 'none', width: 'min(10%, 25px)', height: 'min(10%, 25px)', background: usePath === undefined ? 'white' : usePath === 'alternate' ? 'black' : 'gray', @@ -430,11 +319,13 @@ export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp return paths.length ? paths : [Utils.CorsProxy('https://cs.brown.edu/~bcz/noImage.png')]; } + @observable _error = ''; + @observable _isHovering = false; // flag to switch between primary and alternate images on hover @computed get content() { TraceMobx(); - const backColor = DashColor(this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.BackgroundColor)); + const backColor = DashColor(this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.BackgroundColor)); const backAlpha = backColor.red() === 0 && backColor.green() === 0 && backColor.blue() === 0 ? backColor.alpha() : 1; const srcpath = this.layoutDoc.hideImage ? '' : this.paths[0]; const fadepath = this.layoutDoc.hideImage ? '' : this.paths.lastElement(); @@ -452,21 +343,18 @@ export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp transformOrigin = 'right top'; transform = `translate(-100%, 0%) rotate(${rotation}deg) scale(${aspect})`; } - const usePath = this.rootDoc[`_${this.fieldKey}_usePath`]; + 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-fader" style={{ opacity: backAlpha }}> - <img key="paths" src={srcpath} style={{ transform, transformOrigin }} draggable={false} width={nativeWidth} /> + <img 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} /> </div> )} </div> - {!Doc.noviceMode && this.considerDownloadIcon} - {this.considerGooglePhotosLink()} - <FaceRectangles document={this.dataDoc} color={'#0000FF'} backgroundColor={'#0000FF'} /> {this.overlayImageIcon} </div> ); @@ -474,21 +362,20 @@ export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp private _mainCont: React.RefObject<HTMLDivElement> = React.createRef(); private _annotationLayer: React.RefObject<HTMLDivElement> = React.createRef(); - @observable _marqueeing: number[] | undefined; @observable _savedAnnotations = new ObservableMap<number, HTMLDivElement[]>(); @computed get annotationLayer() { TraceMobx(); - return <div className="imageBox-annotationLayer" style={{ height: this.props.PanelHeight() }} ref={this._annotationLayer} />; + return <div className="imageBox-annotationLayer" style={{ height: this._props.PanelHeight() }} ref={this._annotationLayer} />; } - screenToLocalTransform = () => this.props.ScreenToLocalTransform().translate(0, NumCast(this.layoutDoc._layout_scrollTop) * this.props.ScreenToLocalTransform().Scale); + screenToLocalTransform = () => this.ScreenToLocalBoxXf().translate(0, NumCast(this.layoutDoc._layout_scrollTop) * this.ScreenToLocalBoxXf().Scale); marqueeDown = (e: React.PointerEvent) => { - if (!e.altKey && e.button === 0 && NumCast(this.rootDoc._freeform_scale, 1) <= NumCast(this.rootDoc.freeform_scaleMin, 1) && this.props.isContentActive(true) && ![InkTool.Highlighter, InkTool.Pen, InkTool.Write].includes(Doc.ActiveTool)) { + if (!e.altKey && e.button === 0 && NumCast(this.layoutDoc._freeform_scale, 1) <= NumCast(this.dataDoc.freeform_scaleMin, 1) && this._props.isContentActive() && ![InkTool.Highlighter, InkTool.Pen, InkTool.Write].includes(Doc.ActiveTool)) { setupMoveUpEvents( this, e, action(e => { MarqueeAnnotator.clearAnnotations(this._savedAnnotations); - this._marqueeing = [e.clientX, e.clientY]; + this._marqueeref.current?.onInitiateSelection([e.clientX, e.clientY]); return true; }), returnFalse, @@ -500,17 +387,17 @@ export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp @action finishMarquee = () => { this._getAnchor = AnchorMenu.Instance?.GetAnchor; - this._marqueeing = undefined; - this.props.select(false); + this._marqueeref.current?.onTerminateSelection(); + this._props.select(false); }; - focus = (anchor: Doc, options: DocFocusOptions) => (anchor.type === DocumentType.CONFIG ? undefined : this._ffref.current?.focus(anchor, options)); + 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(); - const borderRad = this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.BorderRounding); - const borderRadius = borderRad?.includes('px') ? `${Number(borderRad.split('px')[0]) / (this.props.NativeDimScaling?.() || 1)}px` : borderRad; + const borderRad = this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.BorderRounding); + const borderRadius = borderRad?.includes('px') ? `${Number(borderRad.split('px')[0]) / (this._props.NativeDimScaling?.() || 1)}px` : borderRad; return ( <div className="imageBox" @@ -526,25 +413,25 @@ export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp } })} style={{ - width: this.props.PanelWidth() ? undefined : `100%`, - height: this.props.PanelWidth() ? undefined : `100%`, + width: this._props.PanelWidth() ? undefined : `100%`, + height: this._props.PanelWidth() ? undefined : `100%`, pointerEvents: this.layoutDoc._lockedPosition ? 'none' : undefined, borderRadius, - overflow: this.layoutDoc.layout_fitWidth || this.props.layout_fitWidth?.(this.rootDoc) ? 'auto' : undefined, + overflow: this.layoutDoc.layout_fitWidth || this._props.layout_fitWidth?.(this.Document) ? 'auto' : undefined, }}> <CollectionFreeFormView ref={this._ffref} - {...this.props} - setContentView={emptyFunction} + {...this._props} + setContentViewBox={emptyFunction} NativeWidth={returnZero} NativeHeight={returnZero} - renderDepth={this.props.renderDepth + 1} + renderDepth={this._props.renderDepth + 1} fieldKey={this.annotationKey} - styleProvider={this.props.styleProvider} + styleProvider={this._props.styleProvider} isAnnotationOverlay={true} annotationLayerHostsContent={true} - PanelWidth={this.props.PanelWidth} - PanelHeight={this.props.PanelHeight} + PanelWidth={this._props.PanelWidth} + PanelHeight={this._props.PanelHeight} ScreenToLocalTransform={this.screenToLocalTransform} select={emptyFunction} focus={this.focus} @@ -558,19 +445,21 @@ export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp {this.content} </CollectionFreeFormView> {this.annotationLayer} - {!this._marqueeing || !this._mainCont.current || !this._annotationLayer.current ? null : ( + {!this._mainCont.current || !this.DocumentView || !this._annotationLayer.current ? null : ( <MarqueeAnnotator - rootDoc={this.rootDoc} + Document={this.Document} + ref={this._marqueeref} scrollTop={0} - down={this._marqueeing} - scaling={this.props.NativeDimScaling} - docView={this.props.docViewPath().slice(-1)[0]} + annotationLayerScrollTop={0} + scaling={returnOne} + annotationLayerScaling={this._props.NativeDimScaling} + docView={this.DocumentView} addDocument={this.addDocument} finishMarquee={this.finishMarquee} savedAnnotations={this.savedAnnotations} selectionText={returnEmptyString} annotationLayer={this._annotationLayer.current} - mainCont={this._mainCont.current} + marqueeContainer={this._mainCont.current} highlightDragSrcColor={''} anchorMenuCrop={this.crop} /> diff --git a/src/client/views/nodes/KeyValueBox.scss b/src/client/views/nodes/KeyValueBox.scss index ffcba4981..a44f614b2 100644 --- a/src/client/views/nodes/KeyValueBox.scss +++ b/src/client/views/nodes/KeyValueBox.scss @@ -1,7 +1,7 @@ -@import "../global/globalCssVariables"; +@import '../global/globalCssVariables.module.scss'; .keyValueBox-cont { overflow-y: scroll; - width:100%; + width: 100%; height: 100%; background-color: $white; border: 1px solid $medium-gray; @@ -15,45 +15,45 @@ } $header-height: 30px; .keyValueBox-tbody { - width:100%; - height:100%; + width: 100%; + height: 100%; position: absolute; overflow-y: scroll; } .keyValueBox-key { display: inline-block; - height:100%; - width:50%; + height: 100%; + width: 50%; text-align: center; } .keyValueBox-fields { display: inline-block; - height:100%; - width:50%; + height: 100%; + width: 50%; text-align: center; } .keyValueBox-table { position: absolute; - width:100%; - height:100%; + width: 100%; + height: 100%; border-collapse: collapse; } .keyValueBox-td-key { - display:inline-block; - height:30px; + display: inline-block; + height: 30px; } .keyValueBox-td-value { - display:inline-block; - height:30px; + display: inline-block; + height: 30px; } .keyValueBox-valueRow { - width:100%; - height:30px; + width: 100%; + height: 30px; display: inline-block; } .keyValueBox-header { - width:100%; + width: 100%; position: relative; display: inline-block; background: $medium-gray; @@ -74,8 +74,8 @@ $header-height: 30px; .keyValueBox-evenRow { position: relative; display: flex; - width:100%; - height:$header-height; + width: 100%; + height: $header-height; background: $white; .formattedTextBox-cont { background: $white; @@ -86,10 +86,10 @@ $header-height: 30px; position: relative; } } -.keyValueBox-dividerDraggerThumb{ +.keyValueBox-dividerDraggerThumb { position: relative; width: 4px; - float: left; + float: left; height: 30px; width: 5px; z-index: 20; @@ -99,10 +99,10 @@ $header-height: 30px; background: black; pointer-events: all; } -.keyValueBox-dividerDragger{ - position: relative; +.keyValueBox-dividerDragger { + position: relative; width: 100%; - float: left; + float: left; height: 37px; z-index: 20; right: 0; @@ -114,10 +114,10 @@ $header-height: 30px; .keyValueBox-oddRow { position: relative; display: flex; - width:100%; - height:30px; + width: 100%; + height: 30px; background: $light-gray; .formattedTextBox-cont { background: $light-gray; } -}
\ No newline at end of file +} diff --git a/src/client/views/nodes/KeyValueBox.tsx b/src/client/views/nodes/KeyValueBox.tsx index 673f711be..39a45693e 100644 --- a/src/client/views/nodes/KeyValueBox.tsx +++ b/src/client/views/nodes/KeyValueBox.tsx @@ -1,26 +1,27 @@ -import { action, computed, observable } from 'mobx'; +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 { 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 { returnAll, returnAlways, returnTrue } from '../../../Utils'; import { Docs } from '../../documents/Documents'; import { SetupDrag } from '../../util/DragManager'; -import { CompiledScript, CompileScript, ScriptOptions } from '../../util/Scripting'; +import { CompileScript, CompiledScript, ScriptOptions } from '../../util/Scripting'; import { undoBatch } from '../../util/UndoManager'; import { ContextMenu } from '../ContextMenu'; import { ContextMenuProps } from '../ContextMenuItem'; +import { ObservableReactComponent } from '../ObservableReactComponent'; import { DocumentIconContainer } from './DocumentIcon'; import { OpenWhere } from './DocumentView'; import { FieldView, FieldViewProps } from './FieldView'; -import { FormattedTextBox } from './formattedText/FormattedTextBox'; import { ImageBox } from './ImageBox'; import './KeyValueBox.scss'; import { KeyValuePair } from './KeyValuePair'; -import React = require('react'); +import { FormattedTextBox } from './formattedText/FormattedTextBox'; export type KVPScript = { script: CompiledScript; @@ -28,10 +29,14 @@ export type KVPScript = { onDelegate: boolean; }; @observer -export class KeyValueBox extends React.Component<FieldViewProps> { +export class KeyValueBox extends ObservableReactComponent<FieldViewProps> { public static LayoutString() { return FieldView.LayoutString(KeyValueBox, 'data'); } + constructor(props: any) { + super(props); + makeObservable(this); + } private _mainCont = React.createRef<HTMLDivElement>(); private _keyHeader = React.createRef<HTMLTableHeaderCellElement>(); @@ -39,19 +44,18 @@ export class KeyValueBox extends React.Component<FieldViewProps> { private _valInput = React.createRef<HTMLInputElement>(); componentDidMount() { - this.props.setContentView?.(this); + this._props.setContentViewBox?.(this); } - reverseNativeScaling = returnTrue; + isKeyValueBox = returnTrue; able = returnAlways; layout_fitWidth = returnTrue; - overridePointerEvents = returnAll; onClickScriptDisable = returnAlways; @observable private rows: KeyValuePair[] = []; @observable _splitPercentage = 50; get fieldDocToLayout() { - return this.props.fieldKey ? DocCast(this.props.Document[this.props.fieldKey], DocCast(this.props.Document)) : this.props.Document; + return this._props.fieldKey ? DocCast(this._props.Document[this._props.fieldKey], DocCast(this._props.Document)) : this._props.Document; } @action @@ -110,7 +114,7 @@ export class KeyValueBox extends React.Component<FieldViewProps> { } onPointerDown = (e: React.PointerEvent): void => { - if (e.buttons === 1 && this.props.isSelected(true)) { + if (e.buttons === 1 && this._props.isSelected()) { e.stopPropagation(); } }; @@ -156,8 +160,8 @@ export class KeyValueBox extends React.Component<FieldViewProps> { rows.push( <KeyValuePair doc={realDoc} - addDocTab={this.props.addDocTab} - PanelWidth={this.props.PanelWidth} + addDocTab={this._props.addDocTab} + PanelWidth={this._props.PanelWidth} PanelHeight={this.rowHeight} ref={(function () { let oldEl: KeyValuePair | undefined; @@ -221,19 +225,19 @@ export class KeyValueBox extends React.Component<FieldViewProps> { getFieldView = () => { const rows = this.rows.filter(row => row.isChecked); if (rows.length > 1) { - const parent = Docs.Create.StackingDocument([], { _layout_autoHeight: true, _width: 300, title: `field views for ${DocCast(this.props.Document).title}`, _chromeHidden: true }); + const parent = Docs.Create.StackingDocument([], { _layout_autoHeight: true, _width: 300, title: `field views for ${DocCast(this._props.Document).title}`, _chromeHidden: true }); for (const row of rows) { - const field = this.createFieldView(DocCast(this.props.Document), row); + const field = this.createFieldView(DocCast(this._props.Document), row); field && Doc.AddDocToList(parent, 'data', field); row.uncheck(); } return parent; } - return rows.length ? this.createFieldView(DocCast(this.props.Document), rows.lastElement()) : undefined; + return rows.length ? this.createFieldView(DocCast(this._props.Document), rows.lastElement()) : undefined; }; createFieldView = (templateDoc: Doc, row: KeyValuePair) => { - const metaKey = row.props.keyName; + const metaKey = row._props.keyName; const fieldTemplate = Doc.IsDelegateField(templateDoc, metaKey) ? Doc.MakeDelegate(templateDoc) : Doc.MakeEmbedding(templateDoc); fieldTemplate.title = metaKey; fieldTemplate.layout_fitWidth = true; @@ -279,8 +283,8 @@ export class KeyValueBox extends React.Component<FieldViewProps> { openItems.push({ description: 'Default Perspective', event: () => { - this.props.addDocTab(this.props.Document, OpenWhere.close); - this.props.addDocTab(this.fieldDocToLayout, OpenWhere.addRight); + this._props.addDocTab(this._props.Document, OpenWhere.close); + this._props.addDocTab(this.fieldDocToLayout, OpenWhere.addRight); }, icon: 'image', }); diff --git a/src/client/views/nodes/KeyValuePair.scss b/src/client/views/nodes/KeyValuePair.scss index 57d36932e..46ea9c18e 100644 --- a/src/client/views/nodes/KeyValuePair.scss +++ b/src/client/views/nodes/KeyValuePair.scss @@ -1,4 +1,4 @@ -@import '../global/globalCssVariables'; +@import '../global/globalCssVariables.module.scss'; .keyValuePair-td-key { display: inline-block; @@ -26,6 +26,7 @@ position: relative; overflow: auto; display: inline; + align-self: center; } } } diff --git a/src/client/views/nodes/KeyValuePair.tsx b/src/client/views/nodes/KeyValuePair.tsx index f22cb195f..f9e8ce4f3 100644 --- a/src/client/views/nodes/KeyValuePair.tsx +++ b/src/client/views/nodes/KeyValuePair.tsx @@ -1,21 +1,21 @@ -import { action, observable } from 'mobx'; +import { Tooltip } from '@mui/material'; +import { action, makeObservable, observable } from 'mobx'; import { observer } from 'mobx-react'; -import { Doc, Field } from '../../../fields/Doc'; +import * as React from 'react'; import { emptyFunction, returnEmptyDoclist, returnEmptyFilter, returnFalse, returnZero } from '../../../Utils'; +import { Doc, Field } from '../../../fields/Doc'; +import { DocCast } from '../../../fields/Types'; +import { DocumentOptions, FInfo } from '../../documents/Documents'; import { Transform } from '../../util/Transform'; import { undoBatch } from '../../util/UndoManager'; import { ContextMenu } from '../ContextMenu'; import { EditableView } from '../EditableView'; +import { ObservableReactComponent } from '../ObservableReactComponent'; import { DefaultStyleProvider } from '../StyleProvider'; -import { OpenWhere } from './DocumentView'; -import { FieldView, FieldViewProps } from './FieldView'; +import { OpenWhere, returnEmptyDocViewList } from './DocumentView'; import { KeyValueBox } from './KeyValueBox'; import './KeyValueBox.scss'; import './KeyValuePair.scss'; -import React = require('react'); -import { DocCast } from '../../../fields/Types'; -import { Tooltip } from '@material-ui/core'; -import { DocumentOptions, FInfo } from '../../documents/Documents'; // Represents one row in a key value plane @@ -29,10 +29,14 @@ export interface KeyValuePairProps { addDocTab: (doc: Doc, where: OpenWhere) => boolean; } @observer -export class KeyValuePair extends React.Component<KeyValuePairProps> { +export class KeyValuePair extends ObservableReactComponent<KeyValuePairProps> { @observable private isPointerOver = false; @observable public isChecked = false; private checkbox = React.createRef<HTMLInputElement>(); + constructor(props: any) { + super(props); + makeObservable(this); + } @action handleCheck = (e: React.ChangeEvent<HTMLInputElement>) => { @@ -46,46 +50,21 @@ export class KeyValuePair extends React.Component<KeyValuePairProps> { }; onContextMenu = (e: React.MouseEvent) => { - const value = this.props.doc[this.props.keyName]; + const value = this._props.doc[this._props.keyName]; if (value instanceof Doc) { e.stopPropagation(); e.preventDefault(); - ContextMenu.Instance.addItem({ description: 'Open Fields', event: () => this.props.addDocTab(value, ((OpenWhere.addRight as string) + 'KeyValue') as OpenWhere), icon: 'layer-group' }); + ContextMenu.Instance.addItem({ description: 'Open Fields', event: () => this._props.addDocTab(value, OpenWhere.addRightKeyvalue), icon: 'layer-group' }); ContextMenu.Instance.displayMenu(e.clientX, e.clientY); } }; render() { - const props: FieldViewProps = { - Document: this.props.doc, - DataDoc: this.props.doc, - childFilters: returnEmptyFilter, - childFiltersByRanges: returnEmptyFilter, - searchFilterDocs: returnEmptyDoclist, - styleProvider: DefaultStyleProvider, - docViewPath: returnEmptyDoclist, - fieldKey: this.props.keyName, - rootSelected: returnFalse, - isSelected: returnFalse, - setHeight: returnFalse, - select: emptyFunction, - bringToFront: emptyFunction, - renderDepth: 1, - isContentActive: returnFalse, - whenChildContentsActiveChanged: emptyFunction, - ScreenToLocalTransform: Transform.Identity, - focus: emptyFunction, - PanelWidth: this.props.PanelWidth, - PanelHeight: this.props.PanelHeight, - addDocTab: returnFalse, - pinToPres: returnZero, - }; - const contents = <FieldView {...props} />; // let fieldKey = Object.keys(props.Document).indexOf(props.fieldKey) !== -1 ? props.fieldKey : "(" + props.fieldKey + ")"; let protoCount = 0; - let doc: Doc | undefined = props.Document; + let doc = this._props.doc; while (doc) { - if (Object.keys(doc).includes(props.fieldKey)) { + if (Object.keys(doc).includes(this._props.keyName)) { break; } protoCount++; @@ -97,32 +76,57 @@ export class KeyValuePair extends React.Component<KeyValuePairProps> { const hover = { transition: '0.3s ease opacity', opacity: this.isPointerOver || this.isChecked ? 1 : 0 }; return ( - <tr className={this.props.rowStyle} onPointerEnter={action(() => (this.isPointerOver = true))} onPointerLeave={action(() => (this.isPointerOver = false))}> - <td className="keyValuePair-td-key" style={{ width: `${this.props.keyWidth}%` }}> + <tr className={this._props.rowStyle} onPointerEnter={action(() => (this.isPointerOver = true))} onPointerLeave={action(() => (this.isPointerOver = false))}> + <td className="keyValuePair-td-key" style={{ width: `${this._props.keyWidth}%` }}> <div className="keyValuePair-td-key-container"> <button style={hover} className="keyValuePair-td-key-delete" onClick={undoBatch(() => { - if (Object.keys(props.Document).indexOf(props.fieldKey) !== -1) { - delete props.Document[props.fieldKey]; - } else delete DocCast(props.Document.proto)?.[props.fieldKey]; + if (Object.keys(this._props.doc).indexOf(this._props.keyName) !== -1) { + delete this._props.doc[this._props.keyName]; + } else delete DocCast(this._props.doc.proto)?.[this._props.keyName]; })}> X </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(/^_/, '') === props.fieldKey)?.[1].description ?? ''}> - <div className="keyValuePair-keyField" style={{ marginLeft: 20 * (props.fieldKey.match(/_/g)?.length || 0), color: keyStyle }}> + <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 }}> {'('.repeat(parenCount)} - {props.fieldKey} + {this._props.keyName} {')'.repeat(parenCount)} </div> </Tooltip> </div> </td> - <td className="keyValuePair-td-value" style={{ width: `${100 - this.props.keyWidth}%` }} onContextMenu={this.onContextMenu}> + <td className="keyValuePair-td-value" style={{ width: `${100 - this._props.keyWidth}%` }} onContextMenu={this.onContextMenu}> <div className="keyValuePair-td-value-container"> - <EditableView contents={contents} GetValue={() => Field.toKeyValueString(props.Document, props.fieldKey)} SetValue={(value: string) => KeyValueBox.SetField(props.Document, props.fieldKey, value)} /> + <EditableView + contents={undefined} + fieldContents={{ + Document: this._props.doc, + childFilters: returnEmptyFilter, + childFiltersByRanges: returnEmptyFilter, + searchFilterDocs: returnEmptyDoclist, + styleProvider: DefaultStyleProvider, + docViewPath: returnEmptyDocViewList, + fieldKey: this._props.keyName, + isSelected: returnFalse, + setHeight: returnFalse, + select: emptyFunction, + renderDepth: 1, + isContentActive: returnFalse, + whenChildContentsActiveChanged: emptyFunction, + ScreenToLocalTransform: Transform.Identity, + focus: emptyFunction, + PanelWidth: this._props.PanelWidth, + PanelHeight: this._props.PanelHeight, + addDocTab: returnFalse, + pinToPres: returnZero, + }} + GetValue={() => Field.toKeyValueString(this._props.doc, this._props.keyName)} + SetValue={(value: string) => KeyValueBox.SetField(this._props.doc, this._props.keyName, value)} + /> </div> </td> </tr> diff --git a/src/client/views/nodes/LabelBox.scss b/src/client/views/nodes/LabelBox.scss index 42e158584..0b195713d 100644 --- a/src/client/views/nodes/LabelBox.scss +++ b/src/client/views/nodes/LabelBox.scss @@ -18,6 +18,9 @@ display: inline-block; margin: auto; text-overflow: ellipsis; + > span { + max-height: 100%; // make sure top of text is in view, otherwise it would center on middle of large text span + } } .labelBox-params { @@ -29,4 +32,4 @@ width: 100%; background: lightgray; border: dimgray solid 1px; -}
\ No newline at end of file +} diff --git a/src/client/views/nodes/LabelBox.tsx b/src/client/views/nodes/LabelBox.tsx index 4439be0cd..10eeff08d 100644 --- a/src/client/views/nodes/LabelBox.tsx +++ b/src/client/views/nodes/LabelBox.tsx @@ -1,10 +1,10 @@ -import { action, computed, observable } from 'mobx'; +import { action, computed, makeObservable, observable } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; -import { Doc, DocListCast } from '../../../fields/Doc'; +import { Doc, DocListCast, Field } from '../../../fields/Doc'; import { List } from '../../../fields/List'; import { listSpec } from '../../../fields/Schema'; -import { Cast, StrCast, NumCast, BoolCast } from '../../../fields/Types'; +import { BoolCast, Cast, NumCast, StrCast } from '../../../fields/Types'; import { DragManager } from '../../util/DragManager'; import { undoBatch } from '../../util/UndoManager'; import { ContextMenu } from '../ContextMenu'; @@ -14,13 +14,15 @@ import { StyleProp } from '../StyleProvider'; import { FieldView, FieldViewProps } from './FieldView'; import BigText from './LabelBigText'; import './LabelBox.scss'; +import { PinProps, PresBox } from './trails'; +import { Docs } from '../../documents/Documents'; -export interface LabelBoxProps { +export interface LabelBoxProps extends FieldViewProps { label?: string; } @observer -export class LabelBox extends ViewBoxBaseComponent<FieldViewProps & LabelBoxProps>() { +export class LabelBox extends ViewBoxBaseComponent<LabelBoxProps>() { public static LayoutString(fieldKey: string) { return FieldView.LayoutString(LabelBox, fieldKey); } @@ -29,21 +31,27 @@ export class LabelBox extends ViewBoxBaseComponent<FieldViewProps & LabelBoxProp } private dropDisposer?: DragManager.DragDropDisposer; private _timeout: any; + + constructor(props: LabelBoxProps) { + super(props); + makeObservable(this); + } + componentDidMount() { - this.props.setContentView?.(this); + this._props.setContentViewBox?.(this); } componentWillUnMount() { this._timeout && clearTimeout(this._timeout); } - getTitle() { - return this.rootDoc.title_custom ? StrCast(this.rootDoc.title) : this.props.label ? this.props.label : typeof this.rootDoc[this.fieldKey] === 'string' ? StrCast(this.rootDoc[this.fieldKey]) : StrCast(this.rootDoc.title); + @computed get Title() { + return this.dataDoc.title_custom ? StrCast(this.Document.title) : this._props.label ? this._props.label : Field.toString(this.dataDoc[this.fieldKey] as Field); } protected createDropTarget = (ele: HTMLDivElement) => { this.dropDisposer?.(); if (ele) { - this.dropDisposer = DragManager.MakeDropTarget(ele, this.drop.bind(this), this.props.Document); + this.dropDisposer = DragManager.MakeDropTarget(ele, this.drop.bind(this), this.Document); } }; @@ -66,7 +74,6 @@ export class LabelBox extends ViewBoxBaseComponent<FieldViewProps & LabelBoxProp }; @undoBatch - @action drop = (e: Event, de: DragManager.DropEvent) => { const docDragData = de.complete.docDragData; const params = Cast(this.paramsDoc['onClick-paramFieldKeys'], listSpec('string'), []); @@ -81,16 +88,44 @@ export class LabelBox extends ViewBoxBaseComponent<FieldViewProps & LabelBoxProp @observable _mouseOver = false; @computed get hoverColor() { - return this._mouseOver ? StrCast(this.layoutDoc._hoverBackgroundColor) : this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.BackgroundColor); + return this._mouseOver ? StrCast(this.layoutDoc._hoverBackgroundColor) : this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.BackgroundColor); } - fitTextToBox = (r: any) => { - const singleLine = BoolCast(this.rootDoc._singleLine, true); + getAnchor = (addAsAnnotation: boolean, pinProps?: PinProps) => { + if (!pinProps) return this.Document; + const anchor = Docs.Create.ConfigDocument({ title: StrCast(this.Document.title), annotationOn: this.Document }); + + if (anchor) { + if (!addAsAnnotation) anchor.backgroundColor = 'transparent'; + // addAsAnnotation && this.addDocument(anchor); + PresBox.pinDocView(anchor, { pinDocLayout: pinProps?.pinDocLayout, pinData: { ...(pinProps?.pinData ?? {}) } }, this.Document); + return anchor; + } + return anchor; + }; + + fitTextToBox = ( + r: any + ): + | NodeJS.Timeout + | { + rotateText: null; + fontSizeFactor: number; + minimumFontSize: number; + maximumFontSize: number; + limitingDimension: string; + horizontalAlign: string; + verticalAlign: string; + textAlign: string; + singleLine: boolean; + whiteSpace: string; + } => { + const singleLine = BoolCast(this.layoutDoc._singleLine, true); const params = { rotateText: null, fontSizeFactor: 1, - minimumFontSize: NumCast(this.rootDoc._label_minFontSize, 8), - maximumFontSize: NumCast(this.rootDoc._label_maxFontSize, 1000), + minimumFontSize: NumCast(this.layoutDoc._label_minFontSize, 8), + maximumFontSize: NumCast(this.layoutDoc._label_maxFontSize, 1000), limitingDimension: 'both', horizontalAlign: 'center', verticalAlign: 'center', @@ -119,7 +154,7 @@ export class LabelBox extends ViewBoxBaseComponent<FieldViewProps & LabelBoxProp const params = Cast(this.paramsDoc['onClick-paramFieldKeys'], listSpec('string'), []); const missingParams = params?.filter(p => !this.paramsDoc[p]); params?.map(p => DocListCast(this.paramsDoc[p])); // bcz: really hacky form of prefetching ... - const label = this.getTitle(); + const label = this.Title; return ( <div className="labelBox-outerDiv" @@ -127,7 +162,7 @@ export class LabelBox extends ViewBoxBaseComponent<FieldViewProps & LabelBoxProp onMouseOver={action(() => (this._mouseOver = true))} ref={this.createDropTarget} onContextMenu={this.specificContextMenu} - style={{ boxShadow: this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.BoxShadow) }}> + style={{ boxShadow: this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.BoxShadow) }}> <div className="labelBox-mainButton" style={{ @@ -137,15 +172,15 @@ export class LabelBox extends ViewBoxBaseComponent<FieldViewProps & LabelBoxProp fontFamily: StrCast(this.layoutDoc._text_fontFamily) || 'inherit', letterSpacing: StrCast(this.layoutDoc.letterSpacing), textTransform: StrCast(this.layoutDoc.textTransform) as any, - paddingLeft: NumCast(this.rootDoc._xPadding), - paddingRight: NumCast(this.rootDoc._xPadding), - paddingTop: NumCast(this.rootDoc._yPadding), - paddingBottom: NumCast(this.rootDoc._yPadding), - width: this.props.PanelWidth(), - height: this.props.PanelHeight(), - whiteSpace: typeof boxParams !== 'number' && boxParams.singleLine ? 'pre' : 'pre-wrap', + paddingLeft: NumCast(this.layoutDoc._xPadding), + paddingRight: NumCast(this.layoutDoc._xPadding), + paddingTop: NumCast(this.layoutDoc._yPadding), + paddingBottom: NumCast(this.layoutDoc._yPadding), + width: this._props.PanelWidth(), + height: this._props.PanelHeight(), + whiteSpace: 'singleLine' in boxParams && boxParams.singleLine ? 'pre' : 'pre-wrap', }}> - <span style={{ width: typeof boxParams !== 'number' && boxParams.singleLine ? '' : '100%' }} ref={action((r: any) => this.fitTextToBox(r))}> + <span style={{ width: 'singleLine' in boxParams ? '' : '100%' }} ref={action((r: any) => this.fitTextToBox(r))}> {label.startsWith('#') ? null : label.replace(/([^a-zA-Z])/g, '$1\u200b')} </span> </div> diff --git a/src/client/views/nodes/LinkAnchorBox.tsx b/src/client/views/nodes/LinkAnchorBox.tsx index fd7d13655..00e1f04c5 100644 --- a/src/client/views/nodes/LinkAnchorBox.tsx +++ b/src/client/views/nodes/LinkAnchorBox.tsx @@ -1,9 +1,10 @@ -import { action, computed, observable } from 'mobx'; +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 { emptyFunction, setupMoveUpEvents, Utils } from '../../../Utils'; import { DragManager } from '../../util/DragManager'; import { LinkFollower } from '../../util/LinkFollower'; import { SelectionManager } from '../../util/SelectionManager'; @@ -11,10 +12,8 @@ import { ViewBoxBaseComponent } from '../DocComponent'; import { StyleProp } from '../StyleProvider'; import { FieldView, FieldViewProps } from './FieldView'; import './LinkAnchorBox.scss'; -import { LinkDocPreview } from './LinkDocPreview'; -import React = require('react'); -import globalCssVariables = require('../global/globalCssVariables.scss'); - +import { LinkInfo } from './LinkDocPreview'; +const { default: { MEDIUM_GRAY }, } = require('../global/globalCssVariables.module.scss'); // prettier-ignore @observer export class LinkAnchorBox extends ViewBoxBaseComponent<FieldViewProps>() { public static LayoutString(fieldKey: string) { @@ -25,22 +24,27 @@ export class LinkAnchorBox extends ViewBoxBaseComponent<FieldViewProps>() { _ref = React.createRef<HTMLDivElement>(); _isOpen = false; _timeout: NodeJS.Timeout | undefined; - @observable _x = 0; - @observable _y = 0; + + constructor(props: FieldViewProps) { + super(props); + makeObservable(this); + } componentDidMount() { - this.props.setContentView?.(this); + this._props.setContentViewBox?.(this); } @computed get linkSource() { - return this.props.docViewPath()[this.props.docViewPath().length - 2].rootDoc; // this.props.styleProvider?.(this.dataDoc, this.props, StyleProp.LinkSource); + return this.DocumentView?.().containerViewPath?.().lastElement().Document; // this._props.styleProvider?.(this.dataDoc, this._props, StyleProp.LinkSource); } onPointerDown = (e: React.PointerEvent) => { - setupMoveUpEvents(this, e, this.onPointerMove, emptyFunction, (e, doubleTap) => { - if (doubleTap) LinkFollower.FollowLink(this.rootDoc, this.linkSource, false); - else this.props.select(false); - }); + 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; @@ -49,15 +53,15 @@ export class LinkAnchorBox extends ViewBoxBaseComponent<FieldViewProps>() { 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.rootDoc]); + const dragData = new DragManager.DocumentDragData([this.Document]); dragData.dropAction = '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.rootDoc[this.fieldKey + '_x'] = ((pt[0] - bounds.left) / bounds.width) * 100; - this.rootDoc[this.fieldKey + '_y'] = ((pt[1] - bounds.top) / bounds.height) * 100; - this.rootDoc.link_autoMoveAnchors = false; + 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; @@ -67,28 +71,29 @@ export class LinkAnchorBox extends ViewBoxBaseComponent<FieldViewProps>() { render() { TraceMobx(); - const small = this.props.PanelWidth() <= 1; // this happens when rendered in a treeView - const x = NumCast(this.rootDoc[this.fieldKey + '_x'], 100); - const y = NumCast(this.rootDoc[this.fieldKey + '_y'], 100); - const background = this.props.styleProvider?.(this.dataDoc, this.props, StyleProp.BackgroundColor + ':anchor'); + 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') + 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' - : ''; + : 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 => - LinkDocPreview.SetLinkInfo({ - docProps: this.props, + LinkInfo.SetLinkInfo({ + DocumentView: this.DocumentView, + styleProvider: this._props.styleProvider, linkSrc: this.linkSource, - linkDoc: this.rootDoc, + linkDoc: this.Document, showHeader: true, location: [e.clientX, e.clientY + 20], noPreview: false, @@ -97,7 +102,7 @@ export class LinkAnchorBox extends ViewBoxBaseComponent<FieldViewProps>() { onPointerDown={this.onPointerDown} onContextMenu={this.specificContextMenu} style={{ - border: selView && this.rootDoc[selView] === this.rootDoc[this.fieldKey] ? `solid ${globalCssVariables.MEDIUM_GRAY} 2px` : undefined, + 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)`, diff --git a/src/client/views/nodes/LinkBox.tsx b/src/client/views/nodes/LinkBox.tsx index 38ff21209..8b6293806 100644 --- a/src/client/views/nodes/LinkBox.tsx +++ b/src/client/views/nodes/LinkBox.tsx @@ -1,19 +1,18 @@ -import React = require('react'); import { Bezier } from 'bezier-js'; -import { computed, action } from 'mobx'; +import { computed, IReactionDisposer, makeObservable, observable, reaction } from 'mobx'; import { observer } from 'mobx-react'; -import { Height, Width } from '../../../fields/DocSymbols'; +import * as React from 'react'; import { Id } from '../../../fields/FieldSymbols'; import { DocCast, NumCast, StrCast } from '../../../fields/Types'; import { aggregateBounds, emptyFunction, returnAlways, returnFalse, Utils } from '../../../Utils'; import { DocumentManager } from '../../util/DocumentManager'; +import { Transform } from '../../util/Transform'; +import { CollectionFreeFormView } from '../collections/collectionFreeForm'; import { ViewBoxBaseComponent } from '../DocComponent'; import { StyleProp } from '../StyleProvider'; import { ComparisonBox } from './ComparisonBox'; import { FieldView, FieldViewProps } from './FieldView'; import './LinkBox.scss'; -import { CollectionFreeFormView } from '../collections/collectionFreeForm'; -import { Transform } from '../../util/Transform'; @observer export class LinkBox extends ViewBoxBaseComponent<FieldViewProps>() { @@ -21,26 +20,28 @@ export class LinkBox extends ViewBoxBaseComponent<FieldViewProps>() { return FieldView.LayoutString(LinkBox, fieldKey); } - onClickScriptDisable = returnAlways; - componentDidMount() { - this.props.setContentView?.(this); + constructor(props: FieldViewProps) { + super(props); + makeObservable(this); } + + onClickScriptDisable = returnAlways; @computed get anchor1() { - const anchor1 = DocCast(this.rootDoc.link_anchor_1); + const anchor1 = DocCast(this.dataDoc.link_anchor_1); const anchor_1 = anchor1?.layout_unrendered ? DocCast(anchor1.annotationOn) : anchor1; - return DocumentManager.Instance.getDocumentView(anchor_1, this.props.docViewPath()[this.props.docViewPath().length - 2]); // this.props.docViewPath().lastElement()); + return DocumentManager.Instance.getDocumentView(anchor_1, this.DocumentView?.().containerViewPath?.().lastElement()); } @computed get anchor2() { - const anchor2 = DocCast(this.rootDoc.link_anchor_2); + const anchor2 = DocCast(this.dataDoc.link_anchor_2); const anchor_2 = anchor2?.layout_unrendered ? DocCast(anchor2.annotationOn) : anchor2; - return DocumentManager.Instance.getDocumentView(anchor_2, this.props.docViewPath()[this.props.docViewPath().length - 2]); // this.props.docViewPath().lastElement()); + return DocumentManager.Instance.getDocumentView(anchor_2, this.DocumentView?.().containerViewPath?.().lastElement()); } screenBounds = () => { - if (this.layoutDoc._layout_isSvg && this.anchor1 && this.anchor2 && this.anchor1.props.CollectionFreeFormDocumentView?.().props.CollectionFreeFormView) { - const a_invXf = this.anchor1.props.ScreenToLocalTransform().inverse(); - const b_invXf = this.anchor2.props.ScreenToLocalTransform().inverse(); - const a_scrBds = { tl: a_invXf.transformPoint(0, 0), br: a_invXf.transformPoint(this.anchor1.rootDoc[Width](), this.anchor1.rootDoc[Height]()) }; - const b_scrBds = { tl: b_invXf.transformPoint(0, 0), br: b_invXf.transformPoint(this.anchor2.rootDoc[Width](), this.anchor2.rootDoc[Height]()) }; + if (this.layoutDoc._layout_isSvg && this.anchor1 && this.anchor2 && this.anchor1.CollectionFreeFormView) { + const a_invXf = this.anchor1.screenToViewTransform().inverse(); + const b_invXf = this.anchor2.screenToViewTransform().inverse(); + const a_scrBds = { tl: a_invXf.transformPoint(0, 0), br: a_invXf.transformPoint(NumCast(this.anchor1.Document._width), NumCast(this.anchor1.Document._height)) }; + const b_scrBds = { tl: b_invXf.transformPoint(0, 0), br: b_invXf.transformPoint(NumCast(this.anchor2.Document._width), NumCast(this.anchor2.Document._height)) }; const pts = [] as number[][]; pts.push([(a_scrBds.tl[0] + a_scrBds.br[0]) / 2, (a_scrBds.tl[1] + a_scrBds.br[1]) / 2]); @@ -54,53 +55,81 @@ export class LinkBox extends ViewBoxBaseComponent<FieldViewProps>() { ); return { left: agg.x, top: agg.y, right: agg.r, bottom: agg.b, center: undefined }; } - return { left: 0, top: 0, right: 0, bottom: 0, center: undefined }; + return undefined; }; - render() { - if (this.layoutDoc._layout_isSvg && (this.anchor1 || this.anchor2)?.props.CollectionFreeFormDocumentView?.().props.CollectionFreeFormView) { - const a = (this.anchor1 ?? this.anchor2)!; - const b = (this.anchor2 ?? this.anchor1)!; - - const parxf = this.props.docViewPath()[this.props.docViewPath().length - 2].ComponentView as CollectionFreeFormView; - const this_xf = parxf?.getTransform() ?? Transform.Identity; //this.props.ScreenToLocalTransform(); - const a_invXf = a.props.ScreenToLocalTransform().inverse(); - const b_invXf = b.props.ScreenToLocalTransform().inverse(); - const a_scrBds = { tl: a_invXf.transformPoint(0, 0), br: a_invXf.transformPoint(a.rootDoc[Width](), a.rootDoc[Height]()) }; - const b_scrBds = { tl: b_invXf.transformPoint(0, 0), br: b_invXf.transformPoint(b.rootDoc[Width](), b.rootDoc[Height]()) }; - const a_bds = { tl: this_xf.transformPoint(a_scrBds.tl[0], a_scrBds.tl[1]), br: this_xf.transformPoint(a_scrBds.br[0], a_scrBds.br[1]) }; - const b_bds = { tl: this_xf.transformPoint(b_scrBds.tl[0], b_scrBds.tl[1]), br: this_xf.transformPoint(b_scrBds.br[0], b_scrBds.br[1]) }; + disposer: IReactionDisposer | undefined; + componentDidMount() { + this._props.setContentViewBox?.(this); + this.disposer = reaction( + () => { + if (this.layoutDoc._layout_isSvg && (this.anchor1 || this.anchor2)?.CollectionFreeFormView) { + const a = (this.anchor1 ?? this.anchor2)!; + const b = (this.anchor2 ?? this.anchor1)!; - const ppt1 = [(a_bds.tl[0] + a_bds.br[0]) / 2, (a_bds.tl[1] + a_bds.br[1]) / 2]; - const pt1 = Utils.getNearestPointInPerimeter(a_bds.tl[0], a_bds.tl[1], a_bds.br[0] - a_bds.tl[0], a_bds.br[1] - a_bds.tl[1], (b_bds.tl[0] + b_bds.br[0]) / 2, (b_bds.tl[1] + b_bds.br[1]) / 2); - const pt2 = Utils.getNearestPointInPerimeter(b_bds.tl[0], b_bds.tl[1], b_bds.br[0] - b_bds.tl[0], b_bds.br[1] - b_bds.tl[1], (a_bds.tl[0] + a_bds.br[0]) / 2, (a_bds.tl[1] + a_bds.br[1]) / 2); - const ppt2 = [(b_bds.tl[0] + b_bds.br[0]) / 2, (b_bds.tl[1] + b_bds.br[1]) / 2]; + const parxf = this.DocumentView?.().containerViewPath?.().lastElement().ComponentView as CollectionFreeFormView; + const this_xf = parxf?.screenToFreeformContentsXf ?? Transform.Identity; //this.ScreenToLocalTransform(); + const a_invXf = a.screenToViewTransform().inverse(); + const b_invXf = b.screenToViewTransform().inverse(); + const a_scrBds = { tl: a_invXf.transformPoint(0, 0), br: a_invXf.transformPoint(NumCast(a.Document._width), NumCast(a.Document._height)) }; + const b_scrBds = { tl: b_invXf.transformPoint(0, 0), br: b_invXf.transformPoint(NumCast(b.Document._width), NumCast(b.Document._height)) }; + const a_bds = { tl: this_xf.transformPoint(a_scrBds.tl[0], a_scrBds.tl[1]), br: this_xf.transformPoint(a_scrBds.br[0], a_scrBds.br[1]) }; + const b_bds = { tl: this_xf.transformPoint(b_scrBds.tl[0], b_scrBds.tl[1]), br: this_xf.transformPoint(b_scrBds.br[0], b_scrBds.br[1]) }; - const pts = [ppt1, pt1, pt2, ppt2].map(pt => [pt[0], pt[1]]); - const [lx, rx, ty, by] = [Math.min(pt1[0], pt2[0]), Math.max(pt1[0], pt2[0]), Math.min(pt1[1], pt2[1]), Math.max(pt1[1], pt2[1])]; - setTimeout( - action(() => { - this.layoutDoc.x = lx; - this.layoutDoc.y = ty; - this.layoutDoc._width = rx - lx; - this.layoutDoc._height = by - ty; - }) - ); + const ppt1 = [(a_bds.tl[0] + a_bds.br[0]) / 2, (a_bds.tl[1] + a_bds.br[1]) / 2]; + const pt1 = Utils.getNearestPointInPerimeter(a_bds.tl[0], a_bds.tl[1], a_bds.br[0] - a_bds.tl[0], a_bds.br[1] - a_bds.tl[1], (b_bds.tl[0] + b_bds.br[0]) / 2, (b_bds.tl[1] + b_bds.br[1]) / 2); + const pt2 = Utils.getNearestPointInPerimeter(b_bds.tl[0], b_bds.tl[1], b_bds.br[0] - b_bds.tl[0], b_bds.br[1] - b_bds.tl[1], (a_bds.tl[0] + a_bds.br[0]) / 2, (a_bds.tl[1] + a_bds.br[1]) / 2); + const ppt2 = [(b_bds.tl[0] + b_bds.br[0]) / 2, (b_bds.tl[1] + b_bds.br[1]) / 2]; - const highlight = this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.Highlighting); + const pts = [ppt1, pt1, pt2, ppt2].map(pt => [pt[0], pt[1]]); + const [lx, rx, ty, by] = [Math.min(pt1[0], pt2[0]), Math.max(pt1[0], pt2[0]), Math.min(pt1[1], pt2[1]), Math.max(pt1[1], pt2[1])]; + return { pts, lx, rx, ty, by }; + } + return undefined; + }, + params => { + this.renderProps = params; + if (params) { + if ( + Math.abs(params.lx - NumCast(this.layoutDoc.x)) > 1e-5 || + Math.abs(params.ty - NumCast(this.layoutDoc.y)) > 1e-5 || + Math.abs(params.rx - params.lx - NumCast(this.layoutDoc._width)) > 1e-5 || + Math.abs(params.by - params.ty - NumCast(this.layoutDoc._height)) > 1e-5 + ) { + this.layoutDoc.x = params?.lx; + this.layoutDoc.y = params?.ty; + this.layoutDoc._width = params.rx - params?.lx; + this.layoutDoc._height = params?.by - params?.ty; + } + } else { + this.layoutDoc._width = Math.max(50, NumCast(this.layoutDoc._width)); + this.layoutDoc._height = Math.max(50, NumCast(this.layoutDoc._height)); + } + }, + { fireImmediately: true } + ); + } + componentWillUnmount(): void { + this.disposer?.(); + } + @observable renderProps: { lx: number; rx: number; ty: number; by: number; pts: number[][] } | undefined = undefined; + render() { + if (this.renderProps) { + const highlight = this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.Highlighting); const highlightColor = highlight?.highlightIndex ? highlight?.highlightColor : undefined; - const bez = new Bezier(pts.map(p => ({ x: p[0], y: p[1] }))); + const bez = new Bezier(this.renderProps.pts.map(p => ({ x: p[0], y: p[1] }))); const text = bez.get(0.5); - const linkDesc = StrCast(this.rootDoc.link_description) || 'description'; - const strokeWidth = NumCast(this.rootDoc.stroke_width, 4); - const dash = StrCast(this.rootDoc.stroke_dash); + const linkDesc = StrCast(this.dataDoc.link_description) || 'description'; + const strokeWidth = NumCast(this.dataDoc.stroke_width, 4); + const dash = StrCast(this.Document.stroke_dash); const strokeDasharray = dash && Number(dash) ? String(strokeWidth * Number(dash)) : undefined; + const { pts, lx, ty, rx, by } = this.renderProps; return ( <div style={{ transition: 'inherit', pointerEvents: 'none', position: 'absolute', width: '100%', height: '100%' }}> <svg width={Math.max(100, rx - lx)} height={Math.max(100, by - ty)} style={{ transition: 'inherit', overflow: 'visible' }}> <defs> - <filter x="0" y="0" width="1" height="1" id={`${this.rootDoc[Id] + 'background'}`}> - <feFlood floodColor={`${StrCast(this.rootDoc._backgroundColor, 'lightblue')}`} result="bg" /> + <filter x="0" y="0" width="1" height="1" id={`${this.Document[Id] + 'background'}`}> + <feFlood floodColor={`${StrCast(this.layoutDoc._backgroundColor, 'lightblue')}`} result="bg" /> <feMerge> <feMergeNode in="bg" /> <feMergeNode in="SourceGraphic" /> @@ -110,7 +139,7 @@ export class LinkBox extends ViewBoxBaseComponent<FieldViewProps>() { <path className="collectionfreeformlinkview-linkLine" style={{ - pointerEvents: this.props.pointerEvents?.() === 'none' ? 'none' : 'visibleStroke', // + pointerEvents: this._props.pointerEvents?.() === 'none' ? 'none' : 'visibleStroke', // stroke: highlightColor ?? 'lightblue', strokeDasharray, strokeWidth, @@ -120,8 +149,8 @@ export class LinkBox extends ViewBoxBaseComponent<FieldViewProps>() { ${pts[2][0] + pts[2][0] - pts[3][0] - lx} ${pts[2][1] + pts[2][1] - pts[3][1] - ty}, ${pts[2][0] - lx} ${pts[2][1] - ty}`} /> <text - filter={`url(#${this.rootDoc[Id] + 'background'})`} - style={{ pointerEvents: this.props.pointerEvents?.() === 'none' ? 'none' : 'all', textAnchor: 'middle', fontSize: '12', stroke: 'black' }} + filter={`url(#${this.Document[Id] + 'background'})`} + style={{ pointerEvents: this._props.pointerEvents?.() === 'none' ? 'none' : 'all', textAnchor: 'middle', fontSize: '12', stroke: 'black' }} x={text.x - lx} y={text.y - ty}> <tspan> </tspan> @@ -133,14 +162,13 @@ export class LinkBox extends ViewBoxBaseComponent<FieldViewProps>() { ); } return ( - <div className={`linkBox-container${this.props.isContentActive() ? '-interactive' : ''}`} style={{ background: this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.BackgroundColor) }}> + <div className={`linkBox-container${this._props.isContentActive() ? '-interactive' : ''}`} style={{ background: this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.BackgroundColor) }}> <ComparisonBox - {...this.props} + {...this._props} // fieldKey="link_anchor" setHeight={emptyFunction} dontRegisterView={true} - renderDepth={this.props.renderDepth + 1} - isContentActive={this.props.isContentActive} + renderDepth={this._props.renderDepth + 1} addDocument={returnFalse} removeDocument={returnFalse} moveDocument={returnFalse} diff --git a/src/client/views/nodes/LinkDescriptionPopup.scss b/src/client/views/nodes/LinkDescriptionPopup.scss index a8db5d360..104301656 100644 --- a/src/client/views/nodes/LinkDescriptionPopup.scss +++ b/src/client/views/nodes/LinkDescriptionPopup.scss @@ -1,7 +1,6 @@ -@import "../global/globalCssVariables.scss"; +@import '../global/globalCssVariables.module.scss'; .linkDescriptionPopup { - display: flex; flex-direction: row; justify-content: center; @@ -26,7 +25,6 @@ } .linkDescriptionPopup-btn { - float: right; justify-content: center; vertical-align: middle; @@ -53,6 +51,4 @@ color: black; } } - - -}
\ No newline at end of file +} diff --git a/src/client/views/nodes/LinkDescriptionPopup.tsx b/src/client/views/nodes/LinkDescriptionPopup.tsx index c45045a8a..13f0ac4fc 100644 --- a/src/client/views/nodes/LinkDescriptionPopup.tsx +++ b/src/client/views/nodes/LinkDescriptionPopup.tsx @@ -1,20 +1,27 @@ -import React = require('react'); -import { action, observable } from 'mobx'; +import { action, makeObservable, observable } from 'mobx'; import { observer } from 'mobx-react'; -import { Doc } from '../../../fields/Doc'; +import * as React from 'react'; +import { DocData } from '../../../fields/DocSymbols'; import { LinkManager } from '../../util/LinkManager'; import './LinkDescriptionPopup.scss'; import { TaskCompletionBox } from './TaskCompletedBox'; @observer export class LinkDescriptionPopup extends React.Component<{}> { - @observable public static descriptionPopup: boolean = false; - @observable public static showDescriptions: string = 'ON'; - @observable public static popupX: number = 700; - @observable public static popupY: number = 350; + public static Instance: LinkDescriptionPopup; + @observable public display: boolean = false; + @observable public showDescriptions: string = 'ON'; + @observable public popupX: number = 700; + @observable public popupY: number = 350; @observable description: string = ''; @observable popupRef = React.createRef<HTMLDivElement>(); + constructor(props: any) { + super(props); + makeObservable(this); + LinkDescriptionPopup.Instance = this; + } + @action descriptionChanged = (e: React.ChangeEvent<HTMLInputElement>) => { this.description = e.currentTarget.value; @@ -22,16 +29,16 @@ export class LinkDescriptionPopup extends React.Component<{}> { @action onDismiss = (add: boolean) => { - LinkDescriptionPopup.descriptionPopup = false; + this.display = false; if (add) { - LinkManager.currentLink && (Doc.GetProto(LinkManager.currentLink).link_description = this.description); + LinkManager.currentLink && (LinkManager.currentLink[DocData].link_description = this.description); } }; @action onClick = (e: PointerEvent) => { if (this.popupRef && !!!this.popupRef.current?.contains(e.target as any)) { - LinkDescriptionPopup.descriptionPopup = false; + this.display = false; TaskCompletionBox.taskCompleted = false; } }; @@ -46,13 +53,13 @@ export class LinkDescriptionPopup extends React.Component<{}> { } render() { - return ( + return !this.display ? null : ( <div className="linkDescriptionPopup" ref={this.popupRef} style={{ - left: LinkDescriptionPopup.popupX ? LinkDescriptionPopup.popupX : 700, - top: LinkDescriptionPopup.popupY ? LinkDescriptionPopup.popupY : 350, + left: this.popupX ? this.popupX : 700, + top: this.popupY ? this.popupY : 350, }}> <input className="linkDescriptionPopup-input" diff --git a/src/client/views/nodes/LinkDocPreview.tsx b/src/client/views/nodes/LinkDocPreview.tsx index 198cbe851..4b242649a 100644 --- a/src/client/views/nodes/LinkDocPreview.tsx +++ b/src/client/views/nodes/LinkDocPreview.tsx @@ -1,101 +1,110 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { Tooltip } from '@material-ui/core'; -import { action, computed, observable } from 'mobx'; +import { Tooltip } from '@mui/material'; +import { action, computed, makeObservable, observable, runInAction } from 'mobx'; import { observer } from 'mobx-react'; +import * as React from 'react'; import wiki from 'wikijs'; -import { Doc, DocCastAsync, Opt } from '../../../fields/Doc'; -import { Height, Width } from '../../../fields/DocSymbols'; -import { Cast, DocCast, NumCast, PromiseValue, StrCast } from '../../../fields/Types'; import { emptyFunction, returnEmptyDoclist, returnEmptyFilter, returnEmptyString, returnFalse, returnNone, setupMoveUpEvents } from '../../../Utils'; +import { Doc, Opt } from '../../../fields/Doc'; +import { Cast, DocCast, NumCast, PromiseValue, StrCast } from '../../../fields/Types'; import { DocServer } from '../../DocServer'; -import { Docs } from '../../documents/Documents'; 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 { Transform } from '../../util/Transform'; -import { DocumentView, DocumentViewSharedProps, OpenWhere } from './DocumentView'; +import { ObservableReactComponent } from '../ObservableReactComponent'; +import { DocumentView, OpenWhere } from './DocumentView'; +import { StyleProviderFuncType } from './FieldView'; import './LinkDocPreview.scss'; -import React = require('react'); -import { DocumentManager } from '../../util/DocumentManager'; + +export class LinkInfo { + private static _instance: Opt<LinkInfo>; + constructor() { + LinkInfo._instance = this; + makeObservable(this); + } + @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)); + } + public static SetLinkInfo(info?: LinkDocPreviewProps) { + runInAction(() => LinkInfo.Instance && (LinkInfo.Instance.LinkInfo = info)); + } +} interface LinkDocPreviewProps { linkDoc?: Doc; linkSrc?: Doc; - docProps: DocumentViewSharedProps; + DocumentView?: () => DocumentView; + styleProvider?: StyleProviderFuncType; location: number[]; hrefs?: string[]; showHeader?: boolean; noPreview?: boolean; } @observer -export class LinkDocPreview extends React.Component<LinkDocPreviewProps> { - @action public static Clear() { - LinkDocPreview.LinkInfo = undefined; - } - @action public static SetLinkInfo(info?: LinkDocPreviewProps) { - LinkDocPreview.LinkInfo !== info && (LinkDocPreview.LinkInfo = info); - } - - static _instance: Opt<LinkDocPreview>; - +export class LinkDocPreview extends ObservableReactComponent<LinkDocPreviewProps> { _infoRef = React.createRef<HTMLDivElement>(); _linkDocRef = React.createRef<HTMLDivElement>(); - @observable public static LinkInfo: Opt<LinkDocPreviewProps>; - @observable _targetDoc: Opt<Doc>; - @observable _markerTargetDoc: Opt<Doc>; - @observable _linkDoc: Opt<Doc>; - @observable _linkSrc: Opt<Doc>; + @observable _targetDoc: Opt<Doc> = undefined; + @observable _markerTargetDoc: Opt<Doc> = undefined; + @observable _linkDoc: Opt<Doc> = undefined; + @observable _linkSrc: Opt<Doc> = undefined; @observable _toolTipText = ''; @observable _hrefInd = 0; - constructor(props: any) { super(props); - LinkDocPreview._instance = this; + makeObservable(this); } - @action init() { - var linkTarget = this.props.linkDoc; - this._linkSrc = this.props.linkSrc; - this._linkDoc = this.props.linkDoc; - const link_anchor_1 = this._linkDoc?.link_anchor_1 as Doc; - const link_anchor_2 = this._linkDoc?.link_anchor_2 as Doc; + @action + init() { + var 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; } if (linkTarget?.annotationOn && linkTarget?.type !== DocumentType.RTF) { - // want to show annotation embedContainer document if annotation is not text - linkTarget && DocCastAsync(linkTarget.annotationOn).then(action(anno => (this._markerTargetDoc = this._targetDoc = anno))); - } else { - this._markerTargetDoc = this._targetDoc = linkTarget; + linkTarget = DocCast(linkTarget.annotationOn); // want to show annotation embedContainer document if annotation is not text } + this._markerTargetDoc = this._targetDoc = linkTarget; this._toolTipText = ''; this.updateHref(); } - componentDidUpdate(props: any) { - if (props.linkSrc !== this.props.linkSrc || props.linkDoc !== this.props.linkDoc || props.hrefs !== this.props.hrefs) this.init(); + componentDidUpdate(prevProps: Readonly<LinkDocPreviewProps>) { + super.componentDidUpdate(prevProps); + if (prevProps.linkSrc !== this._props.linkSrc || prevProps.linkDoc !== this._props.linkDoc || prevProps.hrefs !== this._props.hrefs) this.init(); } componentDidMount() { this.init(); document.addEventListener('pointerdown', this.onPointerDown, true); } - @action componentWillUnmount() { - LinkDocPreview.SetLinkInfo(undefined); + LinkInfo.Clear(); document.removeEventListener('pointerdown', this.onPointerDown, true); } onPointerDown = (e: PointerEvent) => { - !this._linkDocRef.current?.contains(e.target as any) && LinkDocPreview.Clear(); // close preview when not clicking anywhere other than the info bar of the preview + !this._linkDocRef.current?.contains(e.target as any) && LinkInfo.Clear(); // close preview when not clicking anywhere other than the info bar of the preview }; @action updateHref() { - if (this.props.hrefs?.length) { - const href = this.props.hrefs[this._hrefInd]; + if (this._props.hrefs?.length) { + const href = this._props.hrefs[this._hrefInd]; if (href.indexOf(Doc.localServerPath()) !== 0) { // link to a web page URL -- try to show a preview if (href.startsWith('https://en.wikipedia.org/wiki/')) { @@ -125,7 +134,7 @@ export class LinkDocPreview extends React.Component<LinkDocPreviewProps> { this._markerTargetDoc = linkTarget; this._targetDoc = /*linkTarget?.type === DocumentType.MARKER &&*/ linkTarget?.annotationOn ? Cast(linkTarget.annotationOn, Doc, null) ?? linkTarget : linkTarget; } - if (LinkDocPreview.LinkInfo?.noPreview || this._linkSrc?.followLinkToggle || this._markerTargetDoc?.type === DocumentType.PRES) this.followLink(); + if (LinkInfo.Instance?.LinkInfo?.noPreview || this._linkSrc?.followLinkToggle || this._markerTargetDoc?.type === DocumentType.PRES) this.followLink(); } }) ); @@ -145,9 +154,9 @@ export class LinkDocPreview extends React.Component<LinkDocPreviewProps> { action(() => { LinkManager.currentLink = this._linkDoc; LinkManager.currentLinkAnchor = this._linkSrc; - this.props.docProps.DocumentView?.().select(false); - if ((SettingsManager.propertiesWidth ?? 0) < 100) { - SettingsManager.propertiesWidth = 250; + this._props.DocumentView?.().select(false); + if ((SettingsManager.Instance.propertiesWidth ?? 0) < 100) { + SettingsManager.Instance.propertiesWidth = 250; } }) ); @@ -159,7 +168,7 @@ export class LinkDocPreview extends React.Component<LinkDocPreviewProps> { returnFalse, emptyFunction, action(() => { - const nextHrefInd = (this._hrefInd + 1) % (this.props.hrefs?.length || 1); + const nextHrefInd = (this._hrefInd + 1) % (this._props.hrefs?.length || 1); if (nextHrefInd !== this._hrefInd) { this._linkDoc = undefined; this._hrefInd = nextHrefInd; @@ -170,19 +179,19 @@ export class LinkDocPreview extends React.Component<LinkDocPreviewProps> { }; followLink = () => { - LinkDocPreview.Clear(); + LinkInfo.Clear(); if (this._linkDoc && this._linkSrc) { LinkFollower.FollowLink(this._linkDoc, this._linkSrc, false); - } else if (this.props.hrefs?.length) { + } else if (this._props.hrefs?.length) { const webDoc = - Array.from(SearchUtil.SearchCollection(Doc.MyFilesystem, this.props.hrefs[0]).keys()).lastElement() ?? - Docs.Create.WebDocument(this.props.hrefs[0], { title: this.props.hrefs[0], _nativeWidth: 850, _width: 200, _height: 400, data_useCors: true }); + Array.from(SearchUtil.SearchCollection(Doc.MyFilesystem, this._props.hrefs[0]).keys()).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, { openLocation: OpenWhere.lightbox, willPan: true, zoomTime: 500, }); - //this.props.docProps?.addDocTab(webDoc, OpenWhere.lightbox); + //this._props.docProps?.addDocTab(webDoc, OpenWhere.lightbox); } }; @@ -190,17 +199,17 @@ export class LinkDocPreview extends React.Component<LinkDocPreviewProps> { width = () => { if (!this._targetDoc) return 225; - if (this._targetDoc[Width]() < this._targetDoc?.[Height]()) { - return (Math.min(225, this._targetDoc[Height]()) * this._targetDoc[Width]()) / this._targetDoc[Height](); + if (NumCast(this._targetDoc._width) < NumCast(this._targetDoc._height)) { + return (Math.min(225, NumCast(this._targetDoc._height)) * NumCast(this._targetDoc._width)) / NumCast(this._targetDoc._height); } - return Math.min(225, NumCast(this._targetDoc?.[Width](), 225)); + return Math.min(225, NumCast(this._targetDoc._width, 225)); }; height = () => { if (!this._targetDoc) return 225; - if (this._targetDoc[Width]() > this._targetDoc?.[Height]()) { - return (Math.min(225, this._targetDoc[Width]()) * this._targetDoc[Height]()) / this._targetDoc[Width](); + if (NumCast(this._targetDoc._width) > NumCast(this._targetDoc._height)) { + return (Math.min(225, NumCast(this._targetDoc._width)) * NumCast(this._targetDoc._height)) / NumCast(this._targetDoc._width); } - return Math.min(225, NumCast(this._targetDoc?.[Height](), 225)); + return Math.min(225, NumCast(this._targetDoc._height, 225)); }; @computed get previewHeader() { return !this._linkDoc || !this._markerTargetDoc || !this._targetDoc || !this._linkSrc ? null : ( @@ -218,7 +227,7 @@ export class LinkDocPreview extends React.Component<LinkDocPreviewProps> { </div> <div className="linkDocPreview-buttonBar" style={{ float: 'right' }}> <Tooltip title={<div className="dash-tooltip">Next Link</div>} placement="top"> - <div className="linkDocPreview-button" style={{ background: (this.props.hrefs?.length || 0) <= 1 ? 'gray' : 'green' }} onPointerDown={this.nextHref}> + <div className="linkDocPreview-button" style={{ background: (this._props.hrefs?.length || 0) <= 1 ? 'gray' : 'green' }} onPointerDown={this.nextHref}> <FontAwesomeIcon className="linkDocPreview-fa-icon" icon="chevron-right" color="white" size="sm" /> </div> </Tooltip> @@ -230,7 +239,7 @@ export class LinkDocPreview extends React.Component<LinkDocPreviewProps> { @computed get docPreview() { return (!this._linkDoc || !this._targetDoc || !this._linkSrc) && !this._toolTipText ? null : ( <div className="linkDocPreview-inner"> - {!this.props.showHeader ? null : this.previewHeader} + {!this._props.showHeader ? null : this.previewHeader} <div className="linkDocPreview-preview-wrapper" onPointerDown={e => @@ -240,7 +249,7 @@ export class LinkDocPreview extends React.Component<LinkDocPreviewProps> { (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); - LinkDocPreview.Clear(); + LinkInfo.Clear(); return true; } return false; @@ -258,13 +267,12 @@ export class LinkDocPreview extends React.Component<LinkDocPreviewProps> { <DocumentView ref={r => { const targetanchor = this._linkDoc && this._linkSrc && LinkManager.getOppositeAnchor(this._linkDoc, this._linkSrc); - targetanchor && this._targetDoc !== targetanchor && r?.props.focus?.(targetanchor, {}); + targetanchor && this._targetDoc !== targetanchor && r?._props.focus?.(targetanchor, {}); }} Document={this._targetDoc!} moveDocument={returnFalse} - rootSelected={returnFalse} - styleProvider={this.props.docProps?.styleProvider} - docViewPath={returnEmptyDoclist} + styleProvider={this._props.styleProvider} + containerViewPath={returnEmptyDoclist} ScreenToLocalTransform={Transform.Identity} isDocumentActive={returnFalse} isContentActive={returnFalse} @@ -285,7 +293,6 @@ export class LinkDocPreview extends React.Component<LinkDocPreviewProps> { focus={emptyFunction} whenChildContentsActiveChanged={returnFalse} ignoreAutoHeight={true} // need to ignore layout_autoHeight otherwise layout_autoHeight text boxes will expand beyond the preview panel size. - bringToFront={returnFalse} NativeWidth={Doc.NativeWidth(this._targetDoc) ? () => Doc.NativeWidth(this._targetDoc) : undefined} NativeHeight={Doc.NativeHeight(this._targetDoc) ? () => Doc.NativeHeight(this._targetDoc) : undefined} /> @@ -302,7 +309,7 @@ export class LinkDocPreview extends React.Component<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: 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) }}> {this.docPreview} </div> ); diff --git a/src/client/views/nodes/LoadingBox.tsx b/src/client/views/nodes/LoadingBox.tsx index bdc074e0c..adccc9db6 100644 --- a/src/client/views/nodes/LoadingBox.tsx +++ b/src/client/views/nodes/LoadingBox.tsx @@ -6,6 +6,7 @@ import { Doc } from '../../../fields/Doc'; import { Id } from '../../../fields/FieldSymbols'; import { StrCast } from '../../../fields/Types'; import { Networking } from '../../Network'; +import { DocumentManager } from '../../util/DocumentManager'; import { ViewBoxAnnotatableComponent } from '../DocComponent'; import { FieldView, FieldViewProps } from './FieldView'; import './LoadingBox.scss'; @@ -36,32 +37,47 @@ 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 (!Doc.CurrentlyLoading?.includes(this.rootDoc)) { - this.rootDoc.loadingError = 'Upload interrupted, please try again'; + if (!DocumentManager.Instance.CurrentlyLoading?.includes(this.Document)) { + this.Document.loadingError = 'Upload interrupted, please try again'; } else { const updateFunc = async () => { - const result = await Networking.QueryYoutubeProgress(StrCast(this.rootDoc[Id])); // We use the guid of the overwriteDoc to track file uploads. + 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)); - !this.rootDoc.loadingError && (this._timer = setTimeout(updateFunc, 1000)); + !this.Document.loadingError && this._timer && (this._timer = setTimeout(updateFunc, 1000)); }; this._timer = setTimeout(updateFunc, 1000); } } componentWillUnmount() { clearTimeout(this._timer); + this._timer = undefined; } render() { return ( - <div className="loadingBoxContainer" style={{ background: !this.rootDoc.loadingError ? '' : 'red' }}> + <div className="loadingBoxContainer" style={{ background: !this.Document.loadingError ? '' : 'red' }}> <div className="loadingBox-textContainer"> - <span className="loadingBox-title">{StrCast(this.rootDoc.title)}</span> - <p className="loadingBox-headerText">{StrCast(this.rootDoc.loadingError, 'Loading ' + (this.progress.replace('[download]', '') || '(can take several minutes)'))}</p> - {this.rootDoc.loadingError ? null : ( + <span className="loadingBox-title">{StrCast(this.Document.title)}</span> + <p className="loadingBox-headerText">{StrCast(this.Document.loadingError, 'Loading ' + (this.progress.replace('[download]', '') || '(can take several minutes)'))}</p> + {this.Document.loadingError ? null : ( <div className="loadingBox-spinner"> <ReactLoading type="spinningBubbles" color="blue" height={100} width={100} /> </div> diff --git a/src/client/views/nodes/MapBox/AnimationSpeedIcons.tsx b/src/client/views/nodes/MapBox/AnimationSpeedIcons.tsx new file mode 100644 index 000000000..d54a175b2 --- /dev/null +++ b/src/client/views/nodes/MapBox/AnimationSpeedIcons.tsx @@ -0,0 +1,35 @@ +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"> + {` + .fil0 { fill: black; fill-rule: nonzero; } + .fil1 { fill: #FE0000; fill-rule: nonzero; } + `} + </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"/> + </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"/> + </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"/> + </svg> +); + diff --git a/src/client/views/nodes/MapBox/AnimationUtility.ts b/src/client/views/nodes/MapBox/AnimationUtility.ts new file mode 100644 index 000000000..35153f439 --- /dev/null +++ b/src/client/views/nodes/MapBox/AnimationUtility.ts @@ -0,0 +1,447 @@ +import * as turf from '@turf/turf'; +import { Position } from '@turf/turf'; +import * as d3 from 'd3'; +import { Feature, GeoJsonProperties, Geometry } from 'geojson'; +import mapboxgl, { MercatorCoordinate } from 'mapbox-gl'; +import { action, computed, makeObservable, observable, runInAction } from 'mobx'; +import { MapRef } from 'react-map-gl'; + +export enum AnimationStatus { + START = 'start', + RESUME = 'resume', + RESTART = 'restart', +} + +export enum AnimationSpeed { + SLOW = '1x', + MEDIUM = '2x', + FAST = '3x', +} + +export class AnimationUtility { + private SMOOTH_FACTOR = 0.95; + private ROUTE_COORDINATES: Position[] = []; + + @observable + private PATH?: turf.helpers.Feature<turf.helpers.LineString, turf.helpers.Properties> = undefined; + + private PATH_DISTANCE: number = 0; + private FLY_IN_START_PITCH = 40; + private FIRST_LNG_LAT: { lng: number; lat: number } = { lng: 0, lat: 0 }; + private START_ALTITUDE = 3_000_000; + private MAP_REF: MapRef | null = null; + + @observable private isStreetViewAnimation: boolean = false; + @observable private animationSpeed: AnimationSpeed = AnimationSpeed.MEDIUM; + + @observable + private previousLngLat: { lng: number; lat: number }; + + private previousAltitude: number | null = null; + private previousPitch: number | null = null; + + private currentStreetViewBearing: number = 0; + + private terrainDisplayed: boolean; + + @computed get flyInEndBearing() { + return this.isStreetViewAnimation + ? this.calculateBearing( + { + lng: this.ROUTE_COORDINATES[0][0], + lat: this.ROUTE_COORDINATES[0][1], + }, + { + lng: this.ROUTE_COORDINATES[1][0], + lat: this.ROUTE_COORDINATES[1][1], + } + ) + : -20; + } + + @computed get currentAnimationAltitude(): number { + if (!this.isStreetViewAnimation) return 20_000; + if (!this.terrainDisplayed) return 50; + const coords: mapboxgl.LngLatLike = [this.previousLngLat.lng, this.previousLngLat.lat]; + // console.log('MAP REF: ', this.MAP_REF) + // console.log("current elevation: ", this.MAP_REF?.queryTerrainElevation(coords)); + let altitude = this.MAP_REF ? this.MAP_REF.queryTerrainElevation(coords) ?? 0 : 0; + if (altitude === 0) { + altitude += 50; + } + if (this.previousAltitude) { + return this.lerp(altitude, this.previousAltitude, 0.02); + } + return altitude; + } + + @computed get flyInStartBearing() { + return Math.max(0, Math.min(this.flyInEndBearing + 20, 360)); // between 0 and 360 + } + + @computed get flyInEndAltitude() { + // return this.isStreetViewAnimation ? (this.currentAnimationAltitude + 70 ): 10_000; + return this.currentAnimationAltitude; + } + + @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))); + + 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() { + return this.currentPitch; + } + + @computed get flyToDuration() { + switch (this.animationSpeed) { + case AnimationSpeed.SLOW: + return 4_000; + case AnimationSpeed.MEDIUM: + return 2_500; + case AnimationSpeed.FAST: + return 1_250; + default: + return 2_500; + } + } + + @computed get animationDuration(): number { + let scalingFactor: number; + const MIN_DISTANCE = 0; + const MAX_DISTANCE = 3_000; // anything greater than 3000 is just set to 1 when normalized + const MAX_DURATION = this.isStreetViewAnimation ? 120_000 : 60_000; + + const normalizedDistance = Math.min(1, (this.PATH_DISTANCE - MIN_DISTANCE) / (MAX_DISTANCE - MIN_DISTANCE)); + const easedDistance = d3.easeExpOut(Math.min(normalizedDistance, 1)); + + switch (this.animationSpeed) { + case AnimationSpeed.SLOW: + scalingFactor = 250; + break; + case AnimationSpeed.MEDIUM: + scalingFactor = 150; + break; + case AnimationSpeed.FAST: + scalingFactor = 85; + break; + default: + scalingFactor = 150; + break; + } + + const duration = Math.min(MAX_DURATION, easedDistance * MAX_DISTANCE * (this.isStreetViewAnimation ? scalingFactor * 1.12 : scalingFactor)); + + return duration; + } + + @action + public updateAnimationSpeed(speed: AnimationSpeed) { + // calculate new flyToDuration and animationDuration + this.animationSpeed = speed; + } + + @action + public updateIsStreetViewAnimation(isStreetViewAnimation: boolean) { + this.isStreetViewAnimation = isStreetViewAnimation; + } + + @action + public setPath = (path: turf.helpers.Feature<turf.helpers.LineString, turf.helpers.Properties>) => { + this.PATH = path; + }; + + constructor(firstLngLat: { lng: number; lat: number }, routeCoordinates: Position[], isStreetViewAnimation: boolean, animationSpeed: AnimationSpeed, terrainDisplayed: boolean, mapRef: MapRef | null) { + makeObservable(this); + this.FIRST_LNG_LAT = firstLngLat; + this.previousLngLat = firstLngLat; + this.isStreetViewAnimation = isStreetViewAnimation; + this.MAP_REF = mapRef; + + this.ROUTE_COORDINATES = routeCoordinates; + this.PATH = turf.lineString(routeCoordinates); + this.PATH_DISTANCE = turf.lineDistance(this.PATH); + this.terrainDisplayed = terrainDisplayed; + + const bearing = this.calculateBearing( + { + lng: routeCoordinates[0][0], + lat: routeCoordinates[0][1], + }, + { + lng: routeCoordinates[1][0], + lat: routeCoordinates[1][1], + } + ); + this.currentStreetViewBearing = bearing; + this.animationSpeed = animationSpeed; + } + + public animatePath = async ({ + map, + // path, + // startBearing, + // startAltitude, + // pitch, + currentAnimationPhase, + updateAnimationPhase, + updateFrameId, + }: { + map: MapRef; + // path: turf.helpers.Feature<turf.helpers.LineString, turf.helpers.Properties>, + // startBearing: number, + // startAltitude: number, + // pitch: number, + currentAnimationPhase: number; + updateAnimationPhase: (newAnimationPhase: number) => void; + updateFrameId: (newFrameId: number) => void; + }) => { + return new Promise<void>(async resolve => { + let startTime: number | null = null; + + const frame = async (currentTime: number) => { + if (!startTime) startTime = currentTime; + const elapsedSinceLastFrame = currentTime - startTime; + const phaseIncrement = elapsedSinceLastFrame / this.animationDuration; + const animationPhase = currentAnimationPhase + phaseIncrement; + + // when the duration is complete, resolve the promise and stop iterating + if (animationPhase > 1) { + resolve(); + return; + } + + if (!this.PATH) return; + // calculate the distance along the path based on the animationPhase + const alongPath = turf.along(this.PATH, this.PATH_DISTANCE * animationPhase).geometry.coordinates; + + const lngLat = { + lng: alongPath[0], + lat: alongPath[1], + }; + + let bearing: number; + if (this.isStreetViewAnimation) { + bearing = this.lerp(this.currentStreetViewBearing, this.calculateBearing(this.previousLngLat, lngLat), 0.032); + this.currentStreetViewBearing = bearing; + // bearing = this.calculateBearing(this.previousLngLat, lngLat); // TODO: Calculate actual bearing + } else { + // slowly rotate the map at a constant rate + bearing = this.flyInEndBearing - animationPhase * 200.0; + // bearing = startBearing - animationPhase * 200.0; + } + + runInAction(() => { + this.previousLngLat = lngLat; + }); + + updateAnimationPhase(animationPhase); + + // compute corrected camera ground position, so that he leading edge of the path is in view + var correctedPosition = this.computeCameraPosition( + this.isStreetViewAnimation, + this.currentPitch, + bearing, + lngLat, + this.currentAnimationAltitude, + true // smooth + ); + + // set the pitch and bearing of the camera + const camera = map.getFreeCameraOptions(); + camera.setPitchBearing(this.currentPitch, bearing); + + // set the position and altitude of the camera + camera.position = MercatorCoordinate.fromLngLat(correctedPosition, this.currentAnimationAltitude); + + // apply the new camera options + map.setFreeCameraOptions(camera); + + this.previousAltitude = this.currentAnimationAltitude; + this.previousPitch = this.previousPitch; + + // repeat! + const innerFrameId = await window.requestAnimationFrame(frame); + updateFrameId(innerFrameId); + }; + + 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 => { + let start: number | null; + + var currentAltitude; + var currentBearing; + var currentPitch; + + // the animation frame will run as many times as necessary until the duration has been reached + const frame = async (time: number) => { + if (!start) { + start = time; + } + + // otherwise, use the current time to determine how far along in the duration we are + let animationPhase = (time - start) / this.flyToDuration; + + // because the phase calculation is imprecise, the final zoom can vary + // if it ended up greater than 1, set it to 1 so that we get the exact endAltitude that was requested + if (animationPhase > 1) { + animationPhase = 1; + } + + currentAltitude = this.START_ALTITUDE + (this.flyInEndAltitude - this.START_ALTITUDE) * d3.easeCubicOut(animationPhase); + // rotate the camera between startBearing and endBearing + currentBearing = this.flyInStartBearing + (this.flyInEndBearing - this.flyInStartBearing) * d3.easeCubicOut(animationPhase); + + 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); + + // set the pitch and bearing of the camera + const camera = map.getFreeCameraOptions(); + camera.setPitchBearing(currentPitch, currentBearing); + + // set the position and altitude of the camera + camera.position = MercatorCoordinate.fromLngLat(correctedPosition, currentAltitude); + + // apply the new camera options + map.setFreeCameraOptions(camera); + + // when the animationPhase is done, resolve the promise so the parent function can move on to the next step in the sequence + if (animationPhase === 1) { + resolve({ + bearing: currentBearing, + altitude: currentAltitude, + }); + + // return so there are no further iterations of this frame + return; + } + + const innerFrameId = await window.requestAnimationFrame(frame); + updateFrameId(innerFrameId); + }; + + 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; + }; + + computeCameraPosition = (isStreetViewAnimation: boolean, pitch: number, bearing: number, targetPosition: { lng: number; lat: number }, altitude: number, smooth = false) => { + const bearingInRadian = (bearing * Math.PI) / 180; + const pitchInRadian = ((90 - pitch) * Math.PI) / 180; + + let correctedLng = targetPosition.lng; + let correctedLat = targetPosition.lat; + + if (!isStreetViewAnimation) { + const lngDiff = ((altitude / Math.tan(pitchInRadian)) * Math.sin(-bearingInRadian)) / 70000; // ~70km/degree longitude + const latDiff = ((altitude / Math.tan(pitchInRadian)) * Math.cos(-bearingInRadian)) / 110000; // 110km/degree latitude + + correctedLng = targetPosition.lng + lngDiff; + correctedLat = targetPosition.lat - latDiff; + } + + const newCameraPosition = { + lng: correctedLng, + lat: correctedLat, + }; + + if (smooth) { + if (this.previousCameraPosition) { + newCameraPosition.lng = this.lerp(newCameraPosition.lng, this.previousCameraPosition.lng, this.SMOOTH_FACTOR); + newCameraPosition.lat = this.lerp(newCameraPosition.lat, this.previousCameraPosition.lat, this.SMOOTH_FACTOR); + } + } + + this.previousCameraPosition = newCameraPosition; + + return newCameraPosition; + }; + + public static createGeoJSONCircle = (center: number[], radiusInKm: number, points = 64): Feature<Geometry, GeoJsonProperties> => { + const coords = { + latitude: center[1], + longitude: center[0], + }; + const km = radiusInKm; + const ret = []; + const distanceX = km / (111.32 * Math.cos((coords.latitude * Math.PI) / 180)); + const distanceY = km / 110.574; + let theta; + let x; + let y; + for (let i = 0; i < points; i += 1) { + theta = (i / points) * (2 * Math.PI); + x = distanceX * Math.cos(theta); + y = distanceY * Math.sin(theta); + ret.push([coords.longitude + x, coords.latitude + y]); + } + ret.push(ret[0]); + return { + type: 'Feature', + geometry: { + type: 'Polygon', + coordinates: [ret], + }, + properties: {}, + }; + }; + + private calculateBearing(from: { lng: number; lat: number }, to: { lng: number; lat: number }): number { + const lon1 = from.lng; + const lat1 = from.lat; + const lon2 = to.lng; + const lat2 = to.lat; + + const lon1Rad = (lon1 * Math.PI) / 180; + const lon2Rad = (lon2 * Math.PI) / 180; + const lat1Rad = (lat1 * Math.PI) / 180; + const lat2Rad = (lat2 * Math.PI) / 180; + + const y = Math.sin(lon2Rad - lon1Rad) * Math.cos(lat2Rad); + const x = Math.cos(lat1Rad) * Math.sin(lat2Rad) - Math.sin(lat1Rad) * Math.cos(lat2Rad) * Math.cos(lon2Rad - lon1Rad); + + let bearing = Math.atan2(y, x); + + // Convert bearing from radians to degrees + bearing = (bearing * 180) / Math.PI; + + // Ensure the bearing is within [0, 360) + if (bearing < 0) { + bearing += 360; + } + + return bearing; + } +} diff --git a/src/client/views/nodes/MapBox/DirectionsAnchorMenu.tsx b/src/client/views/nodes/MapBox/DirectionsAnchorMenu.tsx new file mode 100644 index 000000000..7e99795b5 --- /dev/null +++ b/src/client/views/nodes/MapBox/DirectionsAnchorMenu.tsx @@ -0,0 +1,122 @@ +import { IconLookup, faAdd, faCalendarDays, faRoute } from '@fortawesome/free-solid-svg-icons'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +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 { 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'; + +@observer +export class DirectionsAnchorMenu extends AntimodeMenu<AntimodeMenuProps> { + static Instance: DirectionsAnchorMenu; + + private _disposer: IReactionDisposer | undefined; + + public onMakeAnchor: () => Opt<Doc> = () => undefined; // Method to get anchor from text search + + public Center: () => void = unimplementedFunction; + 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 Delete: () => void = unimplementedFunction; + // public MakeTargetToggle: () => void = unimplementedFunction; + // public ShowTargetTrail: () => void = unimplementedFunction; + public IsTargetToggler: () => boolean = returnFalse; + + private title: string | undefined = undefined; + + public setPinDoc(pinDoc: Doc) { + this.title = StrCast(pinDoc.title ? pinDoc.title : `${NumCast(pinDoc.longitude)}, ${NumCast(pinDoc.latitude)}`); + console.log('Title: ', this.title); + } + + public get Active() { + return this._left > 0; + } + + constructor(props: Readonly<{}>) { + super(props); + + DirectionsAnchorMenu.Instance = this; + DirectionsAnchorMenu.Instance._canFade = false; + } + + componentWillUnmount() { + this._disposer?.(); + } + + componentDidMount() { + this._disposer = reaction( + () => SelectionManager.Views.slice(), + sel => DirectionsAnchorMenu.Instance.fadeOut(true) + ); + } + // audioDown = (e: React.PointerEvent) => { + // setupMoveUpEvents(this, e, returnFalse, returnFalse, e => this.OnAudio?.(e)); + // }; + + // cropDown = (e: React.PointerEvent) => { + // setupMoveUpEvents( + // this, + // e, + // (e: PointerEvent) => { + // this.StartCropDrag(e, this._commentCont.current!); + // return true; + // }, + // returnFalse, + // e => this.OnCrop?.(e) + // ); + // }; + // notePointerDown = (e: React.PointerEvent) => { + // setupMoveUpEvent( + // this, + // e, + // (e: PointerEvent) => { + // this.StartDrag(e, this._commentRef.current!); + // return true; + // }, + // returnFalse, + // e => this.OnClick(e) + // ); + // }; + + static top = React.createRef<HTMLDivElement>(); + + // public get Top(){ + // return this.top + // } + + render() { + const buttons = ( + <div className="directions-menu-buttons" style={{ display: 'flex' }}> + <IconButton + tooltip="Add route" // + onPointerDown={this.Delete} + icon={<FontAwesomeIcon icon={faAdd 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> + ); + + return this.getElement( + <div ref={DirectionsAnchorMenu.top} style={{ height: 'max-content', width: '100%', display: 'flex', flexDirection: 'column' }}> + <div>{this.title}</div> + <div className="direction-inputs" style={{ display: 'flex', flexDirection: 'column' }}> + <input placeholder="Origin" /> + <input placeholder="Destination" /> + </div> + {buttons} + </div> + ); + } +} diff --git a/src/client/views/nodes/MapBox/GeocoderControl.tsx b/src/client/views/nodes/MapBox/GeocoderControl.tsx new file mode 100644 index 000000000..e4ba51316 --- /dev/null +++ b/src/client/views/nodes/MapBox/GeocoderControl.tsx @@ -0,0 +1,107 @@ +// import React from 'react'; +// import MapboxGeocoder , { GeocoderOptions} from '@mapbox/mapbox-gl-geocoder' +// 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'>; +// position: ControlPosition; + +// onLoading: (...args: any[]) => void; +// onResults: (...args: any[]) => void; +// onResult: (...args: any[]) => void; +// onError: (...args: any[]) => void; +// } + +// export const GeocoderControl = (props: GeocoderControlProps) => { + +// console.log(props); + +// const geocoder = useControl<MapboxGeocoder>( +// () => { +// const ctrl = new MapboxGeocoder({ +// ...props, +// marker: false, +// accessToken: props.mapboxAccessToken +// }); +// ctrl.on('loading', props.onLoading); +// ctrl.on('results', props.onResults); +// ctrl.on('result', evt => { +// props.onResult(evt); + +// // const {result} = evt; +// // const location = +// // result && +// // (result.center || (result.geometry?.type === 'Point' && result.geometry.coordinates)); +// // if (location && props.marker) { +// // setMarker(<Marker {...props.marker} longitude={location[0]} latitude={location[1]} />); +// // } else { +// // setMarker(null); +// // } +// }); +// ctrl.on('error', props.onError); +// return ctrl; +// }, +// { +// position: props.position +// } +// ); + + +// // @ts-ignore (TS2339) private member +// if (geocoder._map) { +// if (geocoder.getProximity() !== props.proximity && props.proximity !== undefined) { +// geocoder.setProximity(props.proximity); +// } +// if (geocoder.getRenderFunction() !== props.render && props.render !== undefined) { +// geocoder.setRenderFunction(props.render); +// } +// if (geocoder.getLanguage() !== props.language && props.language !== undefined) { +// geocoder.setLanguage(props.language); +// } +// if (geocoder.getZoom() !== props.zoom && props.zoom !== undefined) { +// geocoder.setZoom(props.zoom); +// } +// if (geocoder.getFlyTo() !== props.flyTo && props.flyTo !== undefined) { +// geocoder.setFlyTo(props.flyTo); +// } +// if (geocoder.getPlaceholder() !== props.placeholder && props.placeholder !== undefined) { +// geocoder.setPlaceholder(props.placeholder); +// } +// if (geocoder.getCountries() !== props.countries && props.countries !== undefined) { +// geocoder.setCountries(props.countries); +// } +// if (geocoder.getTypes() !== props.types && props.types !== undefined) { +// geocoder.setTypes(props.types); +// } +// if (geocoder.getMinLength() !== props.minLength && props.minLength !== undefined) { +// geocoder.setMinLength(props.minLength); +// } +// if (geocoder.getLimit() !== props.limit && props.limit !== undefined) { +// geocoder.setLimit(props.limit); +// } +// if (geocoder.getFilter() !== props.filter && props.filter !== undefined) { +// geocoder.setFilter(props.filter); +// } +// if (geocoder.getOrigin() !== props.origin && props.origin !== undefined) { +// geocoder.setOrigin(props.origin); +// } +// } +// return ( +// <div> +// Geocoder +// </div> +// ) +// } + +// const noop = () => {}; + +// GeocoderControl.defaultProps = { +// marker: true, +// onLoading: noop, +// onResults: noop, +// onError: noop +// };
\ No newline at end of file diff --git a/src/client/views/nodes/MapBox/MapAnchorMenu.scss b/src/client/views/nodes/MapBox/MapAnchorMenu.scss index 6990bdcf1..c36d98afe 100644 --- a/src/client/views/nodes/MapBox/MapAnchorMenu.scss +++ b/src/client/views/nodes/MapBox/MapAnchorMenu.scss @@ -51,4 +51,81 @@ border: 2px solid white; } } -}
\ No newline at end of file +} + +.map-anchor-menu-container { + display: flex; + flex-direction: column; + gap: 5px; + padding: 5px; + height: max-content; + min-width: 300px; + + .direction-inputs { + display: flex; + flex-direction: column; + gap: 5px; + + #get-routes-button { + padding: 8px 10px; + border-radius: 5px; + } + } + + .MuiInputBase-input{ + color: white !important; + } + + + .css-1t8l2tu-MuiInputBase-input-MuiOutlinedInput-input.Mui-disabled{ + -webkit-text-fill-color: #b3b2b2 !important; + } + + .current-route-info-container { + width: 100%; + + .transportation-icons-container { + display: flex; + justify-content: center; + align-items: center; + gap: 5px; + } + + .selected-route-details-container{ + display: flex; + flex-direction: column; + gap: 3px; + justify-content: center; + align-items: flex-start; + padding: 5px; + } + + + } + + .customized-marker-container{ + display: flex; + flex-direction: column; + gap: 10px; + + .current-marker-container{ + display: flex; + align-items: center; + gap: 5px; + } + + .all-markers-container{ + display: flex; + align-items: center; + gap: 10px; + flex-wrap: wrap; + max-width: 400px; + } + } + + + + +} + + diff --git a/src/client/views/nodes/MapBox/MapAnchorMenu.tsx b/src/client/views/nodes/MapBox/MapAnchorMenu.tsx index 7af4d9b59..08bea5d9d 100644 --- a/src/client/views/nodes/MapBox/MapAnchorMenu.tsx +++ b/src/client/views/nodes/MapBox/MapAnchorMenu.tsx @@ -1,15 +1,25 @@ -import React = require('react'); +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 { IReactionDisposer, ObservableMap, reaction } from 'mobx'; +import { Autocomplete, Checkbox, FormControlLabel, TextField } from '@mui/material'; +import { IconButton } from 'browndash-components'; +import { Position } from 'geojson'; +import { IReactionDisposer, ObservableMap, action, makeObservable, observable, reaction, runInAction } from 'mobx'; import { observer } from 'mobx-react'; -import { Doc, Opt } from '../../../../fields/Doc'; +import * as React from 'react'; +import { CirclePicker, ColorResult } from 'react-color'; import { returnFalse, setupMoveUpEvents, 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 { AntimodeMenu, AntimodeMenuProps } from '../../AntimodeMenu'; -// import { GPTPopup, GPTPopupMode } from './../../GPTPopup/GPTPopup'; -import { IconButton } from 'browndash-components'; import { SettingsManager } from '../../../util/SettingsManager'; +import { AntimodeMenu, AntimodeMenuProps } from '../../AntimodeMenu'; import './MapAnchorMenu.scss'; +import { MapboxApiUtility, TransportationType } from './MapboxApiUtility'; +import { MarkerIcons } from './MarkerIcons'; +// import { GPTPopup, GPTPopupMode } from './../../GPTPopup/GPTPopup'; + +type MapAnchorMenuType = 'standard' | 'routeCreation' | 'calendar' | 'customize' | 'route'; @observer export class MapAnchorMenu extends AntimodeMenu<AntimodeMenuProps> { @@ -17,6 +27,7 @@ export class MapAnchorMenu extends AntimodeMenu<AntimodeMenuProps> { private _disposer: IReactionDisposer | undefined; private _commentRef = React.createRef<HTMLDivElement>(); + private _fileInputRef = React.createRef<HTMLInputElement>(); public onMakeAnchor: () => Opt<Doc> = () => undefined; // Method to get anchor from text search @@ -30,25 +41,90 @@ export class MapAnchorMenu extends AntimodeMenu<AntimodeMenuProps> { // public MakeTargetToggle: () => void = unimplementedFunction; // public ShowTargetTrail: () => void = unimplementedFunction; public IsTargetToggler: () => boolean = returnFalse; + + public DisplayRoute: (routeInfoMap: Record<TransportationType, any> | undefined, type: TransportationType) => void = unimplementedFunction; + public AddNewRouteToMap: (coordinates: Position[], origin: string, destination: any, createPinForDestination: boolean) => void = unimplementedFunction; + public CreatePin: (feature: any) => void = unimplementedFunction; + + public UpdateMarkerColor: (color: string) => void = unimplementedFunction; + public UpdateMarkerIcon: (iconKey: string) => void = unimplementedFunction; + + public Hide: () => void = unimplementedFunction; + + public OpenAnimationPanel: (routeDoc: Doc | undefined) => void = unimplementedFunction; + + @observable + menuType: MapAnchorMenuType = 'standard'; + + @action + public setMenuType = (menuType: MapAnchorMenuType) => { + this.menuType = menuType; + }; + + private allMapPinDocs: Doc[] = []; + + private pinDoc: Doc | undefined = undefined; + + private routeDoc: Doc | undefined = undefined; + + private title: string | undefined = undefined; + + public setPinDoc(pinDoc: Doc | undefined) { + if (pinDoc) { + this.pinDoc = pinDoc; + this.title = StrCast(pinDoc.title ? pinDoc.title : `${NumCast(pinDoc.longitude)}, ${NumCast(pinDoc.latitude)}`); + } + } + + public setRouteDoc(routeDoc: Doc | undefined) { + if (routeDoc) { + this.routeDoc = routeDoc; + this.title = StrCast(routeDoc.title ?? 'Map route'); + } + } + + @action + public Reset() { + this.destinationSelected = false; + this.currentRouteInfoMap = undefined; + this.destinationFeatures = []; + this.selectedDestinationFeature = undefined; + this.allMapPinDocs = []; + this.title = undefined; + this.routeDoc = undefined; + this.pinDoc = undefined; + } + + public setAllMapboxPins(pinDocs: Doc[]) { + this.allMapPinDocs = pinDocs; + pinDocs.forEach((p, idx) => { + console.log(`Pin ${idx}: ${p.title}`); + }); + } + public get Active() { return this._left > 0; } - constructor(props: Readonly<{}>) { + constructor(props: any) { super(props); - + makeObservable(this); MapAnchorMenu.Instance = this; MapAnchorMenu.Instance._canFade = false; } componentWillUnmount() { + this.destinationFeatures = []; + this.destinationSelected = false; + this.selectedDestinationFeature = undefined; + this.currentRouteInfoMap = undefined; this._disposer?.(); } componentDidMount() { this._disposer = reaction( - () => SelectionManager.Views().slice(), - selected => MapAnchorMenu.Instance.fadeOut(true) + () => SelectionManager.Views.slice(), + sel => MapAnchorMenu.Instance.fadeOut(true) ); } // audioDown = (e: React.PointerEvent) => { @@ -81,39 +157,267 @@ export class MapAnchorMenu extends AntimodeMenu<AntimodeMenuProps> { }; static top = React.createRef<HTMLDivElement>(); + // public get Top(){ // return this.top // } + @action + DirectionsClick = () => { + this.menuType = 'routeCreation'; + }; + + @action + CustomizeClick = () => { + this.currentRouteInfoMap = undefined; + this.menuType = 'customize'; + }; + + @action + BackClick = () => { + this.currentRouteInfoMap = undefined; + this.menuType = 'standard'; + }; + + @action + TriggerFileInputClick = () => { + if (this._fileInputRef) { + this._fileInputRef.current?.click(); // Trigger the file input click event + } + }; + + @action + onMarkerColorChange = (color: ColorResult) => { + if (this.pinDoc) { + this.pinDoc.markerColor = color.hex; + } + }; + + revertToOriginalMarker = () => { + if (this.pinDoc) { + this.pinDoc.markerType = 'MAP_PIN'; + this.pinDoc.markerColor = '#ff5722'; + } + }; + + onMarkerIconChange = (iconKey: string) => { + if (this.pinDoc) { + this.pinDoc.markerType = iconKey; + } + }; + + @observable + destinationFeatures: any[] = []; + + @observable + destinationSelected: boolean = false; + + @observable + selectedDestinationFeature: any = undefined; + + @observable + createPinForDestination: boolean = true; + + @observable + currentRouteInfoMap: Record<TransportationType, any> | undefined = undefined; + + @observable + selectedTransportationType: TransportationType = 'driving'; + + @action + handleTransportationTypeChange = (newType: TransportationType) => { + if (newType !== this.selectedTransportationType) { + this.selectedTransportationType = newType; + this.DisplayRoute(this.currentRouteInfoMap, newType); + } + }; + + @action + handleSelectedDestinationFeature = (destinationFeature: any) => { + this.selectedDestinationFeature = destinationFeature; + }; + + @action + toggleCreatePinForDestinationCheckbox = () => { + this.createPinForDestination = !this.createPinForDestination; + }; + + @action + handleDestinationSearchChange = async (searchText: string) => { + if (this.selectedDestinationFeature !== undefined) this.selectedDestinationFeature = undefined; + const features = await MapboxApiUtility.forwardGeocodeForFeatures(searchText); + if (features) { + runInAction(() => { + this.destinationFeatures = features; + }); + } + }; + + getRoutes = async (destinationFeature: any) => { + const currentPinLong: number = NumCast(this.pinDoc?.longitude); + const currentPinLat: number = NumCast(this.pinDoc?.latitude); + + if (currentPinLong && currentPinLat && destinationFeature.center) { + const routeInfoMap = await MapboxApiUtility.getDirections([currentPinLong, currentPinLat], destinationFeature.center); + if (routeInfoMap) { + runInAction(() => { + this.currentRouteInfoMap = routeInfoMap; + }); + this.DisplayRoute(routeInfoMap, 'driving'); + } + } + + // get route menu, set it equal to here + // create a temporary route + // create pin if createPinForDestination was clicked + }; + + HandleAddRouteClick = () => { + if (this.currentRouteInfoMap && this.selectedTransportationType && this.selectedDestinationFeature) { + const coordinates = this.currentRouteInfoMap[this.selectedTransportationType].coordinates; + console.log(coordinates); + console.log(this.selectedDestinationFeature); + this.AddNewRouteToMap(coordinates, this.title ?? '', this.selectedDestinationFeature, this.createPinForDestination); + } + }; + + getMarkerIcon = (): JSX.Element | undefined => { + if (this.pinDoc) { + const markerType = StrCast(this.pinDoc.markerType); + const markerColor = StrCast(this.pinDoc.markerColor); + + return MarkerIcons.getFontAwesomeIcon(markerType, '2x', markerColor); + } + return undefined; + }; + + 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} + /> + ); + }; + 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> + ); + }; + + linkNoteToPinOrRoutenButton: JSX.Element = ( + <div ref={this._commentRef}> + <IconButton + tooltip="Link Note to Pin" // + onPointerDown={this.notePointerDown} + icon={<FontAwesomeIcon icon="sticky-note" />} + color={SettingsManager.userColor} + /> + </div> + ); + + customizePinButton: JSX.Element = (<IconButton tooltip="Customize pin" onPointerDown={this.CustomizeClick} icon={<FontAwesomeIcon icon={faEdit as IconLookup} />} color={SettingsManager.userColor} />); + + centerOnPinButton: JSX.Element = ( + <IconButton + tooltip="Center on pin" // + onPointerDown={this.Center} + icon={<FontAwesomeIcon icon="compress-arrows-alt" />} + color={SettingsManager.userColor} + /> + ); + + backButton: JSX.Element = ( + <IconButton + tooltip="Go back" // + onPointerDown={this.BackClick} + icon={<FontAwesomeIcon icon={faArrowLeft as IconLookup} />} + color={SettingsManager.userColor} + /> + ); + + addRouteButton: JSX.Element = ( + <IconButton + tooltip="Add route" // + onPointerDown={this.HandleAddRouteClick} + icon={<FontAwesomeIcon icon={faAdd as IconLookup} />} + color={SettingsManager.userColor} + /> + ); + + getDeleteButton = (type: string) => { + return ( + <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} />); + + revertToOriginalMarkerButton = ( + <IconButton + tooltip="Revert to original" // + onPointerDown={() => this.revertToOriginalMarker()} + icon={<FontAwesomeIcon icon={faArrowsRotate as IconLookup} />} + color={SettingsManager.userColor} + /> + ); + render() { const buttons = ( - <> - { - <IconButton - tooltip="Delete Pin" // - onPointerDown={this.Delete} - icon={<FontAwesomeIcon icon="trash-alt" />} - color={SettingsManager.userColor} - /> - } - { - <div ref={this._commentRef}> - <IconButton - tooltip="Link Note to Pin" // - onPointerDown={this.notePointerDown} - icon={<FontAwesomeIcon icon="sticky-note" />} - color={SettingsManager.userColor} - /> - </div> - } - { - <IconButton - tooltip="Center on pin" // - onPointerDown={this.Center} - icon={<FontAwesomeIcon icon="compress-arrows-alt" />} - color={SettingsManager.userColor} - /> - } + <div className="menu-buttons" style={{ display: 'flex' }}> + {this.menuType === 'standard' && ( + <> + {this.getDeleteButton('pin')} + {this.getDirectionsButton} + {this.getAddToCalendarButton('pin')} + {this.getLinkNoteToDocButton('pin')} + {this.customizePinButton} + {this.centerOnPinButton} + </> + )} + {this.menuType === 'routeCreation' && ( + <> + {this.backButton} + {this.addRouteButton} + </> + )} + {this.menuType === 'route' && ( + <> + {this.getDeleteButton('route')} + {this.animateRouteButton} + {this.getAddToCalendarButton('route')} + {this.getLinkNoteToDocButton('route')} + </> + )} + {this.menuType === 'customize' && ( + <> + {this.backButton} + {this.revertToOriginalMarkerButton} + </> + )} + {/* {this.IsTargetToggler !== returnFalse && ( <Toggle tooltip={'Make target visibility toggle on click'} @@ -125,13 +429,100 @@ export class MapAnchorMenu extends AntimodeMenu<AntimodeMenuProps> { color={SettingsManager.userColor} /> )} */} - </> + </div> ); return this.getElement( - <div ref={MapAnchorMenu.top} style={{ width: '100%', display: 'flex' }}> + <div ref={MapAnchorMenu.top} className="map-anchor-menu-container"> + {this.menuType === 'standard' && <div>{this.title}</div>} + {this.menuType === 'routeCreation' && ( + <div className="direction-inputs" style={{ display: 'flex', flexDirection: 'column' }}> + <TextField fullWidth disabled value={this.title} /> + <FontAwesomeIcon icon={faArrowDown as IconLookup} size="xs" /> + <Autocomplete + fullWidth + id="route-destination-searcher" + onInputChange={(e: any, searchText: any) => this.handleDestinationSearchChange(searchText)} + onChange={(e: any, feature: any, reason: any) => { + if (reason === 'clear') { + this.handleSelectedDestinationFeature(undefined); + } else if (reason === 'selectOption') { + this.handleSelectedDestinationFeature(feature); + } + }} + options={this.destinationFeatures.filter(feature => feature.place_name).map(feature => feature)} + getOptionLabel={(feature: any) => feature.place_name} + 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)}> + Get routes + </button> + + {/* <input + placeholder="Origin" + /> */} + </div> + )} + {this.currentRouteInfoMap && ( + <div className="current-route-info-container"> + <div className="transportation-icons-container"> + <IconButton + tooltip="Driving route" + onPointerDown={() => this.handleTransportationTypeChange('driving')} + icon={<FontAwesomeIcon icon={faCar as IconLookup} />} + color={this.selectedTransportationType === 'driving' ? 'lightblue' : 'grey'} + /> + <IconButton + tooltip="Cycling route" + onPointerDown={() => this.handleTransportationTypeChange('cycling')} + icon={<FontAwesomeIcon icon={faBicycle as IconLookup} />} + color={this.selectedTransportationType === 'cycling' ? 'lightblue' : 'grey'} + /> + <IconButton + tooltip="Walking route" + onPointerDown={() => this.handleTransportationTypeChange('walking')} + icon={<FontAwesomeIcon icon={faPersonWalking as IconLookup} />} + color={this.selectedTransportationType === 'walking' ? 'lightblue' : 'grey'} + /> + </div> + <div className="selected-route-details-container"> + <div>Duration: {this.currentRouteInfoMap[this.selectedTransportationType].duration}</div> + <div>Distance: {this.currentRouteInfoMap[this.selectedTransportationType].distance}</div> + </div> + </div> + )} + {this.menuType === 'customize' && ( + <div className="customized-marker-container"> + <div className="current-marker-container"> + <div>Current Marker: </div> + <div>{this.getMarkerIcon()}</div> + </div> + <div className="color-picker-container" style={{ marginBottom: '10px' }}> + <CirclePicker circleSize={15} circleSpacing={7} width="100%" onChange={color => this.onMarkerColorChange(color)} /> + </div> + <div className="all-markers-container"> + {Object.keys(MarkerIcons.FAMarkerIconsMap).map(iconKey => ( + <div key={iconKey} className="marker-icon"> + <IconButton onPointerDown={() => this.onMarkerIconChange(iconKey)} icon={MarkerIcons.getFontAwesomeIcon(iconKey, '1x', 'white')} /> + </div> + ))} + </div> + <div style={{ width: '100%', height: '3px', color: 'white' }}></div> + </div> + )} + {this.menuType === 'route' && this.routeDoc && <div>{StrCast(this.routeDoc.title)}</div>} {buttons} - </div> + </div>, + true ); } } diff --git a/src/client/views/nodes/MapBox/MapBox.scss b/src/client/views/nodes/MapBox/MapBox.scss index 242677231..25b4587a5 100644 --- a/src/client/views/nodes/MapBox/MapBox.scss +++ b/src/client/views/nodes/MapBox/MapBox.scss @@ -1,9 +1,17 @@ -@import '../../global/globalCssVariables.scss'; +@import '../../global/globalCssVariables.module.scss'; .mapBox { width: 100%; height: 100%; overflow: hidden; display: flex; + position: absolute; + + .mapboxgl-map { + overflow: unset !important; + } + .mapboxgl-ctrl { + display: none !important; + } .mapBox-infoWindow { background-color: white; @@ -14,15 +22,133 @@ .mapBox-searchbar { display: flex; flex-direction: row; + gap: 5px; + align-items: center; width: calc(100% - 40px); - .editableText-container { + + // .editableText-container { + // width: 100%; + // font-size: 16px !important; + // } + // input { + // width: 100%; + // } + } + + .mapbox-settings-panel { + z-index: 900; + padding: 10px 20px; + display: flex; + background-color: rgb(187, 187, 187); + font-size: 1.3em; + flex-direction: column; + align-items: flex-start; + justify-content: center; + gap: 7px; + position: absolute; + border-top-left-radius: 5px; + border-bottom-left-radius: 5px; + + .mapbox-style-select { + display: flex; + flex-direction: column; + align-items: flex-start; + justify-content: center; + gap: 4px; + } + + .mapbox-terrain-selection { + display: flex; + flex-direction: row; + align-items: center; + justify-content: flex-start; + gap: 4px; + } + } + + .mapbox-geocoding-search-results { + z-index: 900; + display: flex; + flex-direction: column; + justify-content: center; + align-items: flex-start; + position: absolute; + background-color: rgb(187, 187, 187); + font-size: 1.4em; + padding: 10px; + border-top-right-radius: 5px; + border-bottom-right-radius: 5px; + + .search-result-container { width: 100%; - font-size: 16px !important; + padding: 10px; + &:hover { + background-color: lighten(rgb(187, 187, 187), 10%); + } } - input { + } + + .animation-panel { + z-index: 900; + display: flex; + flex-direction: column; + justify-content: flex-start; + align-items: flex-start; + background-color: rgb(187, 187, 187); + padding: 10px; + border-top-right-radius: 5px; + border-bottom-right-radius: 5px; + position: absolute; + + #route-to-animate-title { + font-size: 1.25em; + font-weight: bold; + } + + .route-animation-options { + display: flex; + justify-content: flex-start; + align-items: center; width: 100%; + + .animation-suboptions { + display: flex; + justify-content: flex-start; + align-items: center; + gap: 7px; + width: 100%; + + label { + margin-bottom: 0; + } + + .speed-label { + margin-right: 5px; + } + + #divider { + margin-left: 10px; + margin-right: 10px; + } + } } } + + .zoom-box { + position: absolute; + z-index: 900; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + background-color: white; + font-size: 1.4em; + border-radius: 5px; + bottom: 5px; + left: 5px; + padding: 3px; + } + .mapBox-topbar { display: flex; flex-direction: row; diff --git a/src/client/views/nodes/MapBox/MapBox.tsx b/src/client/views/nodes/MapBox/MapBox.tsx index 50b070e7f..c185c66fc 100644 --- a/src/client/views/nodes/MapBox/MapBox.tsx +++ b/src/client/views/nodes/MapBox/MapBox.tsx @@ -1,34 +1,44 @@ +import { IconLookup, faCircleXmark, faGear, faPause, faPlay, faRotate } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import BingMapsReact from 'bingmaps-react'; -import { Button, EditableText, IconButton, Type } from 'browndash-components'; -import { action, computed, IReactionDisposer, observable, ObservableMap, reaction, runInAction } from 'mobx'; +import { Checkbox, FormControlLabel, TextField } from '@mui/material'; +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 { IReactionDisposer, ObservableMap, action, autorun, computed, makeObservable, observable, reaction, 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 { Doc, DocListCast, Field, LinkedTo, Opt } from '../../../../fields/Doc'; -import { DocCss, Highlight, Width } from '../../../../fields/DocSymbols'; -import { Id } from '../../../../fields/FieldSymbols'; -import { InkTool } from '../../../../fields/InkField'; +import { DocCss, Highlight } from '../../../../fields/DocSymbols'; import { DocCast, NumCast, StrCast } from '../../../../fields/Types'; -import { emptyFunction, returnEmptyDoclist, returnEmptyFilter, returnEmptyString, returnFalse, returnOne, setupMoveUpEvents, Utils } from '../../../../Utils'; -import { Docs, DocUtils } from '../../../documents/Documents'; import { DocumentType } from '../../../documents/DocumentTypes'; +import { DocUtils, Docs } from '../../../documents/Documents'; import { DocumentManager } from '../../../util/DocumentManager'; import { DragManager } from '../../../util/DragManager'; import { LinkManager } from '../../../util/LinkManager'; import { SnappingManager } from '../../../util/SnappingManager'; -import { Transform } from '../../../util/Transform'; -import { undoable, UndoManager } from '../../../util/UndoManager'; +import { UndoManager, undoable } from '../../../util/UndoManager'; +import { ViewBoxAnnotatableComponent, ViewBoxInterface } from '../../DocComponent'; +import { SidebarAnnos } from '../../SidebarAnnos'; import { MarqueeOptionsMenu } from '../../collections/collectionFreeForm'; -import { ViewBoxAnnotatableComponent, ViewBoxAnnotatableProps } from '../../DocComponent'; import { Colors } from '../../global/globalEnums'; -import { MarqueeAnnotator } from '../../MarqueeAnnotator'; -import { SidebarAnnos } from '../../SidebarAnnos'; import { DocumentView } from '../DocumentView'; -import { FieldView, FieldViewProps } from '../FieldView'; +import { FocusViewOptions, FieldView, FieldViewProps } from '../FieldView'; import { FormattedTextBox } from '../formattedText/FormattedTextBox'; import { PinProps, PresBox } from '../trails'; +import { fastSpeedIcon, mediumSpeedIcon, slowSpeedIcon } from './AnimationSpeedIcons'; +import { AnimationSpeed, AnimationStatus, AnimationUtility } from './AnimationUtility'; import { MapAnchorMenu } from './MapAnchorMenu'; import './MapBox.scss'; +import { MapboxApiUtility, TransportationType } from './MapboxApiUtility'; +import { MarkerIcons } from './MarkerIcons'; +// import { GeocoderControl } from './GeocoderControl'; + // amongus /** * MapBox architecture: @@ -44,6 +54,30 @@ import './MapBox.scss'; */ 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> +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; + latitude: number; + title: string; + description: string; +}; + +// export type GeocoderControlProps = Omit<GeocoderOptions, 'accessToken' | 'mapboxgl' | 'marker'> & { +// mapboxAccessToken: string; +// marker?: Omit<MarkerProps, 'longitude' | 'latitude'>; +// position: ControlPosition; + +// onResult: (...args: any[]) => void; +// }; + +type MapMarker = { + longitude: number; + latitude: number; +}; /** * Consider integrating later: allows for drawing, circling, making shapes on map @@ -62,19 +96,22 @@ const bingApiKey = process.env.BING_MAPS; // if you're running local, get a Bing // }); @observer -export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps & FieldViewProps>() { +export class MapBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implements ViewBoxInterface { public static LayoutString(fieldKey: string) { return FieldView.LayoutString(MapBox, fieldKey); } private _dragRef = React.createRef<HTMLDivElement>(); - private _mainCont: React.RefObject<HTMLDivElement> = React.createRef(); - private _annotationLayer: React.RefObject<HTMLDivElement> = React.createRef(); private _sidebarRef = React.createRef<SidebarAnnos>(); private _ref: React.RefObject<HTMLDivElement> = React.createRef(); + private _mapRef: React.RefObject<MapRef> = React.createRef(); private _disposers: { [key: string]: IReactionDisposer } = {}; private _setPreviewCursor: undefined | ((x: number, y: number, drag: boolean, hide: boolean, doc: Opt<Doc>) => void); - @observable private _marqueeing: number[] | undefined; + constructor(props: FieldViewProps) { + super(props); + makeObservable(this); + } + @observable private _savedAnnotations = new ObservableMap<number, HTMLDivElement[]>(); @computed get allSidebarDocs() { return DocListCast(this.dataDoc[this.SidebarKey]); @@ -86,6 +123,116 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps @computed get allPushpins() { return this.allAnnotations.filter(anno => anno.type === DocumentType.PUSHPIN); } + @computed get allRoutes() { + return this.allAnnotations.filter(anno => anno.type === DocumentType.MAPROUTE); + } + @computed get updatedRouteCoordinates(): Feature<Geometry, GeoJsonProperties> { + if (this.routeToAnimate?.routeCoordinates) { + const originalCoordinates: Position[] = JSON.parse(StrCast(this.routeToAnimate.routeCoordinates)); + // const index = Math.floor(this.animationPhase * originalCoordinates.length); + const index = this.animationPhase * (originalCoordinates.length - 1); // Calculate the fractional index + console.log('Animation phase', this.animationPhase); + const startIndex = Math.floor(index); + const endIndex = Math.ceil(index); + let feature: Feature<Geometry, GeoJsonProperties>; + + let geometry: LineString; + if (startIndex === endIndex) { + // AnimationPhase is at a whole number (no interpolation needed) + const coordinates = [originalCoordinates[startIndex]]; + geometry = { + type: 'LineString', + coordinates, + }; + feature = { + type: 'Feature', + properties: { + routeTitle: StrCast(this.routeToAnimate.title), + }, + geometry: geometry, + }; + } else { + // Interpolate between two coordinates + const startCoord = originalCoordinates[startIndex]; + const endCoord = originalCoordinates[endIndex]; + const fraction = index - startIndex; + + const interpolator = d3.interpolateArray(startCoord, endCoord); + + const interpolatedCoord = interpolator(fraction); + + const coordinates = originalCoordinates.slice(0, startIndex + 1).concat([interpolatedCoord]); + + geometry = { + type: 'LineString', + coordinates, + }; + feature = { + type: 'Feature', + properties: { + routeTitle: StrCast(this.routeToAnimate.title), + }, + geometry: geometry, + }; + } + + autorun(() => { + const animationUtil = this.animationUtility; + const concattedCoordinates = geometry.coordinates.concat(originalCoordinates.slice(endIndex)); + const newFeature: Feature<LineString, turf.Properties> = { + type: 'Feature', + properties: {}, + geometry: { + type: 'LineString', + coordinates: concattedCoordinates, + }, + }; + if (animationUtil) { + animationUtil.setPath(newFeature); + } + }); + return feature; + } + console.log('ERROR'); + return { + type: 'Feature', + properties: {}, + geometry: { + type: 'LineString', + coordinates: [], + }, + }; + } + @computed get selectedRouteCoordinates(): Position[] { + let coordinates: Position[] = []; + if (this.routeToAnimate?.routeCoordinates) { + coordinates = JSON.parse(StrCast(this.routeToAnimate.routeCoordinates)); + } + return coordinates; + } + + @computed get allRoutesGeoJson(): FeatureCollection { + const features: Feature<Geometry, GeoJsonProperties>[] = this.allRoutes.map((routeDoc: Doc) => { + console.log('Route coords: ', routeDoc.routeCoordinates); + const geometry: LineString = { + type: 'LineString', + coordinates: JSON.parse(StrCast(routeDoc.routeCoordinates)), + }; + return { + type: 'Feature', + properties: { + routeTitle: routeDoc.title, + }, + geometry: geometry, + }; + }); + + return { + type: 'FeatureCollection', + features: features, + }; + } + @computed get SidebarShown() { return this.layoutDoc._layout_showSidebar ? true : false; } @@ -93,7 +240,7 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps return StrCast(this.layoutDoc._layout_sidebarWidthPercent, '0%'); } @computed get sidebarColor() { - return StrCast(this.layoutDoc.sidebar_color, StrCast(this.layoutDoc[this.props.fieldKey + '_backgroundColor'], '#e4e4e4')); + return StrCast(this.layoutDoc.sidebar_color, StrCast(this.layoutDoc[this._props.fieldKey + '_backgroundColor'], '#e4e4e4')); } @computed get SidebarKey() { return this.fieldKey + '_sidebar'; @@ -101,13 +248,13 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps componentDidMount() { this._unmounting = false; - this.props.setContentView?.(this); + this._props.setContentViewBox?.(this); } _unmounting = false; componentWillUnmount(): void { this._unmounting = true; - this.deselectPin(); + this.deselectPinOrRoute(); this._rerenderTimeout && clearTimeout(this._rerenderTimeout); Object.keys(this._disposers).forEach(key => this._disposers[key]?.()); } @@ -122,7 +269,7 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps if (!this.layoutDoc._layout_showSidebar) this.toggleSidebar(); const docs = doc instanceof Doc ? [doc] : doc; docs.forEach(doc => { - let existingPin = this.allPushpins.find(pin => pin.latitude === doc.latitude && pin.longitude === doc.longitude) ?? this.selectedPin; + 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)); } @@ -166,11 +313,11 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps e, (e, down, delta) => runInAction(() => { - const localDelta = this.props + const localDelta = this._props .ScreenToLocalTransform() - .scale(this.props.NativeDimScaling?.() || 1) + .scale(this._props.NativeDimScaling?.() || 1) .transformDirection(delta[0], delta[1]); - const fullWidth = this.layoutDoc[Width](); + const fullWidth = NumCast(this.layoutDoc._width); const mapWidth = fullWidth - this.sidebarWidth(); if (this.sidebarWidth() + localDelta[0] > 0) { this.layoutDoc._layout_showSidebar = true; @@ -187,7 +334,7 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps () => UndoManager.RunInBatch(this.toggleSidebar, 'toggle sidebar map') ); }; - sidebarWidth = () => (Number(this.sidebarWidthPercent.substring(0, this.sidebarWidthPercent.length - 1)) / 100) * this.props.PanelWidth(); + sidebarWidth = () => (Number(this.sidebarWidthPercent.substring(0, this.sidebarWidthPercent.length - 1)) / 100) * this._props.PanelWidth(); /** * Handles toggle of sidebar on click the little comment button @@ -199,8 +346,8 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps key="sidebar" title="Toggle Sidebar" style={{ - display: !this.props.isContentActive() ? 'none' : undefined, - top: StrCast(this.rootDoc._layout_showTitle) === 'title' ? 20 : 5, + display: !this._props.isContentActive() ? 'none' : undefined, + top: StrCast(this.Document._layout_showTitle) === 'title' ? 20 : 5, backgroundColor: this.SidebarShown ? Colors.MEDIUM_BLUE : Colors.BLACK, }} onPointerDown={this.sidebarBtnDown}> @@ -223,25 +370,25 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps const sourceAnchorCreator = action(() => { const note = this.getAnchor(true); - if (note && this.selectedPin) { - note.latitude = this.selectedPin.latitude; - note.longitude = this.selectedPin.longitude; - note.map = this.selectedPin.map; + if (note && this.selectedPinOrRoute) { + note.latitude = this.selectedPinOrRoute.latitude; + note.longitude = this.selectedPinOrRoute.longitude; + note.map = this.selectedPinOrRoute.map; } return note as Doc; }); const targetCreator = (annotationOn: Doc | undefined) => { - const target = DocUtils.GetNewTextDoc('Note linked to ' + this.rootDoc.title, 0, 0, 100, 100, undefined, annotationOn, undefined, 'yellow'); - FormattedTextBox.SelectOnLoad = target[Id]; + const target = DocUtils.GetNewTextDoc('Note linked to ' + this.Document.title, 0, 0, 100, 100, undefined, annotationOn, 'yellow'); + FormattedTextBox.SetSelectOnLoad(target); return target; }; - const docView = this.props.DocumentView?.(); + 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.props.Document; + e.annoDragData.linkSourceDoc.followLinkToggle = e.annoDragData.dropDocument.annotationOn === this.Document; e.annoDragData.linkSourceDoc.followLinkZoom = false; } }, @@ -252,10 +399,10 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps const createFunc = undoable( action(() => { const note = this._sidebarRef.current?.anchorMenuClick(this.getAnchor(true), ['latitude', 'longitude', LinkedTo]); - if (note && this.selectedPin) { - note.latitude = this.selectedPin.latitude; - note.longitude = this.selectedPin.longitude; - note.map = this.selectedPin.map; + if (note && this.selectedPinOrRoute) { + note.latitude = this.selectedPinOrRoute.latitude; + note.longitude = this.selectedPinOrRoute.longitude; + note.map = this.selectedPinOrRoute.map; } }), 'create note annotation' @@ -278,39 +425,17 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps setPreviewCursor = (func?: (x: number, y: number, drag: boolean, hide: boolean, doc: Opt<Doc>) => void) => (this._setPreviewCursor = func); - @action - onMarqueeDown = (e: React.PointerEvent) => { - if (!e.altKey && e.button === 0 && this.props.isContentActive(true) && ![InkTool.Highlighter, InkTool.Pen, InkTool.Write].includes(Doc.ActiveTool)) { - setupMoveUpEvents( - this, - e, - action(e => { - MarqueeAnnotator.clearAnnotations(this._savedAnnotations); - this._marqueeing = [e.clientX, e.clientY]; - return true; - }), - returnFalse, - () => MarqueeAnnotator.clearAnnotations(this._savedAnnotations), - false - ); - } - }; - @action finishMarquee = (x?: number, y?: number) => { - this._marqueeing = undefined; - x !== undefined && y !== undefined && this._setPreviewCursor?.(x, y, false, false, this.rootDoc); - }; - addDocumentWrapper = (doc: Doc | Doc[], annotationKey?: string) => this.addDocument(doc, annotationKey); - pointerEvents = () => (this.props.isContentActive() && !MarqueeOptionsMenu.Instance.isShown() ? 'all' : 'none'); + pointerEvents = () => (this._props.isContentActive() && !MarqueeOptionsMenu.Instance.isShown() ? 'all' : 'none'); - panelWidth = () => this.props.PanelWidth() / (this.props.NativeDimScaling?.() || 1) - this.sidebarWidth(); - panelHeight = () => this.props.PanelHeight() / (this.props.NativeDimScaling?.() || 1); - scrollXf = () => this.props.ScreenToLocalTransform().translate(0, NumCast(this.layoutDoc._layout_scrollTop)); - transparentFilter = () => [...this.props.childFilters(), Utils.IsTransparentFilter()]; - opaqueFilter = () => [...this.props.childFilters(), Utils.IsOpaqueFilter()]; - infoWidth = () => this.props.PanelWidth() / 5; - infoHeight = () => this.props.PanelHeight() / 5; + panelWidth = () => this._props.PanelWidth() / (this._props.NativeDimScaling?.() || 1) - this.sidebarWidth(); + panelHeight = () => this._props.PanelHeight() / (this._props.NativeDimScaling?.() || 1); + scrollXf = () => this.ScreenToLocalBoxXf().translate(0, NumCast(this.layoutDoc._layout_scrollTop)); + transparentFilter = () => [...this._props.childFilters(), Utils.TransparentBackgroundFilter]; + opaqueFilter = () => [...this._props.childFilters(), Utils.OpaqueBackgroundFilter]; + infoWidth = () => this._props.PanelWidth() / 5; + infoHeight = () => this._props.PanelHeight() / 5; anchorMenuClick = () => this._sidebarRef.current?.anchorMenuClick; savedAnnotations = () => this._savedAnnotations; @@ -348,60 +473,42 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps }; @observable - bingSearchBarContents: any = this.rootDoc.map; // For Bing Maps: The contents of the Bing search bar (string) + bingSearchBarContents: any = this.Document.map; // For Bing Maps: The contents of the Bing search bar (string) geoDataRequestOptions = { entityType: 'PopulatedPlace', }; - // incrementer: number = 0; - /* - * Creates Pushpin doc and adds it to the list of annotations - */ - @action - createPushpin = undoable((latitude: number, longitude: number, map?: string) => { - // Stores the pushpin as a MapMarkerDocument - const pushpin = Docs.Create.PushpinDocument( - NumCast(latitude), - NumCast(longitude), - false, - [], - { title: map ?? `lat=${latitude},lng=${longitude}`, map: map } - // ,'pushpinIDamongus'+ this.incrementer++ - ); - this.addDocument(pushpin, this.annotationKey); - return pushpin; - // mapMarker.infoWindowOpen = true; - }, 'createpin'); - // The pin that is selected - @observable selectedPin: Doc | undefined; + @observable selectedPinOrRoute: Doc | undefined = undefined; @action - deselectPin = () => { - if (this.selectedPin) { - // Removes filter - Doc.setDocFilter(this.rootDoc, 'latitude', this.selectedPin.latitude, 'remove'); - Doc.setDocFilter(this.rootDoc, 'longitude', this.selectedPin.longitude, 'remove'); - Doc.setDocFilter(this.rootDoc, LinkedTo, `mapPin=${Field.toScriptString(DocCast(this.selectedPin))}`, 'remove'); - - const temp = this.selectedPin; - if (!this._unmounting) { - 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)); - if (!this._unmounting) { - this._bingMap.current.entities.push(newpin); - } - this.map_docToPinMap.set(temp, newpin); - this.selectedPin = undefined; - this.bingSearchBarContents = this.rootDoc.map; + deselectPinOrRoute = () => { + if (this.selectedPinOrRoute) { + // // Removes filter + // Doc.setDocFilter(this.Document, 'latitude', this.selectedPin.latitude, 'remove'); + // Doc.setDocFilter(this.Document, 'longitude', this.selectedPin.longitude, 'remove'); + // Doc.setDocFilter(this.Document, LinkedTo, `mapPin=${Field.toScriptString(DocCast(this.selectedPin))}`, 'remove'); + // const temp = this.selectedPin; + // if (!this._unmounting) { + // 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)); + // if (!this._unmounting) { + // this._bingMap.current.entities.push(newpin); + // } + // this.map_docToPinMap.set(temp, newpin); + // this.selectedPin = undefined; + // this.bingSearchBarContents = this.Document.map; } }; - getView = async (doc: Doc) => { - if (this._sidebarRef?.current?.makeDocUnfiltered(doc) && !this.SidebarShown) this.toggleSidebar(); + getView = async (doc: Doc, options: FocusViewOptions) => { + if (this._sidebarRef?.current?.makeDocUnfiltered(doc) && !this.SidebarShown) { + this.toggleSidebar(); + options.didMove = true; + } return new Promise<Opt<DocumentView>>(res => DocumentManager.Instance.AddViewRenderedCb(doc, dv => res(dv))); }; /* @@ -409,25 +516,25 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps */ @action pushpinClicked = (pinDoc: Doc) => { - this.deselectPin(); - this.selectedPin = pinDoc; + this.deselectPinOrRoute(); + this.selectedPinOrRoute = pinDoc; this.bingSearchBarContents = pinDoc.map; - // Doc.setDocFilter(this.rootDoc, 'latitude', this.selectedPin.latitude, 'match'); - // Doc.setDocFilter(this.rootDoc, 'longitude', this.selectedPin.longitude, 'match'); - Doc.setDocFilter(this.rootDoc, LinkedTo, `mapPin=${Field.toScriptString(this.selectedPin)}`, 'check'); + // Doc.setDocFilter(this.Document, 'latitude', this.selectedPin.latitude, 'match'); + // Doc.setDocFilter(this.Document, 'longitude', this.selectedPin.longitude, 'match'); + Doc.setDocFilter(this.Document, LinkedTo, `mapPin=${Field.toScriptString(this.selectedPinOrRoute)}`, 'check'); - this.recolorPin(this.selectedPin, 'green'); + this.recolorPin(this.selectedPinOrRoute, 'green'); - MapAnchorMenu.Instance.Delete = this.deleteSelectedPin; + MapAnchorMenu.Instance.Delete = this.deleteSelectedPinOrRoute; MapAnchorMenu.Instance.Center = this.centerOnSelectedPin; MapAnchorMenu.Instance.OnClick = this.createNoteAnnotation; MapAnchorMenu.Instance.StartDrag = this.startAnchorDrag; - const point = this._bingMap.current.tryLocationToPixel(new this.MicrosoftMaps.Location(this.selectedPin.latitude, this.selectedPin.longitude)); - const x = point.x + (this.props.PanelWidth() - this.sidebarWidth()) / 2; - const y = point.y + this.props.PanelHeight() / 2 + 32; - const cpt = this.props.ScreenToLocalTransform().inverse().transformPoint(x, y); + const point = this._bingMap.current.tryLocationToPixel(new this.MicrosoftMaps.Location(this.selectedPinOrRoute.latitude, this.selectedPinOrRoute.longitude)); + const x = point.x + (this._props.PanelWidth() - this.sidebarWidth()) / 2; + const y = point.y + this._props.PanelHeight() / 2 + 32; + const cpt = this.ScreenToLocalBoxXf().inverse().transformPoint(x, y); MapAnchorMenu.Instance.jumpTo(cpt[0], cpt[1], true); document.addEventListener('pointerdown', this.tryHideMapAnchorMenu, true); @@ -438,8 +545,8 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps */ @action mapOnClick = (e: { location: { latitude: any; longitude: any } }) => { - this.props.select(false); - this.deselectPin(); + this._props.select(false); + this.deselectPinOrRoute(); }; /* * Updates values of layout doc to match the current map @@ -484,24 +591,24 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps getAnchor = (addAsAnnotation: boolean, pinProps?: PinProps, existingPin?: Doc) => { /// this should use SELECTED pushpin for lat/long if there is a selection, otherwise CENTER const anchor = Docs.Create.ConfigDocument({ - title: 'MapAnchor:' + this.rootDoc.title, - text: StrCast(this.selectedPin?.map) || StrCast(this.rootDoc.map) || 'map location', - config_latitude: NumCast((existingPin ?? this.selectedPin)?.latitude ?? this.dataDoc.latitude), - config_longitude: NumCast((existingPin ?? this.selectedPin)?.longitude ?? this.dataDoc.longitude), + title: 'MapAnchor:' + this.Document.title, + text: StrCast(this.selectedPinOrRoute?.map) || StrCast(this.Document.map) || 'map location', + config_latitude: NumCast((existingPin ?? this.selectedPinOrRoute)?.latitude ?? this.dataDoc.latitude), + config_longitude: NumCast((existingPin ?? this.selectedPinOrRoute)?.longitude ?? this.dataDoc.longitude), config_map_zoom: NumCast(this.dataDoc.map_zoom), - config_map_type: StrCast(this.dataDoc.map_type), - config_map: StrCast((existingPin ?? this.selectedPin)?.map) || StrCast(this.dataDoc.map), + // config_map_type: StrCast(this.dataDoc.map_type), + config_map: StrCast((existingPin ?? this.selectedPinOrRoute)?.map) || StrCast(this.dataDoc.map), layout_unrendered: true, - mapPin: existingPin ?? this.selectedPin, - annotationOn: this.rootDoc, + mapPin: existingPin ?? this.selectedPinOrRoute, + annotationOn: this.Document, }); if (anchor) { if (!addAsAnnotation) anchor.backgroundColor = 'transparent'; addAsAnnotation && this.addDocument(anchor); - PresBox.pinDocView(anchor, { pinDocLayout: pinProps?.pinDocLayout, pinData: { ...(pinProps?.pinData ?? {}), map: true } }, this.rootDoc); + PresBox.pinDocView(anchor, { pinDocLayout: pinProps?.pinDocLayout, pinData: { ...(pinProps?.pinData ?? {}), map: true } }, this.Document); return anchor; } - return this.rootDoc; + return this.Document; }; map_docToPinMap = new Map<Doc, any>(); @@ -531,7 +638,7 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps * Removes pin from annotations */ @action - removePushpin = (pinDoc: Doc) => this.removeMapDocument(pinDoc, this.annotationKey); + removePushpinOrRoute = (pinOrRouteDoc: Doc) => this.removeMapDocument(pinOrRouteDoc, this.annotationKey); /* * Removes pushpin from map render @@ -541,18 +648,19 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps this._bingMap.current.entities.remove(this.map_docToPinMap.get(pinDoc)); } this.map_docToPinMap.delete(pinDoc); - this.selectedPin = undefined; + this.selectedPinOrRoute = undefined; }; @action - deleteSelectedPin = undoable(() => { - if (this.selectedPin) { + deleteSelectedPinOrRoute = undoable(() => { + console.log('deleting'); + if (this.selectedPinOrRoute) { // Removes filter - Doc.setDocFilter(this.rootDoc, 'latitude', this.selectedPin.latitude, 'remove'); - Doc.setDocFilter(this.rootDoc, 'longitude', this.selectedPin.longitude, 'remove'); - Doc.setDocFilter(this.rootDoc, LinkedTo, `mapPin=${Field.toScriptString(DocCast(this.selectedPin))}`, 'remove'); + Doc.setDocFilter(this.Document, 'latitude', this.selectedPinOrRoute.latitude, 'remove'); + Doc.setDocFilter(this.Document, 'longitude', this.selectedPinOrRoute.longitude, 'remove'); + Doc.setDocFilter(this.Document, LinkedTo, `mapPin=${Field.toScriptString(DocCast(this.selectedPinOrRoute))}`, 'remove'); - this.removePushpin(this.selectedPin); + this.removePushpinOrRoute(this.selectedPinOrRoute); } MapAnchorMenu.Instance.fadeOut(true); document.removeEventListener('pointerdown', this.tryHideMapAnchorMenu, true); @@ -561,23 +669,36 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps tryHideMapAnchorMenu = (e: PointerEvent) => { let target = document.elementFromPoint(e.x, e.y); while (target) { + if (target.id === 'route-destination-searcher-listbox') return; if (target === MapAnchorMenu.top.current) return; target = target.parentElement; } e.stopPropagation(); e.preventDefault(); MapAnchorMenu.Instance.fadeOut(true); + runInAction(() => { + this.temporaryRouteSource = { + type: 'FeatureCollection', + features: [], + }; + }); + document.removeEventListener('pointerdown', this.tryHideMapAnchorMenu, true); }; @action centerOnSelectedPin = () => { - if (this.selectedPin) { - this.dataDoc.latitude = this.selectedPin.latitude; - this.dataDoc.longitude = this.selectedPin.longitude; - this.dataDoc.map = this.selectedPin.map ?? ''; - this.bingSearchBarContents = this.selectedPin.map; + if (this.selectedPinOrRoute) { + this._mapRef.current?.flyTo({ + center: [NumCast(this.selectedPinOrRoute.longitude), NumCast(this.selectedPinOrRoute.latitude)], + }); } + // if (this.selectedPin) { + // this.dataDoc.latitude = this.selectedPin.latitude; + // this.dataDoc.longitude = this.selectedPin.longitude; + // this.dataDoc.map = this.selectedPin.map ?? ''; + // this.bingSearchBarContents = this.selectedPin.map; + // } MapAnchorMenu.Instance.fadeOut(true); document.removeEventListener('pointerdown', this.tryHideMapAnchorMenu); }; @@ -609,16 +730,13 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps }, }; - @action - 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._bingMap.current.entities.push(newpin); - this.map_docToPinMap.set(pin, newpin); + // 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._bingMap.current.entities.push(newpin); + // this.map_docToPinMap.set(pin, newpin); }; /* @@ -638,7 +756,7 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps this.MicrosoftMaps.Events.addHandler(this._bingMap.current, 'maptypechanged', undoable(this.updateMapType, 'Map ViewType Change')); this._disposers.mapLocation = reaction( - () => this.rootDoc.map, + () => this.Document.map, mapLoc => (this.bingSearchBarContents = mapLoc), { fireImmediately: true } ); @@ -663,7 +781,7 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps ); this._disposers.location = reaction( - () => ({ lat: this.rootDoc.latitude, lng: this.rootDoc.longitude, zoom: this.rootDoc.map_zoom, mapType: this.rootDoc.map_type }), + () => ({ lat: this.Document.latitude, lng: this.Document.longitude, zoom: this.Document.map_zoom, mapType: this.Document.map_type }), locationObject => { // if (this._bingMap.current) try { @@ -688,24 +806,26 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps e, e, e => { + // move event if (!dragClone) { - dragClone = this._dragRef.current?.cloneNode(true) as HTMLDivElement; + dragClone = this._dragRef.current?.cloneNode(true) as HTMLDivElement; // copy draggable pin dragClone.style.position = 'absolute'; dragClone.style.zIndex = '10000'; - DragManager.Root().appendChild(dragClone); + DragManager.Root().appendChild(dragClone); // add clone to root } dragClone.style.transform = `translate(${e.clientX - 15}px, ${e.clientY - 15}px)`; return false; }, e => { + // up event if (!dragClone) return; DragManager.Root().removeChild(dragClone); - let target = document.elementFromPoint(e.x, e.y); + let target = document.elementFromPoint(e.x, e.y); // element for specified x and y coordinates while (target) { if (target === this._ref.current) { - const cpt = this.props.ScreenToLocalTransform().transformPoint(e.clientX, e.clientY); - const x = cpt[0] - (this.props.PanelWidth() - this.sidebarWidth()) / 2; - const y = cpt[1] - 32 /* height of search bar */ - this.props.PanelHeight() / 2; + const cpt = this.ScreenToLocalBoxXf().transformPoint(e.clientX, e.clientY); + const x = cpt[0] - (this._props.PanelWidth() - this.sidebarWidth()) / 2; + const y = cpt[1] - 20 /* height of search bar */ - this._props.PanelHeight() / 2; const location = this._bingMap.current.tryPixelToLocation(new this.MicrosoftMaps.Point(x, y)); this.createPushpin(location.latitude, location.longitude); break; @@ -714,7 +834,7 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps } }, e => { - const createPin = () => this.createPushpin(this.rootDoc.latitude, this.rootDoc.longitude, this.rootDoc.map); + const createPin = () => this.createPushpin(this.Document.latitude, this.Document.longitude, this.Document.map); if (this.bingSearchBarContents) { this.bingSearch().then(createPin); } else createPin(); @@ -722,15 +842,703 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps ); }; + // incrementer: number = 0; + /* + * Creates Pushpin doc and adds it to the list of annotations + */ + @action + createPushpin = undoable((latitude: number, longitude: number, location?: string, wikiData?: string) => { + // Stores the pushpin as a MapMarkerDocument + const pushpin = Docs.Create.PushpinDocument( + NumCast(latitude), + NumCast(longitude), + false, + [], + { + title: location ?? `lat=${NumCast(latitude)},lng=${NumCast(longitude)}`, + map: location, + description: '', + wikiData: wikiData, + markerType: 'MAP_PIN', + markerColor: '#ff5722', + } + // { title: map ?? `lat=${latitude},lng=${longitude}`, map: map }, + // ,'pushpinIDamongus'+ this.incrementer++ + ); + this.addDocument(pushpin, this.annotationKey); + console.log(pushpin); + return pushpin; + + // mapMarker.infoWindowOpen = true; + }, 'createpin'); + + @action + createMapRoute = undoable((coordinates: Position[], originName: string, destination: any, createPinForDestination: boolean) => { + if (originName !== destination.place_name) { + const mapRoute = Docs.Create.MapRouteDocument(false, [], { title: `${originName} --> ${destination.place_name}`, routeCoordinates: JSON.stringify(coordinates) }); + this.addDocument(mapRoute, this.annotationKey); + if (createPinForDestination) { + this.createPushpin(destination.center[1], destination.center[0], destination.place_name); + } + this.temporaryRouteSource = { + type: 'FeatureCollection', + features: [], + }; + MapAnchorMenu.Instance.fadeOut(true); + return mapRoute; + } + // TODO: Display error that can't create route to same location + }, 'createmaproute'); + searchbarKeyDown = (e: any) => e.key === 'Enter' && this.bingSearch(); + @observable + featuresFromGeocodeResults: any[] = []; + + @action + addMarkerForFeature = (feature: any) => { + const location = feature.place_name; + if (feature.center) { + const longitude = feature.center[0]; + const latitude = feature.center[1]; + const wikiData = feature.properties?.wikiData; + + this.createPushpin(latitude, longitude, location, wikiData); + + if (this._mapRef.current) { + this._mapRef.current.flyTo({ + center: feature.center, + }); + } + this.featuresFromGeocodeResults = []; + } else { + // TODO: handle error + } + }; + + /** + * Makes a forward geocoding API call to Mapbox to retrieve locations based on the search input + * @param searchText the search input (presumably a location) + */ + handleSearchChange = async (searchText: string) => { + const features = await MapboxApiUtility.forwardGeocodeForFeatures(searchText); + if (features && !this.isAnimating) { + runInAction(() => { + this.settingsOpen = false; + this.featuresFromGeocodeResults = features; + this.routeToAnimate = undefined; + }); + } + // try { + // 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(); + // runInAction(() => { + // this.featuresFromGeocodeResults = data.features; + // }) + // } catch (error: any){ + // // TODO: handle error in better way + // console.log(error); + // } + }; + // @action + // debouncedCall = React.useCallback(debounce(this.debouncedOnSearchBarChange, 300), []); + + @action + handleMapClick = (e: MapLayerMouseEvent) => { + this.featuresFromGeocodeResults = []; + this.settingsOpen = false; + if (this._mapRef.current) { + const features = this._mapRef.current.queryRenderedFeatures(e.point, { + layers: ['map-routes-layer'], + }); + + 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); + this.deselectPinOrRoute(); // TODO: Also deselect route if selected + if (routeDoc) { + this.selectedPinOrRoute = routeDoc; + Doc.setDocFilter(this.Document, LinkedTo, `mapRoute=${Field.toScriptString(this.selectedPinOrRoute)}`, 'check'); + + // TODO: Recolor route + + MapAnchorMenu.Instance.Delete = this.deleteSelectedPinOrRoute; + MapAnchorMenu.Instance.Center = this.centerOnSelectedPin; + MapAnchorMenu.Instance.OnClick = this.createNoteAnnotation; + MapAnchorMenu.Instance.StartDrag = this.startAnchorDrag; + + MapAnchorMenu.Instance.Reset(); + + MapAnchorMenu.Instance.setRouteDoc(routeDoc); + + // TODO: Subject to change + MapAnchorMenu.Instance.setAllMapboxPins(this.allAnnotations.filter(anno => !anno.layout_unrendered)); + + MapAnchorMenu.Instance.DisplayRoute = this.displayRoute; + MapAnchorMenu.Instance.AddNewRouteToMap = this.createMapRoute; + MapAnchorMenu.Instance.CreatePin = this.addMarkerForFeature; + MapAnchorMenu.Instance.OpenAnimationPanel = this.openAnimationPanel; + + // this.selectedRouteCoordinates = geometry.coordinates; + + MapAnchorMenu.Instance.setMenuType('route'); + + MapAnchorMenu.Instance.jumpTo(e.originalEvent.clientX, e.originalEvent.clientY, true); + + document.addEventListener('pointerdown', this.tryHideMapAnchorMenu, true); + } + } + } + }; + + /** + * Makes a reverse geocoding API call to retrieve features corresponding to a map click (based on longitude + * and latitude). Sets the search results accordingly. + * @param e + */ + handleMapDblClick = async (e: MapLayerMouseEvent) => { + e.preventDefault(); + const lngLat: LngLat = e.lngLat; + const longitude: number = lngLat.lng; + const latitude: number = lngLat.lat; + + const features = await MapboxApiUtility.reverseGeocodeForFeatures(longitude, latitude); + if (features) { + runInAction(() => { + this.featuresFromGeocodeResults = features; + }); + } + + // // REVERSE GEOCODE TO GET LOCATION DETAILS + // try { + // 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(); + // console.log("REV GEOCODE DATA: ", data); + // runInAction(() => { + // this.featuresFromGeocodeResults = data.features; + // }) + // } catch (error: any){ + // // TODO: handle error in better way + // console.log(error); + // } + }; + + @observable + currentPopup: PopupInfo | undefined = undefined; + + @action + handleMarkerClick = (e: MarkerEvent<mapboxgl.Marker, MouseEvent>, pinDoc: Doc) => { + this.featuresFromGeocodeResults = []; + this.deselectPinOrRoute(); // TODO: check this method + this.selectedPinOrRoute = pinDoc; + // this.bingSearchBarContents = pinDoc.map; + + // Doc.setDocFilter(this.Document, 'latitude', this.selectedPin.latitude, 'match'); + // Doc.setDocFilter(this.Document, 'longitude', this.selectedPin.longitude, 'match'); + Doc.setDocFilter(this.Document, LinkedTo, `mapPin=${Field.toScriptString(this.selectedPinOrRoute)}`, 'check'); + + this.recolorPin(this.selectedPinOrRoute, 'green'); // TODO: check this method + + MapAnchorMenu.Instance.Delete = this.deleteSelectedPinOrRoute; + MapAnchorMenu.Instance.Center = this.centerOnSelectedPin; + MapAnchorMenu.Instance.OnClick = this.createNoteAnnotation; + MapAnchorMenu.Instance.StartDrag = this.startAnchorDrag; + + MapAnchorMenu.Instance.Reset(); + + // pass in the pinDoc + MapAnchorMenu.Instance.setPinDoc(pinDoc); + MapAnchorMenu.Instance.setAllMapboxPins(this.allAnnotations.filter(anno => !anno.layout_unrendered)); + + MapAnchorMenu.Instance.DisplayRoute = this.displayRoute; + MapAnchorMenu.Instance.AddNewRouteToMap = this.createMapRoute; + MapAnchorMenu.Instance.CreatePin = this.addMarkerForFeature; + + MapAnchorMenu.Instance.setMenuType('standard'); + + // MapAnchorMenu.Instance.jumpTo(NumCast(pinDoc.longitude), NumCast(pinDoc.latitude)-3, true); + + MapAnchorMenu.Instance.jumpTo(e.originalEvent.clientX, e.originalEvent.clientY, true); + + document.addEventListener('pointerdown', this.tryHideMapAnchorMenu, true); + + // this._mapRef.current.flyTo({ + // center: [NumCast(pinDoc.longitude), NumCast(pinDoc.latitude)-3] + // }) + }; + + @observable + temporaryRouteSource: FeatureCollection = { + type: 'FeatureCollection', + features: [], + }; + + @action + displayRoute = (routeInfoMap: Record<TransportationType, any> | undefined, type: TransportationType) => { + if (routeInfoMap) { + const newTempRouteSource: FeatureCollection = { + type: 'FeatureCollection', + features: [ + { + type: 'Feature', + properties: {}, + geometry: { + type: 'LineString', + coordinates: routeInfoMap[type].coordinates, + }, + }, + ], + }; + // TODO: Create pin for destination + // TODO: Fly to point where full route will be shown + this.temporaryRouteSource = newTempRouteSource; + } + }; + + @observable + isAnimating: boolean = false; + + @observable + routeToAnimate: Doc | undefined = undefined; + + @observable + animationPhase: number = 0; + + @observable + finishedFlyTo: boolean = false; + + @action + setAnimationPhase = (newValue: number) => { + this.animationPhase = newValue; + }; + + @observable + frameId: number | null = null; + + @action + setFrameId = (frameId: number) => { + this.frameId = frameId; + }; + + @observable + animationUtility: AnimationUtility | null = null; + + @action + setAnimationUtility = (util: AnimationUtility) => { + this.animationUtility = util; + }; + + @action + openAnimationPanel = (routeDoc: Doc | undefined) => { + if (routeDoc) { + MapAnchorMenu.Instance.fadeOut(true); + document.removeEventListener('pointerdown', this.tryHideMapAnchorMenu, true); + this.featuresFromGeocodeResults = []; + this.routeToAnimate = routeDoc; + } + }; + + @computed get mapboxMapViewState(): ViewState { + return { + zoom: NumCast(this.dataDoc.map_zoom, 8), + longitude: NumCast(this.dataDoc.longitude, -71.4128), + latitude: NumCast(this.dataDoc.latitude, 41.824), + pitch: NumCast(this.dataDoc.map_pitch), + bearing: NumCast(this.dataDoc.map_bearing), + padding: { + top: 0, + bottom: 0, + left: 0, + right: 0, + }, + }; + } + + @computed + get preAnimationViewState() { + if (!this.isAnimating) { + return this.mapboxMapViewState; + } + } + + @observable + isStreetViewAnimation: boolean = false; + + @observable + animationSpeed: AnimationSpeed = AnimationSpeed.MEDIUM; + + @observable + animationLineColor: string = '#ffff00'; + + @action + setAnimationLineColor = (color: ColorResult) => { + this.animationLineColor = color.hex; + }; + + @action + updateAnimationSpeed = () => { + let newAnimationSpeed: AnimationSpeed; + switch (this.animationSpeed) { + case AnimationSpeed.SLOW: + newAnimationSpeed = AnimationSpeed.MEDIUM; + break; + case AnimationSpeed.MEDIUM: + newAnimationSpeed = AnimationSpeed.FAST; + break; + case AnimationSpeed.FAST: + newAnimationSpeed = AnimationSpeed.SLOW; + break; + default: + newAnimationSpeed = AnimationSpeed.MEDIUM; + break; + } + this.animationSpeed = newAnimationSpeed; + if (this.animationUtility) { + this.animationUtility.updateAnimationSpeed(newAnimationSpeed); + } + }; + @computed get animationSpeedTooltipText(): string { + switch (this.animationSpeed) { + case AnimationSpeed.SLOW: + return '1x speed'; + case AnimationSpeed.MEDIUM: + return '2x speed'; + case AnimationSpeed.FAST: + return '3x speed'; + default: + return '2x speed'; + } + } + @computed get animationSpeedIcon(): JSX.Element { + switch (this.animationSpeed) { + case AnimationSpeed.SLOW: + return slowSpeedIcon; + case AnimationSpeed.MEDIUM: + return mediumSpeedIcon; + case AnimationSpeed.FAST: + return fastSpeedIcon; + default: + return mediumSpeedIcon; + } + } + + @action + toggleIsStreetViewAnimation = () => { + const newVal = !this.isStreetViewAnimation; + this.isStreetViewAnimation = newVal; + if (this.animationUtility) { + this.animationUtility.updateIsStreetViewAnimation(newVal); + } + }; + + @observable + dynamicRouteFeature: Feature<Geometry, GeoJsonProperties> = { + type: 'Feature', + properties: {}, + geometry: { + type: 'LineString', + coordinates: [], + }, + }; + + @observable + path: turf.helpers.Feature<turf.helpers.LineString, turf.helpers.Properties> = { + type: 'Feature', + geometry: { + type: 'LineString', + coordinates: [], + }, + properties: {}, + }; + + getFeatureFromRouteDoc = (routeDoc: Doc): Feature<Geometry, GeoJsonProperties> => { + const geometry: LineString = { + type: 'LineString', + coordinates: JSON.parse(StrCast(routeDoc.routeCoordinates)), + }; + return { + type: 'Feature', + properties: { + routeTitle: routeDoc.title, + }, + geometry: geometry, + }; + }; + + @action + playAnimation = (status: AnimationStatus) => { + if (!this._mapRef.current || !this.routeToAnimate) { + return; + } + + this.animationPhase = status === AnimationStatus.RESUME ? this.animationPhase : 0; + this.frameId = AnimationStatus.RESUME ? this.frameId : null; + this.finishedFlyTo = AnimationStatus.RESUME ? this.finishedFlyTo : false; + + const path = turf.lineString(this.selectedRouteCoordinates); + + this.settingsOpen = false; + 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); + }); + + 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); + } + + 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); + + const bbox2d: LngLatBoundsLike = [bbox3d[0], bbox3d[1], bbox3d[2], bbox3d[3]]; + + this._mapRef.current!.fitBounds(bbox2d, { + duration: 3000, + pitch: 30, + bearing: 0, + padding: 120, + }); + + setTimeout(() => { + this.isStreetViewAnimation = false; + resolve(); + }, 10000); + }); + }); + }; + + @action + pauseAnimation = () => { + if (this.frameId && this.animationPhase > 0) { + window.cancelAnimationFrame(this.frameId); + this.frameId = null; + this.isAnimating = false; + } + }; + + @action + stopAnimation = (close: boolean) => { + if (this.frameId) { + window.cancelAnimationFrame(this.frameId); + } + this.animationPhase = 0; + this.frameId = null; + this.finishedFlyTo = false; + this.isAnimating = false; + if (close) { + this.animationSpeed = AnimationSpeed.MEDIUM; + this.isStreetViewAnimation = false; + this.routeToAnimate = undefined; + this.animationUtility = null; + } + }; + + getRouteAnimationOptions = (): JSX.Element => { + return ( + <> + <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="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> + </> + </> + ); + }; + + @action + hideRoute = () => { + this.temporaryRouteSource = { + type: 'FeatureCollection', + features: [], + }; + }; + + @observable + settingsOpen: boolean = false; + + @observable + mapStyle: string = 'mapbox://styles/mapbox/standard'; + + @observable + showTerrain: boolean = true; + + @action + toggleSettings = () => { + if (!this.isAnimating && this.animationPhase == 0) { + this.featuresFromGeocodeResults = []; + this.settingsOpen = !this.settingsOpen; + } + }; + + @action + changeMapStyle = (e: React.ChangeEvent<HTMLSelectElement>) => { + this.dataDoc.map_style = e.target.value; + // this.mapStyle = `mapbox://styles/mapbox/${e.target.value}` + }; + + @action + onBearingChange = (e: React.ChangeEvent<HTMLInputElement>) => { + const bearing = parseInt(e.target.value); + if (!isNaN(bearing) && this._mapRef.current) { + console.log('bearing change'); + const fixedBearing = Math.max(0, Math.min(360, bearing)); + this._mapRef.current.setBearing(fixedBearing); + this.dataDoc.map_bearing = fixedBearing; + } + }; + + @action + onPitchChange = (e: React.ChangeEvent<HTMLInputElement>) => { + const pitch = parseInt(e.target.value); + if (!isNaN(pitch) && this._mapRef.current) { + console.log('pitch change'); + const fixedPitch = Math.max(0, Math.min(85, pitch)); + this._mapRef.current.setPitch(fixedPitch); + this.dataDoc.map_pitch = fixedPitch; + } + }; + + @action + onZoomChange = (e: React.ChangeEvent<HTMLInputElement>) => { + const zoom = parseInt(e.target.value); + if (!isNaN(zoom) && this._mapRef.current) { + const fixedZoom = Math.max(0, Math.min(16, zoom)); + this._mapRef.current.setZoom(fixedZoom); + this.dataDoc.map_zoom = fixedZoom; + } + }; + + @action + onStepZoomChange = (increment: boolean) => { + if (this._mapRef.current) { + let newZoom: number; + if (increment) { + console.log('inc'); + newZoom = Math.min(16, this.mapboxMapViewState.zoom + 1); + } else { + console.log('dec'); + newZoom = Math.max(0, this.mapboxMapViewState.zoom - 1); + } + this._mapRef.current.setZoom(newZoom); + this.dataDoc.map_zoom = newZoom; + } + }; + + @action + onMapZoom = (e: ViewStateChangeEvent) => (this.dataDoc.map_zoom = e.viewState.zoom); + + @action + onMapMove = (e: ViewStateChangeEvent) => { + this.dataDoc.longitude = e.viewState.longitude; + this.dataDoc.latitude = e.viewState.latitude; + }; + + @action + toggleShowTerrain = () => (this.showTerrain = !this.showTerrain); + + getMarkerIcon = (pinDoc: Doc): JSX.Element | null => { + const markerType = StrCast(pinDoc.markerType); + const markerColor = StrCast(pinDoc.markerColor); + + return MarkerIcons.getFontAwesomeIcon(markerType, '2x', markerColor) ?? null; + }; + static _firstRender = true; static _rerenderDelay = 500; _rerenderTimeout: any; render() { // bcz: no idea what's going on here, but bings maps have some kind of bug // such that we need to delay rendering a second map on startup until the first map is rendered. - this.rootDoc[DocCss]; + this.Document[DocCss]; if (MapBox._rerenderDelay) { // prettier-ignore this._rerenderTimeout = this._rerenderTimeout ?? @@ -739,10 +1547,12 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps MapBox._rerenderDelay = 0; } this._rerenderTimeout = undefined; - this.rootDoc[DocCss] = this.rootDoc[DocCss] + 1; + this.Document[DocCss] = this.Document[DocCss] + 1; }), MapBox._rerenderDelay); return null; } + const scale = this._props.NativeDimScaling?.() || 1; + const parscale = scale === 1 ? 1 : this.ScreenToLocalBoxXf().Scale ?? 1; const renderAnnotations = (childFilters?: () => string[]) => null; return ( @@ -753,110 +1563,188 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps onPointerDown={async e => { e.button === 0 && !e.ctrlKey && e.stopPropagation(); }} - style={{ width: `calc(100% - ${this.sidebarWidthPercent})`, pointerEvents: this.pointerEvents() }}> + 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.GetIsDragging() ? null : renderAnnotations()} - - <div className="mapBox-searchbar"> - <EditableText - // editing - setVal={(newText: string | number) => typeof newText === 'string' && this.searchbarOnEdit(newText)} - onEnter={e => this.bingSearch()} - placeholder={this.bingSearchBarContents || 'enter city/zip/...'} - textAlign="center" - /> - <IconButton - icon={ - <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> - </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'} />} /> + {SnappingManager.IsDragging ? null : renderAnnotations()} + {!this.routeToAnimate && ( + <div className="mapBox-searchbar" style={{ width: `${100 / scale}%`, zIndex: 1, position: 'relative', background: 'lightGray' }}> + <TextField fullWidth placeholder="Enter a location" onChange={(e: any) => this.handleSearchChange(e.target.value)} /> + <IconButton icon={<FontAwesomeIcon icon={faGear as IconLookup} size="1x" />} type={Type.TERT} onClick={e => this.toggleSettings()} /> + </div> + )} + {this.settingsOpen && !this.routeToAnimate && ( + <div className="mapbox-settings-panel" style={{ right: `${0 + this.sidebarWidth()}px` }}> + <div className="mapbox-style-select"> + <div>Map Style:</div> + <div> + <select onChange={this.changeMapStyle} value={StrCast(this.dataDoc.map_style)}> + <option value="mapbox://styles/mapbox/standard">Standard</option> + <option value="mapbox://styles/mapbox/streets-v11">Streets</option> + <option value="mapbox://styles/mapbox/outdoors-v12">Outdoors</option> + <option value="mapbox://styles/mapbox/light-v11">Light</option> + <option value="mapbox://styles/mapbox/dark-v11">Dark</option> + <option value="mapbox://styles/mapbox/satellite-v9">Satellite</option> + <option value="mapbox://styles/mapbox/satellite-streets-v12">Satellite Streets</option> + <option value="mapbox://styles/mapbox/navigation-day-v1">Navigation Day</option> + <option value="mapbox://styles/mapbox/navigation-night-v1">Navigation Night</option> + </select> + </div> + </div> + <div className="mapbox-bearing-selection"> + <div>Bearing: </div> + <input value={NumCast(this.mapboxMapViewState.bearing).toFixed(0)} type="number" onChange={this.onBearingChange} /> + </div> + <div className="mapbox-pitch-selection"> + <div>Pitch: </div> + <input value={NumCast(this.mapboxMapViewState.pitch).toFixed(0)} type="number" onChange={this.onPitchChange} /> + </div> + <div className="mapbox-pitch-selection"> + <div>Zoom: </div> + <input value={NumCast(this.mapboxMapViewState.zoom).toFixed(0)} type="number" onChange={this.onZoomChange} /> + </div> + <div className="mapbox-terrain-selection"> + <div>Show terrain: </div> + <input type="checkbox" checked={this.showTerrain} onChange={this.toggleShowTerrain} /> + </div> + </div> + )} + {this.routeToAnimate && ( + <div className="animation-panel" style={{ width: this.sidebarWidth() === 0 ? '100%' : `calc(100% - ${this.sidebarWidth()}px)` }}> + <div id="route-to-animate-title">{StrCast(this.routeToAnimate.title)}</div> + <div className="route-animation-options">{this.getRouteAnimationOptions()}</div> + </div> + )} + {this.featuresFromGeocodeResults.length > 0 && ( + <div className="mapbox-geocoding-search-results"> + <React.Fragment> + <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> + ))} + </React.Fragment> </div> - </div> - - <BingMapsReact - onMapReady={this.bingMapReady} // - bingMapsKey={bingApiKey} - height="100%" - mapOptions={this.bingMapOptions} - width="100%" - viewOptions={this.bingViewOptions} - /> - <div> - {!this._mapReady - ? null - : this.allAnnotations - .filter(anno => !anno.layout_unrendered) - .map((pushpin, i) => ( - <DocumentView - key={i} - {...this.props} - renderDepth={this.props.renderDepth + 1} - Document={pushpin} - DataDoc={undefined} - PanelWidth={returnOne} - PanelHeight={returnOne} - NativeWidth={returnOne} - NativeHeight={returnOne} - onKey={undefined} - onDoubleClick={undefined} - onBrowseClick={undefined} - childFilters={returnEmptyFilter} - childFiltersByRanges={returnEmptyFilter} - searchFilterDocs={returnEmptyDoclist} - isDocumentActive={returnFalse} - isContentActive={returnFalse} - addDocTab={returnFalse} - ScreenToLocalTransform={Transform.Identity} - fitContentsToBox={undefined} - focus={returnOne} - /> - ))} - </div> - {/* <MapBoxInfoWindow - key={Docs.Create.MapMarkerDocument(NumCast(40), NumCast(40), false, [], {})[Id]} - {...OmitKeys(this.props, ['NativeWidth', 'NativeHeight', 'setContentView']).omit} - place={Docs.Create.MapMarkerDocument(NumCast(40), NumCast(40), false, [], {})} - markerMap={this.markerMap} - PanelWidth={this.infoWidth} - PanelHeight={this.infoHeight} - moveDocument={this.moveDocument} - isAnyChildContentActive={this.isAnyChildContentActive} - whenChildContentsActiveChanged={this.whenChildContentsActiveChanged} - /> */} - - {!this._marqueeing || !this._mainCont.current || !this._annotationLayer.current ? null : ( - <MarqueeAnnotator - rootDoc={this.rootDoc} - anchorMenuClick={this.anchorMenuClick} - scrollTop={0} - down={this._marqueeing} - scaling={returnOne} - addDocument={this.addDocumentWrapper} - docView={this.props.docViewPath().lastElement()} - finishMarquee={this.finishMarquee} - savedAnnotations={this.savedAnnotations} - annotationLayer={this._annotationLayer.current} - selectionText={returnEmptyString} - mainCont={this._mainCont.current} - /> )} + <MapProvider> + <MapboxMap + ref={this._mapRef} + mapboxAccessToken={MAPBOX_ACCESS_TOKEN} + viewState={this.isAnimating || this.routeToAnimate ? undefined : { ...this.mapboxMapViewState, width: NumCast(this.layoutDoc._width), height: NumCast(this.layoutDoc._height) }} + mapStyle={this.dataDoc.map_style ? StrCast(this.dataDoc.map_style) : 'mapbox://styles/mapbox/streets-v11'} + style={{ + position: 'absolute', + top: 0, + left: 0, + zIndex: '0', + width: NumCast(this.layoutDoc._width) * parscale, + height: NumCast(this.layoutDoc._height) * parscale, + }} + initialViewState={this.isAnimating ? undefined : this.mapboxMapViewState} + onZoom={this.onMapZoom} + onMove={this.onMapMove} + onClick={this.handleMapClick} + onDblClick={this.handleMapDblClick} + terrain={this.showTerrain ? { source: 'mapbox-dem', exaggeration: 2.0 } : undefined}> + <Source id="mapbox-dem" type="raster-dem" url="mapbox://mapbox.mapbox-terrain-dem-v1" tileSize={512} maxzoom={14} /> + <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 && <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) && ( + <> + {!this.isStreetViewAnimation && ( + <> + <Source id="animated-route" type="geojson" data={this.updatedRouteCoordinates} /> + <Layer + id="dynamic-animation-line" + type="line" + source="animated-route" + paint={{ + 'line-color': this.animationLineColor, + 'line-width': 5, + }} + /> + </> + )} + <Source id="start-pin-base" type="geojson" data={AnimationUtility.createGeoJSONCircle(this.selectedRouteCoordinates[0], 0.04)} /> + <Source id="start-pin-top" type="geojson" data={AnimationUtility.createGeoJSONCircle(this.selectedRouteCoordinates[0], 0.25)} /> + <Source id="end-pin-base" type="geojson" data={AnimationUtility.createGeoJSONCircle(this.selectedRouteCoordinates.slice(-1)[0], 0.04)} /> + <Source id="end-pin-top" type="geojson" data={AnimationUtility.createGeoJSONCircle(this.selectedRouteCoordinates.slice(-1)[0], 0.25)} /> + <Layer + id="start-fill-pin-base" + type="fill-extrusion" + source="start-pin-base" + paint={{ + 'fill-extrusion-color': '#0bfc03', + 'fill-extrusion-height': 1000, + }} + /> + <Layer + id="start-fill-pin-top" + type="fill-extrusion" + source="start-pin-top" + paint={{ + 'fill-extrusion-color': '#0bfc03', + 'fill-extrusion-base': 1000, + 'fill-extrusion-height': 1200, + }} + /> + <Layer + id="end-fill-pin-base" + type="fill-extrusion" + source="end-pin-base" + paint={{ + 'fill-extrusion-color': '#eb1c1c', + 'fill-extrusion-height': 1000, + }} + /> + <Layer + id="end-fill-pin-top" + type="fill-extrusion" + source="end-pin-top" + paint={{ + 'fill-extrusion-color': '#eb1c1c', + 'fill-extrusion-base': 1000, + 'fill-extrusion-height': 1200, + }} + /> + </> + )} + + <> + {!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.mapMarkers.length > 0 && this.mapMarkers.map((marker, idx) => ( + <Marker key={idx} longitude={marker.longitude} latitude={marker.latitude}/> + ))} */} + </MapboxMap> + </MapProvider> </div> - {/* </LoadScript > */} <div className="mapBox-sidebar" style={{ width: `${this.sidebarWidthPercent}`, backgroundColor: `${this.sidebarColor}` }}> <SidebarAnnos ref={this._sidebarRef} - {...this.props} + {...this._props} fieldKey={this.fieldKey} - rootDoc={this.rootDoc} + Document={this.Document} layoutDoc={this.layoutDoc} dataDoc={this.dataDoc} usePanelWidth={true} diff --git a/src/client/views/nodes/MapBox/MapBox2.tsx b/src/client/views/nodes/MapBox/MapBox2.tsx index 407a91dd0..9825824bd 100644 --- a/src/client/views/nodes/MapBox/MapBox2.tsx +++ b/src/client/views/nodes/MapBox/MapBox2.tsx @@ -1,642 +1,597 @@ -import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { Autocomplete, GoogleMap, GoogleMapProps, Marker } from '@react-google-maps/api'; -import { action, computed, IReactionDisposer, observable, ObservableMap, runInAction } from 'mobx'; -import { observer } from 'mobx-react'; -import * as React from 'react'; -import { Doc, DocListCast, Opt } from '../../../../fields/Doc'; -import { Width } from '../../../../fields/DocSymbols'; -import { Id } from '../../../../fields/FieldSymbols'; -import { InkTool } from '../../../../fields/InkField'; -import { NumCast, StrCast } from '../../../../fields/Types'; -import { emptyFunction, returnEmptyString, returnFalse, returnOne, setupMoveUpEvents, Utils } from '../../../../Utils'; -import { Docs } from '../../../documents/Documents'; -import { DragManager } from '../../../util/DragManager'; -import { SnappingManager } from '../../../util/SnappingManager'; -import { UndoManager } from '../../../util/UndoManager'; -import { MarqueeOptionsMenu } from '../../collections/collectionFreeForm'; -import { ViewBoxAnnotatableComponent, ViewBoxAnnotatableProps } from '../../DocComponent'; -import { Colors } from '../../global/globalEnums'; -import { MarqueeAnnotator } from '../../MarqueeAnnotator'; -import { AnchorMenu } from '../../pdf/AnchorMenu'; -import { Annotation } from '../../pdf/Annotation'; -import { SidebarAnnos } from '../../SidebarAnnos'; -import { FieldView, FieldViewProps } from '../FieldView'; -import { PinProps } from '../trails'; -import './MapBox2.scss'; -import { MapBoxInfoWindow } from './MapBoxInfoWindow'; - -/** - * MapBox2 architecture: - * Main component: MapBox2.tsx - * Supporting Components: SidebarAnnos, CollectionStackingView - * - * MapBox2 is a node that extends the ViewBoxAnnotatableComponent. Similar to PDFBox and WebBox, it supports interaction between sidebar content and document content. - * The main body of MapBox2 uses Google Maps API to allow location retrieval, adding map markers, pan and zoom, and open street view. - * Dash Document architecture is integrated with Maps API: When drag and dropping documents with ExifData (gps Latitude and Longitude information) available, - * sidebarAddDocument function checks if the document contains lat & lng information, if it does, then the document is added to both the sidebar and the infowindow (a pop up corresponding to a map marker--pin on map). - * The lat and lng field of the document is filled when importing (spec see ConvertDMSToDD method and processFileUpload method in Documents.ts). - * A map marker is considered a document that contains a collection with stacking view of documents, it has a lat, lng location, which is passed to Maps API's custom marker (red pin) to be rendered on the google maps - */ - -// const _global = (window /* browser */ || global /* node */) as any; - -const mapContainerStyle = { - height: '100%', -}; - -const defaultCenter = { - lat: 42.360081, - lng: -71.058884, -}; - -const mapOptions = { - fullscreenControl: false, -}; - -const apiKey = process.env.GOOGLE_MAPS; - -const script = document.createElement('script'); -script.defer = true; -script.async = true; -script.src = `https://maps.googleapis.com/maps/api/js?key=${apiKey}&libraries=places,drawing`; -console.log(script.src); -document.head.appendChild(script); - -/** - * Consider integrating later: allows for drawing, circling, making shapes on map - */ -// const drawingManager = new window.google.maps.drawing.DrawingManager({ -// drawingControl: true, -// drawingControlOptions: { -// position: google.maps.ControlPosition.TOP_RIGHT, -// drawingModes: [ -// google.maps.drawing.OverlayType.MARKER, -// // currently we are not supporting the following drawing mode on map, a thought for future development -// google.maps.drawing.OverlayType.CIRCLE, -// google.maps.drawing.OverlayType.POLYLINE, -// ], -// }, -// }); - -// options for searchbox in Google Maps Places Autocomplete API -const options = { - fields: ['formatted_address', 'geometry', 'name'], // note: level of details is charged by item per retrieval, not recommended to return all fields - strictBounds: false, - types: ['establishment'], // type pf places, subject of change according to user need -} as google.maps.places.AutocompleteOptions; - -@observer -export class MapBox2 extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps & FieldViewProps & Partial<GoogleMapProps>>() { - private _dropDisposer?: DragManager.DragDropDisposer; - private _disposers: { [name: string]: IReactionDisposer } = {}; - private _annotationLayer: React.RefObject<HTMLDivElement> = React.createRef(); - @observable private _overlayAnnoInfo: Opt<Doc>; - showInfo = action((anno: Opt<Doc>) => (this._overlayAnnoInfo = anno)); - public static LayoutString(fieldKey: string) { - return FieldView.LayoutString(MapBox2, fieldKey); - } - public get SidebarKey() { - return this.fieldKey + '_sidebar'; - } - private _setPreviewCursor: undefined | ((x: number, y: number, drag: boolean, hide: boolean, doc: Opt<Doc>) => void); - @computed get inlineTextAnnotations() { - return this.allMapMarkers.filter(a => a.text_inlineAnnotations); - } - - @observable private _map: google.maps.Map = null as unknown as google.maps.Map; - @observable private selectedPlace: Doc | undefined; - @observable private markerMap: { [id: string]: google.maps.Marker } = {}; - @observable private center = navigator.geolocation ? navigator.geolocation.getCurrentPosition : defaultCenter; - @observable private _marqueeing: number[] | undefined; - @observable private _isAnnotating = false; - @observable private inputRef = React.createRef<HTMLInputElement>(); - @observable private searchMarkers: google.maps.Marker[] = []; - @observable private searchBox = new window.google.maps.places.Autocomplete(this.inputRef.current!, options); - @observable private _savedAnnotations = new ObservableMap<number, HTMLDivElement[]>(); - @computed get allSidebarDocs() { - return DocListCast(this.dataDoc[this.SidebarKey]); - } - @computed get allMapMarkers() { - return DocListCast(this.dataDoc[this.annotationKey]); - } - @observable private toggleAddMarker = false; - private _mainCont: React.RefObject<HTMLDivElement> = React.createRef(); - - @observable _showSidebar = false; - @computed get SidebarShown() { - return this._showSidebar || this.layoutDoc._layout_showSidebar ? true : false; - } - - static _canAnnotate = true; - static _hadSelection: boolean = false; - private _sidebarRef = React.createRef<SidebarAnnos>(); - private _ref: React.RefObject<HTMLDivElement> = React.createRef(); - - componentDidMount() { - this.props.setContentView?.(this); - } - - @action - private setSearchBox = (searchBox: any) => { - this.searchBox = searchBox; - }; - - // iterate allMarkers to size, center, and zoom map to contain all markers - private fitBounds = (map: google.maps.Map) => { - const curBounds = map.getBounds() ?? new window.google.maps.LatLngBounds(); - const isFitting = this.allMapMarkers.reduce((fits, place) => fits && curBounds?.contains({ lat: NumCast(place.lat), lng: NumCast(place.lng) }), true as boolean); - !isFitting && map.fitBounds(this.allMapMarkers.reduce((bounds, place) => bounds.extend({ lat: NumCast(place.lat), lng: NumCast(place.lng) }), new window.google.maps.LatLngBounds())); - }; - - /** - * Custom control for add marker button - * @param controlDiv - * @param map - */ - private CenterControl = () => { - const controlDiv = document.createElement('div'); - controlDiv.className = 'MapBox2-addMarker'; - // Set CSS for the control border. - const controlUI = document.createElement('div'); - controlUI.style.backgroundColor = '#fff'; - controlUI.style.borderRadius = '3px'; - controlUI.style.cursor = 'pointer'; - controlUI.style.marginTop = '10px'; - controlUI.style.borderRadius = '4px'; - controlUI.style.marginBottom = '22px'; - controlUI.style.textAlign = 'center'; - controlUI.style.position = 'absolute'; - controlUI.style.width = '32px'; - controlUI.style.height = '32px'; - controlUI.title = 'Click to toggle marker mode. In marker mode, click on map to place a marker.'; - - const plIcon = document.createElement('img'); - plIcon.src = 'https://cdn4.iconfinder.com/data/icons/wirecons-free-vector-icons/32/add-256.png'; - plIcon.style.color = 'rgb(25,25,25)'; - plIcon.style.fontFamily = 'Roboto,Arial,sans-serif'; - plIcon.style.fontSize = '16px'; - plIcon.style.lineHeight = '32px'; - plIcon.style.left = '18'; - plIcon.style.top = '15'; - plIcon.style.position = 'absolute'; - plIcon.width = 14; - plIcon.height = 14; - plIcon.innerHTML = 'Add'; - controlUI.appendChild(plIcon); - - // Set CSS for the control interior. - const markerIcon = document.createElement('img'); - markerIcon.src = 'https://cdn0.iconfinder.com/data/icons/small-n-flat/24/678111-map-marker-1024.png'; - markerIcon.style.color = 'rgb(25,25,25)'; - markerIcon.style.fontFamily = 'Roboto,Arial,sans-serif'; - markerIcon.style.fontSize = '16px'; - markerIcon.style.lineHeight = '32px'; - markerIcon.style.left = '-2'; - markerIcon.style.top = '1'; - markerIcon.width = 30; - markerIcon.height = 30; - markerIcon.style.position = 'absolute'; - markerIcon.innerHTML = 'Add'; - controlUI.appendChild(markerIcon); - - // Setup the click event listeners - controlUI.addEventListener('click', () => { - if (this.toggleAddMarker === true) { - this.toggleAddMarker = false; - console.log('add marker button status:' + this.toggleAddMarker); - controlUI.style.backgroundColor = '#fff'; - markerIcon.style.color = 'rgb(25,25,25)'; - } else { - this.toggleAddMarker = true; - console.log('add marker button status:' + this.toggleAddMarker); - controlUI.style.backgroundColor = '#4476f7'; - markerIcon.style.color = 'rgb(255,255,255)'; - } - }); - controlDiv.appendChild(controlUI); - return controlDiv; - }; - - /** - * Place the marker on google maps & store the empty marker as a MapMarker Document in allMarkers list - * @param position - the LatLng position where the marker is placed - * @param map - */ - @action - private placeMarker = (position: google.maps.LatLng, map: google.maps.Map) => { - const marker = new google.maps.Marker({ - position: position, - map: map, - }); - map.panTo(position); - const mapMarker = Docs.Create.PushpinDocument(NumCast(position.lat()), NumCast(position.lng()), false, [], {}); - this.addDocument(mapMarker, this.annotationKey); - }; - - _loadPending = true; - /** - * store a reference to google map instance - * setup the drawing manager on the top right corner of map - * fit map bounds to contain all markers - * @param map - */ - @action - private loadHandler = (map: google.maps.Map) => { - this._map = map; - this._loadPending = true; - const centerControlDiv = this.CenterControl(); - map.controls[google.maps.ControlPosition.TOP_RIGHT].push(centerControlDiv); - //drawingManager.setMap(map); - // if (navigator.geolocation) { - // navigator.geolocation.getCurrentPosition( - // (position: Position) => { - // const pos = { - // lat: position.coords.latitude, - // lng: position.coords.longitude, - // }; - // this._map.setCenter(pos); - // } - // ); - // } else { - // alert("Your geolocation is not supported by browser.") - // }; - map.setZoom(NumCast(this.dataDoc.map_zoom, 2.5)); - map.setCenter(new google.maps.LatLng(NumCast(this.dataDoc.mapLat), NumCast(this.dataDoc.mapLng))); - setTimeout(() => { - if (this._loadPending && this._map.getBounds()) { - this._loadPending = false; - this.layoutDoc.freeform_fitContentsToBox && this.fitBounds(this._map); - } - }, 250); - // listener to addmarker event - this._map.addListener('click', (e: MouseEvent) => { - if (this.toggleAddMarker === true) { - this.placeMarker((e as any).latLng, map); - } - }); - }; - - @action - centered = () => { - if (this._loadPending && this._map.getBounds()) { - this._loadPending = false; - this.layoutDoc.freeform_fitContentsToBox && this.fitBounds(this._map); - } - this.dataDoc.mapLat = this._map.getCenter()?.lat(); - this.dataDoc.mapLng = this._map.getCenter()?.lng(); - }; - - @action - zoomChanged = () => { - if (this._loadPending && this._map.getBounds()) { - this._loadPending = false; - this.layoutDoc.freeform_fitContentsToBox && this.fitBounds(this._map); - } - this.dataDoc.map_zoom = this._map.getZoom(); - }; - - /** - * Load and render all map markers - * @param marker - * @param place - */ - @action - private markerLoadHandler = (marker: google.maps.Marker, place: Doc) => { - place[Id] ? (this.markerMap[place[Id]] = marker) : null; - }; - - /** - * on clicking the map marker, set the selected place to the marker document & set infowindowopen to be true - * @param e - * @param place - */ - @action - private markerClickHandler = (e: google.maps.MapMouseEvent, place: Doc) => { - // set which place was clicked - this.selectedPlace = place; - place.infoWindowOpen = true; - }; - - /** - * 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 sidebarKey - * @returns - */ - sidebarAddDocument = (doc: Doc | Doc[], sidebarKey?: string) => { - console.log('print all sidebar Docs'); - if (!this.layoutDoc._layout_showSidebar) this.toggleSidebar(); - const docs = doc instanceof Doc ? [doc] : doc; - docs.forEach(doc => { - if (doc.lat !== undefined && doc.lng !== undefined) { - const existingMarker = this.allMapMarkers.find(marker => marker.lat === doc.lat && marker.lng === doc.lng); - if (existingMarker) { - Doc.AddDocToList(existingMarker, 'data', doc); - } else { - const marker = Docs.Create.PushpinDocument(NumCast(doc.lat), NumCast(doc.lng), false, [doc], {}); - this.addDocument(marker, this.annotationKey); - } - } - }); //add to annotation list - - return this.addDocument(doc, sidebarKey); // add to sidebar list - }; - - /** - * Removing documents from the sidebar - * @param doc - * @param sidebarKey - * @returns - */ - sidebarRemoveDocument = (doc: Doc | Doc[], sidebarKey?: string) => { - if (this.layoutDoc._layout_showSidebar) this.toggleSidebar(); - const docs = doc instanceof Doc ? [doc] : doc; - return this.removeDocument(doc, sidebarKey); - }; - - /** - * Toggle sidebar onclick the tiny comment button on the top right corner - * @param e - */ - sidebarBtnDown = (e: React.PointerEvent) => { - setupMoveUpEvents( - this, - e, - (e, down, delta) => - runInAction(() => { - const localDelta = this.props - .ScreenToLocalTransform() - .scale(this.props.NativeDimScaling?.() || 1) - .transformDirection(delta[0], delta[1]); - const fullWidth = this.layoutDoc[Width](); - const mapWidth = fullWidth - this.sidebarWidth(); - if (this.sidebarWidth() + localDelta[0] > 0) { - this._showSidebar = true; - this.layoutDoc._width = fullWidth + localDelta[0]; - this.layoutDoc._layout_sidebarWidthPercent = ((100 * (this.sidebarWidth() + localDelta[0])) / (fullWidth + localDelta[0])).toString() + '%'; - } else { - this._showSidebar = false; - this.layoutDoc._width = mapWidth; - this.layoutDoc._layout_sidebarWidthPercent = '0%'; - } - return false; - }), - emptyFunction, - () => UndoManager.RunInBatch(this.toggleSidebar, 'toggle sidebar map') - ); - }; - - sidebarWidth = () => (Number(this.layout_sidebarWidthPercent.substring(0, this.layout_sidebarWidthPercent.length - 1)) / 100) * this.props.PanelWidth(); - @computed get layout_sidebarWidthPercent() { - return StrCast(this.layoutDoc._layout_sidebarWidthPercent, '0%'); - } - @computed get sidebarColor() { - return StrCast(this.layoutDoc.sidebar_color, StrCast(this.layoutDoc[this.props.fieldKey + '_backgroundColor'], '#e4e4e4')); - } - - /** - * function that reads the place inputed from searchbox, then zoom in on the location that's been autocompleted; - * add a customized temporary marker on the map - */ - @action - private handlePlaceChanged = () => { - const place = this.searchBox.getPlace(); - - if (!place.geometry || !place.geometry.location) { - // user entered the name of a place that wasn't suggested & pressed the enter key, or place details request failed - window.alert("No details available for input: '" + place.name + "'"); - return; - } - - // zoom in on the location of the search result - if (place.geometry.viewport) { - this._map.fitBounds(place.geometry.viewport); - } else { - this._map.setCenter(place.geometry.location); - this._map.setZoom(17); - } - - // customize icon => customized icon for the nature of the location selected - const icon = { - url: place.icon as string, - size: new google.maps.Size(71, 71), - origin: new google.maps.Point(0, 0), - anchor: new google.maps.Point(17, 34), - scaledSize: new google.maps.Size(25, 25), - }; - - // put temporary cutomized marker on searched location - this.searchMarkers.forEach(marker => { - marker.setMap(null); - }); - this.searchMarkers = []; - this.searchMarkers.push( - new window.google.maps.Marker({ - map: this._map, - icon, - title: place.name, - position: place.geometry.location, - }) - ); - }; - - /** - * Handles toggle of sidebar on click the little comment button - */ - @computed get sidebarHandle() { - return ( - <div - className="MapBox2-overlayButton-sidebar" - key="sidebar" - title="Toggle Sidebar" - style={{ - display: !this.props.isContentActive() ? 'none' : undefined, - top: StrCast(this.rootDoc._layout_showTitle) === 'title' ? 20 : 5, - backgroundColor: this.SidebarShown ? Colors.MEDIUM_BLUE : Colors.BLACK, - }} - onPointerDown={this.sidebarBtnDown}> - <FontAwesomeIcon style={{ color: Colors.WHITE }} icon={'comment-alt'} size="sm" /> - </div> - ); - } - - // TODO: Adding highlight box layer to Maps - @action - toggleSidebar = () => { - //1.2 * w * ? = .2 * w .2/1.2 - const prevWidth = this.sidebarWidth(); - this.layoutDoc._layout_showSidebar = (this.layoutDoc._layout_sidebarWidthPercent = StrCast(this.layoutDoc._layout_sidebarWidthPercent, '0%') === '0%' ? `${(100 * 0.2) / 1.2}%` : '0%') !== '0%'; - this.layoutDoc._width = this.layoutDoc._layout_showSidebar ? NumCast(this.layoutDoc._width) * 1.2 : Math.max(20, NumCast(this.layoutDoc._width) - prevWidth); - }; - - sidebarDown = (e: React.PointerEvent) => { - setupMoveUpEvents(this, e, this.sidebarMove, emptyFunction, () => setTimeout(this.toggleSidebar), true); - }; - sidebarMove = (e: PointerEvent, down: number[], delta: number[]) => { - 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%'; - e.preventDefault(); - return false; - }; - - setPreviewCursor = (func?: (x: number, y: number, drag: boolean, hide: boolean) => void) => (this._setPreviewCursor = func); - - @action - onMarqueeDown = (e: React.PointerEvent) => { - if (!e.altKey && e.button === 0 && this.props.isContentActive(true) && ![InkTool.Highlighter, InkTool.Pen, InkTool.Write].includes(Doc.ActiveTool)) { - setupMoveUpEvents( - this, - e, - action(e => { - MarqueeAnnotator.clearAnnotations(this._savedAnnotations); - this._marqueeing = [e.clientX, e.clientY]; - return true; - }), - returnFalse, - () => MarqueeAnnotator.clearAnnotations(this._savedAnnotations), - false - ); - } - }; - @action finishMarquee = (x?: number, y?: number) => { - this._marqueeing = undefined; - this._isAnnotating = false; - x !== undefined && y !== undefined && this._setPreviewCursor?.(x, y, false, false, this.props.Document); - }; - - addDocumentWrapper = (doc: Doc | Doc[], annotationKey?: string) => { - return this.addDocument(doc, annotationKey); - }; - - pointerEvents = () => { - return this.props.isContentActive() === false ? 'none' : this.props.isContentActive() && this.props.pointerEvents?.() !== 'none' && !MarqueeOptionsMenu.Instance.isShown() ? 'all' : SnappingManager.GetIsDragging() ? undefined : 'none'; - }; - @computed get annotationLayer() { - return ( - <div className="MapBox2-annotationLayer" style={{ height: Doc.NativeHeight(this.Document) || undefined }} ref={this._annotationLayer}> - {this.inlineTextAnnotations - .sort((a, b) => NumCast(a.y) - NumCast(b.y)) - .map(anno => ( - <Annotation key={`${anno[Id]}-annotation`} {...this.props} fieldKey={this.annotationKey} pointerEvents={this.pointerEvents} showInfo={this.showInfo} dataDoc={this.dataDoc} anno={anno} /> - ))} - </div> - ); - } - - getAnchor = (addAsAnnotation: boolean, pinProps?: PinProps) => AnchorMenu.Instance?.GetAnchor(this._savedAnnotations, addAsAnnotation) ?? this.rootDoc; - - /** - * render contents in allMapMarkers (e.g. images with exifData) into google maps as map marker - * @returns - */ - private renderMarkers = () => { - return this.allMapMarkers.map(place => ( - <Marker key={place[Id]} position={{ lat: NumCast(place.lat), lng: NumCast(place.lng) }} onLoad={marker => this.markerLoadHandler(marker, place)} onClick={(e: google.maps.MapMouseEvent) => this.markerClickHandler(e, place)} /> - )); - }; - - // 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()); - // } - }; - - panelWidth = () => this.props.PanelWidth() / (this.props.NativeDimScaling?.() || 1) - this.sidebarWidth(); - panelHeight = () => this.props.PanelHeight() / (this.props.NativeDimScaling?.() || 1); - scrollXf = () => this.props.ScreenToLocalTransform().translate(0, NumCast(this.layoutDoc._layout_scrollTop)); - transparentFilter = () => [...this.props.childFilters(), Utils.IsTransparentFilter()]; - opaqueFilter = () => [...this.props.childFilters(), Utils.IsOpaqueFilter()]; - infoWidth = () => this.props.PanelWidth() / 5; - infoHeight = () => this.props.PanelHeight() / 5; - anchorMenuClick = () => this._sidebarRef.current?.anchorMenuClick; - savedAnnotations = () => this._savedAnnotations; - - get MicrosoftMaps() { - return (window as any).Microsoft.Maps; - } - render() { - const renderAnnotations = (childFilters?: () => string[]) => null; - return ( - <div className="MapBox2" ref={this._ref}> - <div - className="MapBox2-wrapper" - onWheel={e => e.stopPropagation()} - onPointerDown={async e => { - e.button === 0 && !e.ctrlKey && e.stopPropagation(); - }} - style={{ width: `calc(100% - ${this.layout_sidebarWidthPercent})`, pointerEvents: this.pointerEvents() }}> - <div style={{ mixBlendMode: 'multiply' }}>{renderAnnotations(this.transparentFilter)}</div> - {renderAnnotations(this.opaqueFilter)} - {SnappingManager.GetIsDragging() ? null : renderAnnotations()} - {this.annotationLayer} - - <div> - <GoogleMap mapContainerStyle={mapContainerStyle} onZoomChanged={this.zoomChanged} onCenterChanged={this.centered} onLoad={this.loadHandler} options={mapOptions}> - <Autocomplete onLoad={this.setSearchBox} onPlaceChanged={this.handlePlaceChanged}> - <input className="MapBox2-input" ref={this.inputRef} type="text" onKeyDown={e => e.stopPropagation()} placeholder="Enter location" /> - </Autocomplete> - - {this.renderMarkers()} - {this.allMapMarkers - .filter(marker => marker.infoWindowOpen) - .map(marker => ( - <MapBoxInfoWindow - key={marker[Id]} - {...this.props} - setContentView={emptyFunction} - place={marker} - markerMap={this.markerMap} - PanelWidth={this.infoWidth} - PanelHeight={this.infoHeight} - moveDocument={this.moveDocument} - isAnyChildContentActive={this.isAnyChildContentActive} - whenChildContentsActiveChanged={this.whenChildContentsActiveChanged} - /> - ))} - {/* {this.handleMapCenter(this._map)} */} - </GoogleMap> - </div> - {!this._marqueeing || !this._mainCont.current || !this._annotationLayer.current ? null : ( - <MarqueeAnnotator - rootDoc={this.rootDoc} - anchorMenuClick={this.anchorMenuClick} - scrollTop={0} - down={this._marqueeing} - scaling={returnOne} - addDocument={this.addDocumentWrapper} - docView={this.props.docViewPath().lastElement()} - finishMarquee={this.finishMarquee} - savedAnnotations={this.savedAnnotations} - annotationLayer={this._annotationLayer.current} - selectionText={returnEmptyString} - mainCont={this._mainCont.current} - /> - )} - </div> - {/* </LoadScript > */} - <div className="MapBox2-sidebar" style={{ width: `${this.layout_sidebarWidthPercent}`, backgroundColor: `${this.sidebarColor}` }}> - <SidebarAnnos - ref={this._sidebarRef} - {...this.props} - fieldKey={this.fieldKey} - rootDoc={this.rootDoc} - layoutDoc={this.layoutDoc} - dataDoc={this.dataDoc} - usePanelWidth={true} - showSidebar={this.SidebarShown} - nativeWidth={NumCast(this.layoutDoc._nativeWidth)} - whenChildContentsActiveChanged={this.whenChildContentsActiveChanged} - PanelWidth={this.sidebarWidth} - sidebarAddDocument={this.sidebarAddDocument} - moveDocument={this.moveDocument} - removeDocument={this.sidebarRemoveDocument} - /> - </div> - {this.sidebarHandle} - </div> - ); - } -} +// import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +// import { Autocomplete, GoogleMap, GoogleMapProps, Marker } from '@react-google-maps/api'; +// import { action, computed, IReactionDisposer, observable, ObservableMap, runInAction } from 'mobx'; +// import { observer } from 'mobx-react'; +// import * as React from 'react'; +// import { Doc, DocListCast, Opt } from '../../../../fields/Doc'; +// import { Id } from '../../../../fields/FieldSymbols'; +// import { NumCast, StrCast } from '../../../../fields/Types'; +// import { emptyFunction, setupMoveUpEvents, Utils } from '../../../../Utils'; +// import { Docs } from '../../../documents/Documents'; +// import { DragManager } from '../../../util/DragManager'; +// import { SnappingManager } from '../../../util/SnappingManager'; +// import { UndoManager } from '../../../util/UndoManager'; +// import { MarqueeOptionsMenu } from '../../collections/collectionFreeForm'; +// import { ViewBoxAnnotatableComponent } from '../../DocComponent'; +// import { Colors } from '../../global/globalEnums'; +// import { AnchorMenu } from '../../pdf/AnchorMenu'; +// import { Annotation } from '../../pdf/Annotation'; +// import { SidebarAnnos } from '../../SidebarAnnos'; +// import { FieldView, FieldViewProps } from '../FieldView'; +// import { PinProps } from '../trails'; +// import './MapBox2.scss'; +// import { MapBoxInfoWindow } from './MapBoxInfoWindow'; + +// /** +// * MapBox2 architecture: +// * Main component: MapBox2.tsx +// * Supporting Components: SidebarAnnos, CollectionStackingView +// * +// * MapBox2 is a node that extends the ViewBoxAnnotatableComponent. Similar to PDFBox and WebBox, it supports interaction between sidebar content and document content. +// * The main body of MapBox2 uses Google Maps API to allow location retrieval, adding map markers, pan and zoom, and open street view. +// * Dash Document architecture is integrated with Maps API: When drag and dropping documents with ExifData (gps Latitude and Longitude information) available, +// * sidebarAddDocument function checks if the document contains lat & lng information, if it does, then the document is added to both the sidebar and the infowindow (a pop up corresponding to a map marker--pin on map). +// * The lat and lng field of the document is filled when importing (spec see ConvertDMSToDD method and processFileUpload method in Documents.ts). +// * A map marker is considered a document that contains a collection with stacking view of documents, it has a lat, lng location, which is passed to Maps API's custom marker (red pin) to be rendered on the google maps +// */ + +// // const _global = (window /* browser */ || global /* node */) as any; + +// const mapContainerStyle = { +// height: '100%', +// }; + +// const defaultCenter = { +// lat: 42.360081, +// lng: -71.058884, +// }; + +// const mapOptions = { +// fullscreenControl: false, +// }; + +// const apiKey = process.env.GOOGLE_MAPS; + +// const script = document.createElement('script'); +// script.defer = true; +// script.async = true; +// script.src = `https://maps.googleapis.com/maps/api/js?key=${apiKey}&libraries=places,drawing`; +// console.log(script.src); +// document.head.appendChild(script); + +// /** +// * Consider integrating later: allows for drawing, circling, making shapes on map +// */ +// // const drawingManager = new window.google.maps.drawing.DrawingManager({ +// // drawingControl: true, +// // drawingControlOptions: { +// // position: google.maps.ControlPosition.TOP_RIGHT, +// // drawingModes: [ +// // google.maps.drawing.OverlayType.MARKER, +// // // currently we are not supporting the following drawing mode on map, a thought for future development +// // google.maps.drawing.OverlayType.CIRCLE, +// // google.maps.drawing.OverlayType.POLYLINE, +// // ], +// // }, +// // }); + +// // options for searchbox in Google Maps Places Autocomplete API +// const options = { +// fields: ['formatted_address', 'geometry', 'name'], // note: level of details is charged by item per retrieval, not recommended to return all fields +// strictBounds: false, +// types: ['establishment'], // type pf places, subject of change according to user need +// } as google.maps.places.AutocompleteOptions; + +// @observer +// export class MapBox2 extends ViewBoxAnnotatableComponent<FieldViewProps & Partial<GoogleMapProps>>() { +// private _dropDisposer?: DragManager.DragDropDisposer; +// private _disposers: { [name: string]: IReactionDisposer } = {}; +// private _annotationLayer: React.RefObject<HTMLDivElement> = React.createRef(); +// @observable private _overlayAnnoInfo: Opt<Doc> = undefined; +// showInfo = action((anno: Opt<Doc>) => (this._overlayAnnoInfo = anno)); +// public static LayoutString(fieldKey: string) { +// return FieldView.LayoutString(MapBox2, fieldKey); +// } +// public get SidebarKey() { +// return this.fieldKey + '_sidebar'; +// } +// private _setPreviewCursor: undefined | ((x: number, y: number, drag: boolean, hide: boolean, doc: Opt<Doc>) => void); +// @computed get inlineTextAnnotations() { +// return this.allMapMarkers.filter(a => a.text_inlineAnnotations); +// } + +// @observable private _map: google.maps.Map = null as unknown as google.maps.Map; +// @observable private selectedPlace: Doc | undefined = undefined; +// @observable private markerMap: { [id: string]: google.maps.Marker } = {}; +// @observable private center = navigator.geolocation ? navigator.geolocation.getCurrentPosition : defaultCenter; +// @observable private inputRef = React.createRef<HTMLInputElement>(); +// @observable private searchMarkers: google.maps.Marker[] = []; +// @observable private searchBox = new window.google.maps.places.Autocomplete(this.inputRef.current!, options); +// @observable private _savedAnnotations = new ObservableMap<number, HTMLDivElement[]>(); +// @computed get allSidebarDocs() { +// return DocListCast(this.dataDoc[this.SidebarKey]); +// } +// @computed get allMapMarkers() { +// return DocListCast(this.dataDoc[this.annotationKey]); +// } +// @observable private toggleAddMarker = false; + +// @observable _showSidebar = false; +// @computed get SidebarShown() { +// return this._showSidebar || this.layoutDoc._layout_showSidebar ? true : false; +// } + +// static _canAnnotate = true; +// static _hadSelection: boolean = false; +// private _sidebarRef = React.createRef<SidebarAnnos>(); +// private _ref: React.RefObject<HTMLDivElement> = React.createRef(); + +// componentDidMount() { +// this._props.setContentView?.(this); +// } + +// @action +// private setSearchBox = (searchBox: any) => { +// this.searchBox = searchBox; +// }; + +// // iterate allMarkers to size, center, and zoom map to contain all markers +// private fitBounds = (map: google.maps.Map) => { +// const curBounds = map.getBounds ?? new window.google.maps.LatLngBounds(); +// const isFitting = this.allMapMarkers.reduce((fits, place) => fits && curBounds?.contains({ lat: NumCast(place.lat), lng: NumCast(place.lng) }), true as boolean); +// !isFitting && map.fitBounds(this.allMapMarkers.reduce((bounds, place) => bounds.extend({ lat: NumCast(place.lat), lng: NumCast(place.lng) }), new window.google.maps.LatLngBounds())); +// }; + +// /** +// * Custom control for add marker button +// * @param controlDiv +// * @param map +// */ +// private CenterControl = () => { +// const controlDiv = document.createElement('div'); +// controlDiv.className = 'MapBox2-addMarker'; +// // Set CSS for the control border. +// const controlUI = document.createElement('div'); +// controlUI.style.backgroundColor = '#fff'; +// controlUI.style.borderRadius = '3px'; +// controlUI.style.cursor = 'pointer'; +// controlUI.style.marginTop = '10px'; +// controlUI.style.borderRadius = '4px'; +// controlUI.style.marginBottom = '22px'; +// controlUI.style.textAlign = 'center'; +// controlUI.style.position = 'absolute'; +// controlUI.style.width = '32px'; +// controlUI.style.height = '32px'; +// controlUI.title = 'Click to toggle marker mode. In marker mode, click on map to place a marker.'; + +// const plIcon = document.createElement('img'); +// plIcon.src = 'https://cdn4.iconfinder.com/data/icons/wirecons-free-vector-icons/32/add-256.png'; +// plIcon.style.color = 'rgb(25,25,25)'; +// plIcon.style.fontFamily = 'Roboto,Arial,sans-serif'; +// plIcon.style.fontSize = '16px'; +// plIcon.style.lineHeight = '32px'; +// plIcon.style.left = '18'; +// plIcon.style.top = '15'; +// plIcon.style.position = 'absolute'; +// plIcon.width = 14; +// plIcon.height = 14; +// plIcon.innerHTML = 'Add'; +// controlUI.appendChild(plIcon); + +// // Set CSS for the control interior. +// const markerIcon = document.createElement('img'); +// markerIcon.src = 'https://cdn0.iconfinder.com/data/icons/small-n-flat/24/678111-map-marker-1024.png'; +// markerIcon.style.color = 'rgb(25,25,25)'; +// markerIcon.style.fontFamily = 'Roboto,Arial,sans-serif'; +// markerIcon.style.fontSize = '16px'; +// markerIcon.style.lineHeight = '32px'; +// markerIcon.style.left = '-2'; +// markerIcon.style.top = '1'; +// markerIcon.width = 30; +// markerIcon.height = 30; +// markerIcon.style.position = 'absolute'; +// markerIcon.innerHTML = 'Add'; +// controlUI.appendChild(markerIcon); + +// // Setup the click event listeners +// controlUI.addEventListener('click', () => { +// if (this.toggleAddMarker === true) { +// this.toggleAddMarker = false; +// console.log('add marker button status:' + this.toggleAddMarker); +// controlUI.style.backgroundColor = '#fff'; +// markerIcon.style.color = 'rgb(25,25,25)'; +// } else { +// this.toggleAddMarker = true; +// console.log('add marker button status:' + this.toggleAddMarker); +// controlUI.style.backgroundColor = '#4476f7'; +// markerIcon.style.color = 'rgb(255,255,255)'; +// } +// }); +// controlDiv.appendChild(controlUI); +// return controlDiv; +// }; + +// /** +// * Place the marker on google maps & store the empty marker as a MapMarker Document in allMarkers list +// * @param position - the LatLng position where the marker is placed +// * @param map +// */ +// @action +// private placeMarker = (position: google.maps.LatLng, map: google.maps.Map) => { +// const marker = new google.maps.Marker({ +// position: position, +// map: map, +// }); +// map.panTo(position); +// const mapMarker = Docs.Create.PushpinDocument(NumCast(position.lat()), NumCast(position.lng()), false, [], {}); +// this.addDocument(mapMarker, this.annotationKey); +// }; + +// _loadPending = true; +// /** +// * store a reference to google map instance +// * setup the drawing manager on the top right corner of map +// * fit map bounds to contain all markers +// * @param map +// */ +// @action +// private loadHandler = (map: google.maps.Map) => { +// this._map = map; +// this._loadPending = true; +// const centerControlDiv = this.CenterControl(); +// map.controls[google.maps.ControlPosition.TOP_RIGHT].push(centerControlDiv); +// //drawingManager.setMap(map); +// // if (navigator.geolocation) { +// // navigator.geolocation.getCurrentPosition( +// // (position: Position) => { +// // const pos = { +// // lat: position.coords.latitude, +// // lng: position.coords.longitude, +// // }; +// // this._map.setCenter(pos); +// // } +// // ); +// // } else { +// // alert("Your geolocation is not supported by browser.") +// // }; +// map.setZoom(NumCast(this.dataDoc.map_zoom, 2.5)); +// map.setCenter(new google.maps.LatLng(NumCast(this.dataDoc.mapLat), NumCast(this.dataDoc.mapLng))); +// setTimeout(() => { +// if (this._loadPending && this._map.getBounds) { +// this._loadPending = false; +// this.layoutDoc.freeform_fitContentsToBox && this.fitBounds(this._map); +// } +// }, 250); +// // listener to addmarker event +// this._map.addListener('click', (e: MouseEvent) => { +// if (this.toggleAddMarker === true) { +// this.placeMarker((e as any).latLng, map); +// } +// }); +// }; + +// @action +// centered = () => { +// if (this._loadPending && this._map.getBounds) { +// this._loadPending = false; +// this.layoutDoc.freeform_fitContentsToBox && this.fitBounds(this._map); +// } +// this.dataDoc.mapLat = this._map.getCenter()?.lat(); +// this.dataDoc.mapLng = this._map.getCenter()?.lng(); +// }; + +// @action +// zoomChanged = () => { +// if (this._loadPending && this._map.getBounds) { +// this._loadPending = false; +// this.layoutDoc.freeform_fitContentsToBox && this.fitBounds(this._map); +// } +// this.dataDoc.map_zoom = this._map.getZoom(); +// }; + +// /** +// * Load and render all map markers +// * @param marker +// * @param place +// */ +// @action +// private markerLoadHandler = (marker: google.maps.Marker, place: Doc) => { +// place[Id] ? (this.markerMap[place[Id]] = marker) : null; +// }; + +// /** +// * on clicking the map marker, set the selected place to the marker document & set infowindowopen to be true +// * @param e +// * @param place +// */ +// @action +// private markerClickHandler = (e: google.maps.MapMouseEvent, place: Doc) => { +// // set which place was clicked +// this.selectedPlace = place; +// place.infoWindowOpen = true; +// }; + +// /** +// * 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 sidebarKey +// * @returns +// */ +// sidebarAddDocument = (doc: Doc | Doc[], sidebarKey?: string) => { +// console.log('print all sidebar Docs'); +// if (!this.layoutDoc._layout_showSidebar) this.toggleSidebar(); +// const docs = doc instanceof Doc ? [doc] : doc; +// docs.forEach(doc => { +// if (doc.lat !== undefined && doc.lng !== undefined) { +// const existingMarker = this.allMapMarkers.find(marker => marker.lat === doc.lat && marker.lng === doc.lng); +// if (existingMarker) { +// Doc.AddDocToList(existingMarker, 'data', doc); +// } else { +// const marker = Docs.Create.PushpinDocument(NumCast(doc.lat), NumCast(doc.lng), false, [doc], {}); +// this.addDocument(marker, this.annotationKey); +// } +// } +// }); //add to annotation list + +// return this.addDocument(doc, sidebarKey); // add to sidebar list +// }; + +// /** +// * Removing documents from the sidebar +// * @param doc +// * @param sidebarKey +// * @returns +// */ +// sidebarRemoveDocument = (doc: Doc | Doc[], sidebarKey?: string) => { +// if (this.layoutDoc._layout_showSidebar) this.toggleSidebar(); +// const docs = doc instanceof Doc ? [doc] : doc; +// return this.removeDocument(doc, sidebarKey); +// }; + +// /** +// * Toggle sidebar onclick the tiny comment button on the top right corner +// * @param e +// */ +// sidebarBtnDown = (e: React.PointerEvent) => { +// setupMoveUpEvents( +// this, +// e, +// (e, down, delta) => +// runInAction(() => { +// const localDelta = this._props +// .ScreenToLocalTransform() +// .scale(this._props.NativeDimScaling?.() || 1) +// .transformDirection(delta[0], delta[1]); +// const fullWidth = NumCast(this.layoutDoc._width); +// const mapWidth = fullWidth - this.sidebarWidth(); +// if (this.sidebarWidth() + localDelta[0] > 0) { +// this._showSidebar = true; +// this.layoutDoc._width = fullWidth + localDelta[0]; +// this.layoutDoc._layout_sidebarWidthPercent = ((100 * (this.sidebarWidth() + localDelta[0])) / (fullWidth + localDelta[0])).toString() + '%'; +// } else { +// this._showSidebar = false; +// this.layoutDoc._width = mapWidth; +// this.layoutDoc._layout_sidebarWidthPercent = '0%'; +// } +// return false; +// }), +// emptyFunction, +// () => UndoManager.RunInBatch(this.toggleSidebar, 'toggle sidebar map') +// ); +// }; + +// sidebarWidth = () => (Number(this.layout_sidebarWidthPercent.substring(0, this.layout_sidebarWidthPercent.length - 1)) / 100) * this._props.PanelWidth(); +// @computed get layout_sidebarWidthPercent() { +// return StrCast(this.layoutDoc._layout_sidebarWidthPercent, '0%'); +// } +// @computed get sidebarColor() { +// return StrCast(this.layoutDoc.sidebar_color, StrCast(this.layoutDoc[this._props.fieldKey + '_backgroundColor'], '#e4e4e4')); +// } + +// /** +// * function that reads the place inputed from searchbox, then zoom in on the location that's been autocompleted; +// * add a customized temporary marker on the map +// */ +// @action +// private handlePlaceChanged = () => { +// const place = this.searchBox.getPlace(); + +// if (!place.geometry || !place.geometry.location) { +// // user entered the name of a place that wasn't suggested & pressed the enter key, or place details request failed +// window.alert("No details available for input: '" + place.name + "'"); +// return; +// } + +// // zoom in on the location of the search result +// if (place.geometry.viewport) { +// this._map.fitBounds(place.geometry.viewport); +// } else { +// this._map.setCenter(place.geometry.location); +// this._map.setZoom(17); +// } + +// // customize icon => customized icon for the nature of the location selected +// const icon = { +// url: place.icon as string, +// size: new google.maps.Size(71, 71), +// origin: new google.maps.Point(0, 0), +// anchor: new google.maps.Point(17, 34), +// scaledSize: new google.maps.Size(25, 25), +// }; + +// // put temporary cutomized marker on searched location +// this.searchMarkers.forEach(marker => { +// marker.setMap(null); +// }); +// this.searchMarkers = []; +// this.searchMarkers.push( +// new window.google.maps.Marker({ +// map: this._map, +// icon, +// title: place.name, +// position: place.geometry.location, +// }) +// ); +// }; + +// /** +// * Handles toggle of sidebar on click the little comment button +// */ +// @computed get sidebarHandle() { +// return ( +// <div +// className="MapBox2-overlayButton-sidebar" +// key="sidebar" +// title="Toggle Sidebar" +// style={{ +// display: !this._props.isContentActive() ? 'none' : undefined, +// top: StrCast(this.layoutDoc._layout_showTitle) === 'title' ? 20 : 5, +// backgroundColor: this.SidebarShown ? Colors.MEDIUM_BLUE : Colors.BLACK, +// }} +// onPointerDown={this.sidebarBtnDown}> +// <FontAwesomeIcon style={{ color: Colors.WHITE }} icon={'comment-alt'} size="sm" /> +// </div> +// ); +// } + +// // TODO: Adding highlight box layer to Maps +// @action +// toggleSidebar = () => { +// //1.2 * w * ? = .2 * w .2/1.2 +// const prevWidth = this.sidebarWidth(); +// this.layoutDoc._layout_showSidebar = (this.layoutDoc._layout_sidebarWidthPercent = StrCast(this.layoutDoc._layout_sidebarWidthPercent, '0%') === '0%' ? `${(100 * 0.2) / 1.2}%` : '0%') !== '0%'; +// this.layoutDoc._width = this.layoutDoc._layout_showSidebar ? NumCast(this.layoutDoc._width) * 1.2 : Math.max(20, NumCast(this.layoutDoc._width) - prevWidth); +// }; + +// sidebarDown = (e: React.PointerEvent) => { +// setupMoveUpEvents(this, e, this.sidebarMove, emptyFunction, () => setTimeout(this.toggleSidebar), true); +// }; +// sidebarMove = (e: PointerEvent, down: number[], delta: number[]) => { +// 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%'; +// e.preventDefault(); +// return false; +// }; + +// setPreviewCursor = (func?: (x: number, y: number, drag: boolean, hide: boolean) => void) => (this._setPreviewCursor = func); + +// addDocumentWrapper = (doc: Doc | Doc[], annotationKey?: string) => { +// return this.addDocument(doc, annotationKey); +// }; + +// pointerEvents = () => { +// return this._props.isContentActive() === false ? 'none' : this._props.isContentActive() && this._props.pointerEvents?.() !== 'none' && !MarqueeOptionsMenu.Instance.isShown() ? 'all' : SnappingManager.IsDragging ? undefined : 'none'; +// }; +// @computed get annotationLayer() { +// return ( +// <div className="MapBox2-annotationLayer" style={{ height: Doc.NativeHeight(this.Document) || undefined }} ref={this._annotationLayer}> +// {this.inlineTextAnnotations +// .sort((a, b) => NumCast(a.y) - NumCast(b.y)) +// .map(anno => ( +// <Annotation key={`${anno[Id]}-annotation`} {...this._props} fieldKey={this.annotationKey} pointerEvents={this.pointerEvents} showInfo={this.showInfo} dataDoc={this.dataDoc} anno={anno} /> +// ))} +// </div> +// ); +// } + +// getAnchor = (addAsAnnotation: boolean, pinProps?: PinProps) => AnchorMenu.Instance?.GetAnchor(this._savedAnnotations, addAsAnnotation) ?? this.Document; + +// /** +// * render contents in allMapMarkers (e.g. images with exifData) into google maps as map marker +// * @returns +// */ +// private renderMarkers = () => { +// return this.allMapMarkers.map(place => ( +// <Marker key={place[Id]} position={{ lat: NumCast(place.lat), lng: NumCast(place.lng) }} onLoad={marker => this.markerLoadHandler(marker, place)} onClick={(e: google.maps.MapMouseEvent) => this.markerClickHandler(e, place)} /> +// )); +// }; + +// // 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()); +// // } +// }; + +// panelWidth = () => this._props.PanelWidth() / (this._props.NativeDimScaling?.() || 1) - this.sidebarWidth(); +// panelHeight = () => this._props.PanelHeight() / (this._props.NativeDimScaling?.() || 1); +// scrollXf = () => this.ScreenToLocalTransform().translate(0, NumCast(this.layoutDoc._layout_scrollTop)); +// transparentFilter = () => [...this._props.childFilters(), Utils.TransparentBackgroundFilter]; +// opaqueFilter = () => [...this._props.childFilters(), Utils.OpaqueBackgroundFilter]; +// infoWidth = () => this._props.PanelWidth() / 5; +// infoHeight = () => this._props.PanelHeight() / 5; +// anchorMenuClick = () => this._sidebarRef.current?.anchorMenuClick; +// savedAnnotations = () => this._savedAnnotations; + +// get MicrosoftMaps() { +// return (window as any).Microsoft.Maps; +// } +// render() { +// const renderAnnotations = (childFilters?: () => string[]) => null; +// return ( +// <div className="MapBox2" ref={this._ref}> +// <div +// className="MapBox2-wrapper" +// onWheel={e => e.stopPropagation()} +// onPointerDown={async e => { +// e.button === 0 && !e.ctrlKey && e.stopPropagation(); +// }} +// style={{ width: `calc(100% - ${this.layout_sidebarWidthPercent})`, pointerEvents: this.pointerEvents() }}> +// <div style={{ mixBlendMode: 'multiply' }}>{renderAnnotations(this.transparentFilter)}</div> +// {renderAnnotations(this.opaqueFilter)} +// {SnappingManager.IsDragging ? null : renderAnnotations()} +// {this.annotationLayer} + +// <div> +// <GoogleMap mapContainerStyle={mapContainerStyle} onZoomChanged={this.zoomChanged} onCenterChanged={this.centered} onLoad={this.loadHandler} options={mapOptions}> +// <Autocomplete onLoad={this.setSearchBox} onPlaceChanged={this.handlePlaceChanged}> +// <input className="MapBox2-input" ref={this.inputRef} type="text" onKeyDown={e => e.stopPropagation()} placeholder="Enter location" /> +// </Autocomplete> + +// {this.renderMarkers()} +// {this.allMapMarkers +// .filter(marker => marker.infoWindowOpen) +// .map(marker => ( +// <MapBoxInfoWindow +// key={marker[Id]} +// {...this._props} +// setContentView={emptyFunction} +// place={marker} +// markerMap={this.markerMap} +// PanelWidth={this.infoWidth} +// PanelHeight={this.infoHeight} +// moveDocument={this.moveDocument} +// isAnyChildContentActive={this.isAnyChildContentActive} +// whenChildContentsActiveChanged={this.whenChildContentsActiveChanged} +// /> +// ))} +// {/* {this.handleMapCenter(this._map)} */} +// </GoogleMap> +// </div> +// </div> +// {/* </LoadScript > */} +// <div className="MapBox2-sidebar" style={{ width: `${this.layout_sidebarWidthPercent}`, backgroundColor: `${this.sidebarColor}` }}> +// <SidebarAnnos +// ref={this._sidebarRef} +// {...this._props} +// fieldKey={this.fieldKey} +// Document={this.Document} +// layoutDoc={this.layoutDoc} +// dataDoc={this.dataDoc} +// usePanelWidth={true} +// showSidebar={this.SidebarShown} +// nativeWidth={NumCast(this.layoutDoc._nativeWidth)} +// whenChildContentsActiveChanged={this.whenChildContentsActiveChanged} +// PanelWidth={this.sidebarWidth} +// sidebarAddDocument={this.sidebarAddDocument} +// moveDocument={this.moveDocument} +// removeDocument={this.sidebarRemoveDocument} +// /> +// </div> +// {this.sidebarHandle} +// </div> +// ); +// } +// } diff --git a/src/client/views/nodes/MapBox/MapBoxInfoWindow.tsx b/src/client/views/nodes/MapBox/MapBoxInfoWindow.tsx index 66c47d131..6ccbbbe1c 100644 --- a/src/client/views/nodes/MapBox/MapBoxInfoWindow.tsx +++ b/src/client/views/nodes/MapBox/MapBoxInfoWindow.tsx @@ -1,96 +1,94 @@ -import { InfoWindow } from '@react-google-maps/api'; -import { action } from 'mobx'; -import { observer } from 'mobx-react'; -import * as React from 'react'; -import { Doc } from '../../../../fields/Doc'; -import { Id } from '../../../../fields/FieldSymbols'; -import { emptyFunction, returnAll, returnEmptyFilter, returnFalse, returnOne, returnTrue, returnZero, setupMoveUpEvents } from '../../../../Utils'; -import { Docs } from '../../../documents/Documents'; -import { CollectionViewType, DocumentType } from '../../../documents/DocumentTypes'; -import { CollectionNoteTakingView } from '../../collections/CollectionNoteTakingView'; -import { CollectionStackingView } from '../../collections/CollectionStackingView'; -import { ViewBoxAnnotatableProps } from '../../DocComponent'; -import { FieldViewProps } from '../FieldView'; -import { FormattedTextBox } from '../formattedText/FormattedTextBox'; -import './MapBox.scss'; +// import { InfoWindow } from '@react-google-maps/api'; +// import { action } from 'mobx'; +// import { observer } from 'mobx-react'; +// import * as React from 'react'; +// import { Doc } from '../../../../fields/Doc'; +// import { Id } from '../../../../fields/FieldSymbols'; +// import { emptyFunction, returnAll, returnEmptyFilter, returnFalse, returnOne, returnTrue, returnZero, setupMoveUpEvents } from '../../../../Utils'; +// import { Docs } from '../../../documents/Documents'; +// import { CollectionViewType, DocumentType } from '../../../documents/DocumentTypes'; +// import { CollectionNoteTakingView } from '../../collections/CollectionNoteTakingView'; +// import { CollectionStackingView } from '../../collections/CollectionStackingView'; +// import { FieldViewProps } from '../FieldView'; +// import { FormattedTextBox } from '../formattedText/FormattedTextBox'; +// import './MapBox.scss'; -interface MapBoxInfoWindowProps { - place: Doc; - renderDepth: number; - markerMap: { [id: string]: google.maps.Marker }; - isAnyChildContentActive: () => boolean; -} -@observer -export class MapBoxInfoWindow extends React.Component<MapBoxInfoWindowProps & ViewBoxAnnotatableProps & FieldViewProps> { - @action - private handleInfoWindowClose = () => { - if (this.props.place.infoWindowOpen) { - this.props.place.infoWindowOpen = false; - } - this.props.place.infoWindowOpen = false; - }; +// interface MapBoxInfoWindowProps extends FieldViewProps { +// place: Doc; +// renderDepth: number; +// markerMap: { [id: string]: google.maps.Marker }; +// isAnyChildContentActive: () => boolean; +// } +// @observer +// export class MapBoxInfoWindow extends React.Component<MapBoxInfoWindowProps> { +// @action +// private handleInfoWindowClose = () => { +// if (this.props.place.infoWindowOpen) { +// this.props.place.infoWindowOpen = false; +// } +// this.props.place.infoWindowOpen = false; +// }; - addNoteClick = (e: React.PointerEvent) => { - setupMoveUpEvents(this, e, returnFalse, emptyFunction, e => { - const newBox = Docs.Create.TextDocument('Note', { _layout_autoHeight: true }); - FormattedTextBox.SelectOnLoad = newBox[Id]; // 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', newBox); - this._stack?.scrollToBottom(); - e.stopPropagation(); - e.preventDefault(); - }); - }; +// 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.AddDocToList(this.props.place, 'data', newDoc); +// this._stack?.scrollToBottom(); +// e.stopPropagation(); +// e.preventDefault(); +// }); +// }; - _stack: CollectionStackingView | CollectionNoteTakingView | null | undefined; - childLayoutFitWidth = (doc: Doc) => doc.type === DocumentType.RTF; - addDoc = (doc: Doc | Doc[]) => (doc instanceof Doc ? [doc] : doc).reduce((p, d) => p && Doc.AddDocToList(this.props.place, 'data', d), true as boolean); - removeDoc = (doc: Doc | Doc[]) => (doc instanceof Doc ? [doc] : doc).reduce((p, d) => p && Doc.RemoveDocFromList(this.props.place, 'data', d), true as boolean); - render() { - return ( - <InfoWindow - // anchor={this.props.markerMap[this.props.place[Id]]} - onCloseClick={this.handleInfoWindowClose}> - <div className="mapbox-infowindow"> - <div style={{ width: this.props.PanelWidth(), height: this.props.PanelHeight() }}> - <CollectionStackingView - ref={r => (this._stack = r)} - {...this.props} - setContentView={emptyFunction} - Document={this.props.place} - DataDoc={undefined} - fieldKey="data" - NativeWidth={returnZero} - NativeHeight={returnZero} - childFilters={returnEmptyFilter} - setHeight={emptyFunction} - isAnnotationOverlay={false} - select={emptyFunction} - NativeDimScaling={returnOne} - isContentActive={returnTrue} - chromeHidden={true} - rootSelected={returnFalse} - childHideResizeHandles={returnTrue} - childHideDecorationTitle={returnTrue} - childLayoutFitWidth={this.childLayoutFitWidth} - // childDocumentsActive={returnFalse} - removeDocument={this.removeDoc} - addDocument={this.addDoc} - renderDepth={this.props.renderDepth + 1} - type_collection={CollectionViewType.Stacking} - pointerEvents={returnAll} - /> - </div> - <hr /> - <div - onPointerDown={this.addNoteClick} - onClick={e => { - e.stopPropagation(); - e.preventDefault(); - }}> - Add Note - </div> - </div> - </InfoWindow> - ); - } -} +// _stack: CollectionStackingView | CollectionNoteTakingView | null | undefined; +// childLayoutFitWidth = (doc: Doc) => doc.type === DocumentType.RTF; +// addDoc = (doc: Doc | Doc[]) => (doc instanceof Doc ? [doc] : doc).reduce((p, d) => p && Doc.AddDocToList(this.props.place, 'data', d), true as boolean); +// removeDoc = (doc: Doc | Doc[]) => (doc instanceof Doc ? [doc] : doc).reduce((p, d) => p && Doc.RemoveDocFromList(this.props.place, 'data', d), true as boolean); +// render() { +// return ( +// <InfoWindow +// // anchor={this.props.markerMap[this.props.place[Id]]} +// onCloseClick={this.handleInfoWindowClose}> +// <div className="mapbox-infowindow"> +// <div style={{ width: this.props.PanelWidth(), height: this.props.PanelHeight() }}> +// <CollectionStackingView +// ref={r => (this._stack = r)} +// {...this.props} +// setContentView={emptyFunction} +// Document={this.props.place} +// TemplateDataDocument={undefined} +// fieldKey="data" +// NativeWidth={returnZero} +// NativeHeight={returnZero} +// childFilters={returnEmptyFilter} +// setHeight={emptyFunction} +// isAnnotationOverlay={false} +// select={emptyFunction} +// NativeDimScaling={returnOne} +// isContentActive={returnTrue} +// chromeHidden={true} +// childHideResizeHandles={true} +// childHideDecorationTitle={true} +// childLayoutFitWidth={this.childLayoutFitWidth} +// // childDocumentsActive={returnFalse} +// removeDocument={this.removeDoc} +// addDocument={this.addDoc} +// renderDepth={this.props.renderDepth + 1} +// type_collection={CollectionViewType.Stacking} +// pointerEvents={returnAll} +// /> +// </div> +// <hr /> +// <div +// onPointerDown={this.addNoteClick} +// onClick={e => { +// e.stopPropagation(); +// e.preventDefault(); +// }}> +// Add Note +// </div> +// </div> +// </InfoWindow> +// ); +// } +// } diff --git a/src/client/views/nodes/MapBox/MapPushpinBox.tsx b/src/client/views/nodes/MapBox/MapPushpinBox.tsx index 42bada0ef..fc5b4dd18 100644 --- a/src/client/views/nodes/MapBox/MapPushpinBox.tsx +++ b/src/client/views/nodes/MapBox/MapPushpinBox.tsx @@ -1,31 +1,27 @@ -import { observer } from 'mobx-react'; -// import { SettingsManager } from '../../../util/SettingsManager'; +import * as React from 'react'; import { ViewBoxBaseComponent } from '../../DocComponent'; import { FieldView, FieldViewProps } from '../FieldView'; -import React = require('react'); -import { computed } from 'mobx'; import { MapBox } from './MapBox'; /** * Map Pushpin doc class */ -@observer export class MapPushpinBox extends ViewBoxBaseComponent<FieldViewProps>() { public static LayoutString(fieldKey: string) { return FieldView.LayoutString(MapPushpinBox, fieldKey); } componentDidMount() { - this.mapBoxView.addPushpin(this.rootDoc); + this.mapBoxView.addPushpin(this.Document); } componentWillUnmount() { - this.mapBoxView.deletePushpin(this.rootDoc); + this.mapBoxView.deletePushpin(this.Document); } - @computed get mapBoxView() { - return this.props.DocumentView?.()?.props.docViewPath().lastElement()?.ComponentView as MapBox; + get mapBoxView() { + return this.DocumentView?.()?.containerViewPath?.().lastElement()?.ComponentView as MapBox; } - @computed get mapBox() { - return this.props.DocumentView?.().props.docViewPath().lastElement()?.rootDoc; + get mapBox() { + return this.DocumentView?.().containerViewPath?.().lastElement()?.Document; } render() { diff --git a/src/client/views/nodes/MapBox/MapboxApiUtility.ts b/src/client/views/nodes/MapBox/MapboxApiUtility.ts new file mode 100644 index 000000000..592330ac2 --- /dev/null +++ b/src/client/views/nodes/MapBox/MapboxApiUtility.ts @@ -0,0 +1,139 @@ + +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'; +const MAPBOX_ACCESS_TOKEN = 'pk.eyJ1IjoiemF1bHRhdmFuZ2FyIiwiYSI6ImNscHgwNDd1MDA3MXIydm92ODdianp6cGYifQ.WFAqbhwxtMHOWSPtu0l2uQ'; + +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 response = await fetch(url); + const data = await response.json(); + return data.features; + } 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 response = await fetch(url); + const data = await response.json(); + return data.features; + } 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()) + ); + }); + + const results = await Promise.all(directionsPromises); + + const routeInfoMap: Record<TransportationType, any> = { + '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, + }; + } + }); + + return routeInfoMap; + + // 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` + } + } + + private static metersToMiles = (meters: number) => { + return `${parseFloat((meters/1609.34).toFixed(2))} mi`; + } + +} + +// const drivingQuery = await fetch( +// `${MAPBOX_DIRECTIONS_BASE_URL}/driving/${origin[0]},${origin[1]};${destination[0]},${destination[1]}?steps=true&geometries=geojson&access_token=${MAPBOX_ACCESS_TOKEN}`); + +// const cyclingQuery = await fetch( +// `${MAPBOX_DIRECTIONS_BASE_URL}/cycling/${origin[0]},${origin[1]};${destination[0]},${destination[1]}?steps=true&geometries=geojson&access_token=${MAPBOX_ACCESS_TOKEN}`); + +// const walkingQuery = await fetch( +// `${MAPBOX_DIRECTIONS_BASE_URL}/walking/${origin[0]},${origin[1]};${destination[0]},${destination[1]}?steps=true&geometries=geojson&access_token=${MAPBOX_ACCESS_TOKEN}`); + +// const drivingJson = await drivingQuery.json(); +// const cyclingJson = await cyclingQuery.json(); +// const walkingJson = await walkingQuery.json(); + +// console.log("Driving: ", drivingJson); +// console.log("Cycling: ", cyclingJson); +// console.log("Waling: ", walkingJson); + +// const routeMap = { +// 'driving': drivingJson.routes[0], +// 'cycling': cyclingJson.routes[0], +// 'walking': walkingJson.routes[0] +// } + +// const routeInfoMap: Record<TransportationType, any> = { +// 'driving': {}, +// 'cycling': {}, +// 'walking': {}, +// }; + +// Object.entries(routeMap).forEach(([key, routeData]) => { +// const transportationTypeKey = key as TransportationType; +// const geometry = routeData.geometry; +// const coordinates = geometry.coordinates; + +// console.log(coordinates); + +// routeInfoMap[transportationTypeKey] = { +// duration: this.secondsToMinutesHours(routeData.duration), +// 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 new file mode 100644 index 000000000..a580fcaa0 --- /dev/null +++ b/src/client/views/nodes/MapBox/MarkerIcons.tsx @@ -0,0 +1,103 @@ +import { IconProp } from '@fortawesome/fontawesome-svg-core'; +import { faShopify } from '@fortawesome/free-brands-svg-icons'; +import { + faBasketball, + faBicycle, + faBowlFood, + faBus, + faCameraRetro, + faCar, + faCartShopping, + faFilm, + faFootball, + faFutbol, + faHockeyPuck, + faHospital, + faHotel, + faHouse, + faLandmark, + faLocationDot, + faLocationPin, + faMapPin, + faMasksTheater, + faMugSaucer, + faPersonHiking, + faPlane, + faSchool, + faShirt, + faShop, + faSquareParking, + faStar, + faTrainSubway, + faTree, + faUtensils, + faVolleyball, +} from '@fortawesome/free-solid-svg-icons'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import * as React from 'react'; + +export class MarkerIcons { + // static getMapboxIcon = (color: string) => { + // return ( + // <svg xmlns="http://www.w3.org/2000/svg" id="marker" data-name="marker" width="20" height="48" viewBox="0 0 20 35"> + // <g id="mapbox-marker-icon"> + // <g id="icon"> + // <ellipse id="shadow" cx="10" cy="27" rx="9" ry="5" fill="#c4c4c4" opacity="0.3" /> + // <g id="mask" opacity="0.3"> + // <g id="group"> + // <path id="shadow-2" data-name="shadow" fill="#bfbfbf" d="M10,32c5,0,9-2.2,9-5s-4-5-9-5-9,2.2-9,5S5,32,10,32Z" fillRule="evenodd"/> + // </g> + // </g> + // <path id="color" fill={color} strokeWidth="0.5" d="M19.25,10.4a13.0663,13.0663,0,0,1-1.4607,5.2235,41.5281,41.5281,0,0,1-3.2459,5.5483c-1.1829,1.7369-2.3662,3.2784-3.2541,4.3859-.4438.5536-.8135.9984-1.0721,1.3046-.0844.1-.157.1852-.2164.2545-.06-.07-.1325-.1564-.2173-.2578-.2587-.3088-.6284-.7571-1.0723-1.3147-.8879-1.1154-2.0714-2.6664-3.2543-4.41a42.2677,42.2677,0,0,1-3.2463-5.5535A12.978,12.978,0,0,1,.75,10.4,9.4659,9.4659,0,0,1,10,.75,9.4659,9.4659,0,0,1,19.25,10.4Z"/> + // <path id="circle" fill="#fff" stroke='white' strokeWidth="0.5" d="M13.55,10A3.55,3.55,0,1,1,10,6.45,3.5484,3.5484,0,0,1,13.55,10Z"/> + // </g> + // </g> + // <rect width="20" height="48" fill="none"/> + // </svg> + // ) + // } + + static getFontAwesomeIcon(key: string, size: string, color?: string): JSX.Element { + const icon: IconProp = MarkerIcons.FAMarkerIconsMap[key]; + const iconProps: any = { icon }; + + if (color) { + iconProps.color = color; + } + + return <FontAwesomeIcon {...iconProps} size={size} />; + } + + static FAMarkerIconsMap: { [key: string]: IconProp } = { + MAP_PIN: faLocationDot, + RESTAURANT_ICON: faUtensils, + HOTEL_ICON: faHotel, + HOUSE_ICON: faHouse, + AIRPLANE_ICON: faPlane, + CAR_ICON: faCar, + BUS_ICON: faBus, + TRAIN_ICON: faTrainSubway, + BICYCLE_ICON: faBicycle, + PARKING_ICON: faSquareParking, + PHOTO_ICON: faCameraRetro, + CAFE_ICON: faMugSaucer, + STAR_ICON: faStar, + SHOPPING_CART_ICON: faCartShopping, + SHOPIFY_ICON: faShopify, + SHOP_ICON: faShop, + SHIRT_ICON: faShirt, + FOOD_ICON: faBowlFood, + LANDMARK_ICON: faLandmark, + HOSPITAL_ICON: faHospital, + NATURE_ICON: faTree, + HIKING_ICON: faPersonHiking, + SOCCER_ICON: faFutbol, + VOLLEYBALL_ICON: faVolleyball, + BASKETBALL_ICON: faBasketball, + HOCKEY_ICON: faHockeyPuck, + FOOTBALL_ICON: faFootball, + SCHOOL_ICON: faSchool, + THEATER_ICON: faMasksTheater, + FILM_ICON: faFilm, + }; +} diff --git a/src/client/views/nodes/MapBox/icon_images/mapbox-marker-icon-20px-blue.png b/src/client/views/nodes/MapBox/icon_images/mapbox-marker-icon-20px-blue.png Binary files differnew file mode 100644 index 000000000..8b686e2aa --- /dev/null +++ b/src/client/views/nodes/MapBox/icon_images/mapbox-marker-icon-20px-blue.png diff --git a/src/client/views/nodes/MapboxMapBox/MapboxContainer.tsx b/src/client/views/nodes/MapboxMapBox/MapboxContainer.tsx new file mode 100644 index 000000000..8a5bd7ce6 --- /dev/null +++ b/src/client/views/nodes/MapboxMapBox/MapboxContainer.tsx @@ -0,0 +1,843 @@ +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { Button, EditableText, IconButton, 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 { MapProvider, Map as MapboxMap } from 'react-map-gl'; +import { Utils, emptyFunction, returnEmptyDoclist, returnEmptyFilter, returnFalse, returnOne, setupMoveUpEvents } 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 { DocumentType } from '../../../documents/DocumentTypes'; +import { DocUtils, Docs } from '../../../documents/Documents'; +import { DocumentManager } from '../../../util/DocumentManager'; +import { DragManager } from '../../../util/DragManager'; +import { LinkManager } from '../../../util/LinkManager'; +import { SnappingManager } from '../../../util/SnappingManager'; +import { Transform } from '../../../util/Transform'; +import { UndoManager, undoable } from '../../../util/UndoManager'; +import { ViewBoxAnnotatableComponent } from '../../DocComponent'; +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 { MapAnchorMenu } from '../MapBox/MapAnchorMenu'; +import { FormattedTextBox } from '../formattedText/FormattedTextBox'; +import { PinProps, PresBox } from '../trails'; +import './MapBox.scss'; + +/** + * MapBox architecture: + * Main component: MapBox.tsx + * Supporting Components: SidebarAnnos, CollectionStackingView + * + * MapBox is a node that extends the ViewBoxAnnotatableComponent. Similar to PDFBox and WebBox, it supports interaction between sidebar content and document content. + * The main body of MapBox uses Google Maps API to allow location retrieval, adding map markers, pan and zoom, and open street view. + * Dash Document architecture is integrated with Maps API: When drag and dropping documents with ExifData (gps Latitude and Longitude information) available, + * sidebarAddDocument function checks if the document contains lat & lng information, if it does, then the document is added to both the sidebar and the infowindow (a pop up corresponding to a map marker--pin on map). + * The lat and lng field of the document is filled when importing (spec see ConvertDMSToDD method and processFileUpload method in Documents.ts). + * A map marker is considered a document that contains a collection with stacking view of documents, it has a lat, lng location, which is passed to Maps API's custom marker (red pin) to be rendered on the google maps + */ + +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 + */ +// const drawingManager = new window.google.maps.drawing.DrawingManager({ +// drawingControl: true, +// drawingControlOptions: { +// position: google.maps.ControlPosition.TOP_RIGHT, +// drawingModes: [ +// google.maps.drawing.OverlayType.MARKER, +// // currently we are not supporting the following drawing mode on map, a thought for future development +// google.maps.drawing.OverlayType.CIRCLE, +// google.maps.drawing.OverlayType.POLYLINE, +// ], +// }, +// }); + +@observer +export class MapBoxContainer extends ViewBoxAnnotatableComponent<FieldViewProps>() { + public static LayoutString(fieldKey: string) { + return FieldView.LayoutString(MapBoxContainer, fieldKey); + } + private _dragRef = React.createRef<HTMLDivElement>(); + private _sidebarRef = React.createRef<SidebarAnnos>(); + private _ref: React.RefObject<HTMLDivElement> = React.createRef(); + private _disposers: { [key: string]: IReactionDisposer } = {}; + private _setPreviewCursor: undefined | ((x: number, y: number, drag: boolean, hide: boolean, doc: Opt<Doc>) => void); + + constructor(props: FieldViewProps) { + super(props); + makeObservable(this); + } + + @observable private _savedAnnotations = new ObservableMap<number, HTMLDivElement[]>(); + @computed get allSidebarDocs() { + return DocListCast(this.dataDoc[this.SidebarKey]); + } + // this list contains pushpins and configs + @computed get allAnnotations() { + return DocListCast(this.dataDoc[this.annotationKey]); + } + @computed get allPushpins() { + return this.allAnnotations.filter(anno => anno.type === DocumentType.PUSHPIN); + } + @computed get SidebarShown() { + return this.layoutDoc._layout_showSidebar ? true : false; + } + @computed get sidebarWidthPercent() { + return StrCast(this.layoutDoc._layout_sidebarWidthPercent, '0%'); + } + @computed get sidebarColor() { + return StrCast(this.layoutDoc.sidebar_color, StrCast(this.layoutDoc[this._props.fieldKey + '_backgroundColor'], '#e4e4e4')); + } + @computed get SidebarKey() { + return this.fieldKey + '_sidebar'; + } + + componentDidMount() { + this._unmounting = false; + this._props.setContentViewBox?.(this); + } + + _unmounting = false; + componentWillUnmount(): void { + this._unmounting = true; + this.deselectPin(); + this._rerenderTimeout && clearTimeout(this._rerenderTimeout); + Object.keys(this._disposers).forEach(key => this._disposers[key]?.()); + } + + /** + * 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 sidebarKey + * @returns + */ + sidebarAddDocument = (doc: Doc | Doc[], sidebarKey?: string) => { + if (!this.layoutDoc._layout_showSidebar) this.toggleSidebar(); + const docs = doc instanceof Doc ? [doc] : doc; + 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) { + existingPin = this.createPushpin(NumCast(doc.latitude), NumCast(doc.longitude), StrCast(doc.map)); + } + 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)) { + const anchor = this.getAnchor(true, undefined, existingPin); + anchor && DocUtils.MakeLink(anchor, doc, { link_relationship: 'link to map location' }); + doc.latitude = existingPin?.latitude; + doc.longitude = existingPin?.longitude; + } + }); + } + }); //add to annotation list + + return this.addDocument(doc, 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); + }; + + /** + * Removing documents from the sidebar + * @param doc + * @param sidebarKey + * @returns + */ + sidebarRemoveDocument = (doc: Doc | Doc[], sidebarKey?: string) => this.removeMapDocument(doc, sidebarKey); + + /** + * Toggle sidebar onclick the tiny comment button on the top right corner + * @param e + */ + sidebarBtnDown = (e: React.PointerEvent) => { + setupMoveUpEvents( + this, + e, + (e, down, delta) => + runInAction(() => { + const localDelta = this._props + .ScreenToLocalTransform() + .scale(this._props.NativeDimScaling?.() || 1) + .transformDirection(delta[0], delta[1]); + const fullWidth = NumCast(this.layoutDoc._width); + const mapWidth = fullWidth - this.sidebarWidth(); + if (this.sidebarWidth() + localDelta[0] > 0) { + this.layoutDoc._layout_showSidebar = true; + this.layoutDoc._width = fullWidth + localDelta[0]; + this.layoutDoc._layout_sidebarWidthPercent = ((100 * (this.sidebarWidth() + localDelta[0])) / (fullWidth + localDelta[0])).toString() + '%'; + } else { + this.layoutDoc._layout_showSidebar = false; + this.layoutDoc._width = mapWidth; + this.layoutDoc._layout_sidebarWidthPercent = '0%'; + } + return false; + }), + emptyFunction, + () => UndoManager.RunInBatch(this.toggleSidebar, 'toggle sidebar map') + ); + }; + sidebarWidth = () => (Number(this.sidebarWidthPercent.substring(0, this.sidebarWidthPercent.length - 1)) / 100) * this._props.PanelWidth(); + + /** + * Handles toggle of sidebar on click the little comment button + */ + @computed get sidebarHandle() { + return ( + <div + className="mapBox-overlayButton-sidebar" + key="sidebar" + title="Toggle Sidebar" + style={{ + display: !this._props.isContentActive() ? 'none' : undefined, + top: StrCast(this.Document._layout_showTitle) === 'title' ? 20 : 5, + backgroundColor: this.SidebarShown ? Colors.MEDIUM_BLUE : Colors.BLACK, + }} + onPointerDown={this.sidebarBtnDown}> + <FontAwesomeIcon style={{ color: Colors.WHITE }} icon={'comment-alt'} size="sm" /> + </div> + ); + } + + // TODO: Adding highlight box layer to Maps + @action + toggleSidebar = () => { + const prevWidth = this.sidebarWidth(); + this.layoutDoc._layout_showSidebar = (this.layoutDoc._layout_sidebarWidthPercent = StrCast(this.layoutDoc._layout_sidebarWidthPercent, '0%') === '0%' ? `${(100 * 0.2) / 1.2}%` : '0%') !== '0%'; + this.layoutDoc._width = this.layoutDoc._layout_showSidebar ? NumCast(this.layoutDoc._width) * 1.2 : Math.max(20, NumCast(this.layoutDoc._width) - prevWidth); + }; + + startAnchorDrag = (e: PointerEvent, ele: HTMLElement) => { + e.preventDefault(); + e.stopPropagation(); + + const sourceAnchorCreator = action(() => { + const note = this.getAnchor(true); + if (note && this.selectedPin) { + note.latitude = this.selectedPin.latitude; + note.longitude = this.selectedPin.longitude; + note.map = this.selectedPin.map; + } + return note as Doc; + }); + + const targetCreator = (annotationOn: Doc | undefined) => { + const target = DocUtils.GetNewTextDoc('Note linked to ' + this.Document.title, 0, 0, 100, 100, undefined, annotationOn, 'yellow'); + FormattedTextBox.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; + } + }, + }); + }; + + createNoteAnnotation = () => { + const createFunc = undoable( + action(() => { + const note = this._sidebarRef.current?.anchorMenuClick(this.getAnchor(true), ['latitude', 'longitude', LinkedTo]); + if (note && this.selectedPin) { + note.latitude = this.selectedPin.latitude; + note.longitude = this.selectedPin.longitude; + note.map = this.selectedPin.map; + } + }), + 'create note annotation' + ); + if (!this.layoutDoc.layout_showSidebar) { + this.toggleSidebar(); + setTimeout(createFunc); + } else createFunc(); + }; + sidebarDown = (e: React.PointerEvent) => { + setupMoveUpEvents(this, e, this.sidebarMove, emptyFunction, () => setTimeout(this.toggleSidebar), true); + }; + sidebarMove = (e: PointerEvent, down: number[], delta: number[]) => { + 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%'; + e.preventDefault(); + return false; + }; + + setPreviewCursor = (func?: (x: number, y: number, drag: boolean, hide: boolean, doc: Opt<Doc>) => void) => (this._setPreviewCursor = func); + + addDocumentWrapper = (doc: Doc | Doc[], annotationKey?: string) => this.addDocument(doc, annotationKey); + + pointerEvents = () => (this._props.isContentActive() && !MarqueeOptionsMenu.Instance.isShown() ? 'all' : 'none'); + + 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]; + infoWidth = () => this._props.PanelWidth() / 5; + infoHeight = () => this._props.PanelHeight() / 5; + anchorMenuClick = () => this._sidebarRef.current?.anchorMenuClick; + savedAnnotations = () => this._savedAnnotations; + + _bingSearchManager: any; + _bingMap: any; + get MicrosoftMaps() { + return (window as any).Microsoft.Maps; + } + // uses Bing Search to retrieve lat/lng for a location. eg., + // const results = this.geocodeQuery(map.map, 'Philadelphia, PA'); + // to move the map to that location: + // const location = await this.geocodeQuery(this._bingMap, 'Philadelphia, PA'); + // this._bingMap.current.setView({ + // mapTypeId: this.MicrosoftMaps.MapTypeId.aerial, + // 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. + if (!this._bingSearchManager) { + //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)); + }); + } else { + this._bingSearchManager.geocode({ + where: query, + callback: action((r: any) => res(r.results[0].location)), + errorCallback: (e: any) => reject(), + }); + } + }); + }; + + @observable + bingSearchBarContents: any = this.Document.map; // For Bing Maps: The contents of the Bing search bar (string) + + geoDataRequestOptions = { + entityType: 'PopulatedPlace', + }; + + // incrementer: number = 0; + /* + * Creates Pushpin doc and adds it to the list of annotations + */ + @action + createPushpin = undoable((latitude: number, longitude: number, map?: string) => { + // Stores the pushpin as a MapMarkerDocument + const pushpin = Docs.Create.PushpinDocument( + NumCast(latitude), + NumCast(longitude), + false, + [], + { title: map ?? `lat=${latitude},lng=${longitude}`, map: map } + // ,'pushpinIDamongus'+ this.incrementer++ + ); + this.addDocument(pushpin, this.annotationKey); + return pushpin; + // mapMarker.infoWindowOpen = true; + }, 'createpin'); + + // The pin that is selected + @observable selectedPin: Doc | undefined = undefined; + + @action + deselectPin = () => { + if (this.selectedPin) { + // Removes filter + Doc.setDocFilter(this.Document, 'latitude', this.selectedPin.latitude, 'remove'); + Doc.setDocFilter(this.Document, 'longitude', this.selectedPin.longitude, 'remove'); + Doc.setDocFilter(this.Document, LinkedTo, `mapPin=${Field.toScriptString(DocCast(this.selectedPin))}`, 'remove'); + + const temp = this.selectedPin; + if (!this._unmounting) { + 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)); + if (!this._unmounting) { + this._bingMap.current.entities.push(newpin); + } + this.map_docToPinMap.set(temp, newpin); + this.selectedPin = undefined; + this.bingSearchBarContents = this.Document.map; + } + }; + + getView = async (doc: Doc, options: FocusViewOptions) => { + if (this._sidebarRef?.current?.makeDocUnfiltered(doc) && !this.SidebarShown) { + this.toggleSidebar(); + options.didMove = true; + } + return new Promise<Opt<DocumentView>>(res => DocumentManager.Instance.AddViewRenderedCb(doc, dv => res(dv))); + }; + /* + * Pushpin onclick + */ + @action + pushpinClicked = (pinDoc: Doc) => { + this.deselectPin(); + this.selectedPin = pinDoc; + this.bingSearchBarContents = pinDoc.map; + + // Doc.setDocFilter(this.Document, 'latitude', this.selectedPin.latitude, 'match'); + // Doc.setDocFilter(this.Document, 'longitude', this.selectedPin.longitude, 'match'); + Doc.setDocFilter(this.Document, LinkedTo, `mapPin=${Field.toScriptString(this.selectedPin)}`, 'check'); + + this.recolorPin(this.selectedPin, 'green'); + + MapAnchorMenu.Instance.Delete = this.deleteSelectedPin; + MapAnchorMenu.Instance.Center = this.centerOnSelectedPin; + MapAnchorMenu.Instance.OnClick = this.createNoteAnnotation; + MapAnchorMenu.Instance.StartDrag = this.startAnchorDrag; + + const point = this._bingMap.current.tryLocationToPixel(new this.MicrosoftMaps.Location(this.selectedPin.latitude, this.selectedPin.longitude)); + const x = point.x + (this._props.PanelWidth() - this.sidebarWidth()) / 2; + const y = point.y + this._props.PanelHeight() / 2 + 32; + const cpt = this.ScreenToLocalBoxXf().inverse().transformPoint(x, y); + MapAnchorMenu.Instance.jumpTo(cpt[0], cpt[1], true); + + document.addEventListener('pointerdown', this.tryHideMapAnchorMenu, true); + }; + + /** + * Map OnClick + */ + @action + mapOnClick = (e: { location: { latitude: any; longitude: any } }) => { + this._props.select(false); + this.deselectPin(); + }; + /* + * Updates values of layout doc to match the current map + */ + @action + mapRecentered = () => { + if ( + Math.abs(NumCast(this.dataDoc.latitude) - this._bingMap.current.getCenter().latitude) > 1e-7 || // + Math.abs(NumCast(this.dataDoc.longitude) - this._bingMap.current.getCenter().longitude) > 1e-7 + ) { + this.dataDoc.latitude = this._bingMap.current.getCenter().latitude; + this.dataDoc.longitude = this._bingMap.current.getCenter().longitude; + this.dataDoc.map = ''; + this.bingSearchBarContents = ''; + } + this.dataDoc.map_zoom = this._bingMap.current.getZoom(); + }; + /* + * Updates maptype + */ + @action + 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 => { + 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 + */ + getAnchor = (addAsAnnotation: boolean, pinProps?: PinProps, existingPin?: Doc) => { + /// this should use SELECTED pushpin for lat/long if there is a selection, otherwise CENTER + const anchor = Docs.Create.ConfigDocument({ + title: 'MapAnchor:' + this.Document.title, + text: StrCast(this.selectedPin?.map) || StrCast(this.Document.map) || 'map location', + config_latitude: NumCast((existingPin ?? this.selectedPin)?.latitude ?? this.dataDoc.latitude), + config_longitude: NumCast((existingPin ?? this.selectedPin)?.longitude ?? this.dataDoc.longitude), + config_map_zoom: NumCast(this.dataDoc.map_zoom), + config_map_type: StrCast(this.dataDoc.map_type), + config_map: StrCast((existingPin ?? this.selectedPin)?.map) || StrCast(this.dataDoc.map), + layout_unrendered: true, + mapPin: existingPin ?? this.selectedPin, + annotationOn: this.Document, + }); + if (anchor) { + if (!addAsAnnotation) anchor.backgroundColor = 'transparent'; + addAsAnnotation && this.addDocument(anchor); + PresBox.pinDocView(anchor, { pinDocLayout: pinProps?.pinDocLayout, pinData: { ...(pinProps?.pinData ?? {}), map: true } }, this.Document); + return anchor; + } + return this.Document; + }; + + map_docToPinMap = new Map<Doc, any>(); + map_pinHighlighted = new Map<Doc, boolean>(); + /* + * Input: pin doc + * Adds MicrosoftMaps Pushpin to the map (render) + */ + @action + addPushpin = (pin: Doc) => { + const pushPin = pin.infoWindowOpen + ? new this.MicrosoftMaps.Pushpin(new this.MicrosoftMaps.Location(pin.latitude, pin.longitude), {}) + : new this.MicrosoftMaps.Pushpin( + new this.MicrosoftMaps.Location(pin.latitude, pin.longitude) + // {icon: 'http://icons.iconarchive.com/icons/icons-land/vista-map-markers/24/Map-Marker-Marker-Outside-Chartreuse-icon.png'} + ); + + this._bingMap.current.entities.push(pushPin); + + this.MicrosoftMaps.Events.addHandler(pushPin, 'click', (e: any) => this.pushpinClicked(pin)); + // this.MicrosoftMaps.Events.addHandler(pushPin, 'dblclick', (e: any) => this.pushpinDblClicked(pushPin, pin)); + this.map_docToPinMap.set(pin, pushPin); + }; + + /* + * Input: pin doc + * Removes pin from annotations + */ + @action + removePushpin = (pinDoc: Doc) => this.removeMapDocument(pinDoc, this.annotationKey); + + /* + * Removes pushpin from map render + */ + deletePushpin = (pinDoc: Doc) => { + if (!this._unmounting) { + this._bingMap.current.entities.remove(this.map_docToPinMap.get(pinDoc)); + } + this.map_docToPinMap.delete(pinDoc); + this.selectedPin = undefined; + }; + + @action + deleteSelectedPin = undoable(() => { + if (this.selectedPin) { + // Removes filter + Doc.setDocFilter(this.Document, 'latitude', this.selectedPin.latitude, 'remove'); + Doc.setDocFilter(this.Document, 'longitude', this.selectedPin.longitude, 'remove'); + Doc.setDocFilter(this.Document, LinkedTo, `mapPin=${Field.toScriptString(DocCast(this.selectedPin))}`, 'remove'); + + this.removePushpin(this.selectedPin); + } + MapAnchorMenu.Instance.fadeOut(true); + document.removeEventListener('pointerdown', this.tryHideMapAnchorMenu, true); + }, 'delete pin'); + + tryHideMapAnchorMenu = (e: PointerEvent) => { + let target = document.elementFromPoint(e.x, e.y); + while (target) { + if (target === MapAnchorMenu.top.current) return; + target = target.parentElement; + } + e.stopPropagation(); + e.preventDefault(); + MapAnchorMenu.Instance.fadeOut(true); + document.removeEventListener('pointerdown', this.tryHideMapAnchorMenu, true); + }; + + @action + centerOnSelectedPin = () => { + if (this.selectedPin) { + this.dataDoc.latitude = this.selectedPin.latitude; + this.dataDoc.longitude = this.selectedPin.longitude; + this.dataDoc.map = this.selectedPin.map ?? ''; + this.bingSearchBarContents = this.selectedPin.map; + } + MapAnchorMenu.Instance.fadeOut(true); + document.removeEventListener('pointerdown', this.tryHideMapAnchorMenu); + }; + + /** + * View options for bing maps + */ + bingViewOptions = { + // center: { latitude: this.dataDoc.latitude ?? defaultCenter.lat, longitude: this.dataDoc.longitude ?? defaultCenter.lng }, + zoom: this.dataDoc.latitude ?? 10, + mapTypeId: 'grayscale', + }; + + /** + * Map options + */ + bingMapOptions = { + navigationBarMode: 'square', + backgroundColor: '#f1f3f4', + enableInertia: true, + supportedMapTypes: ['grayscale', 'canvasLight'], + disableMapTypeSelectorMouseOver: true, + // showScalebar:true + // disableRoadView:true, + // disableBirdseye:true + streetsideOptions: { + showProblemReporting: false, + showCurrentAddress: false, + }, + }; + + @action + 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._bingMap.current.entities.push(newpin); + this.map_docToPinMap.set(pin, newpin); + }; + + /* + * Called when BingMap is first rendered + * Initializes starting values + */ + @observable _mapReady = false; + @action + bingMapReady = (map: any) => { + this._mapReady = true; + this._bingMap = map.map; + if (!this._bingMap.current) { + alert('NO Map!?'); + } + this.MicrosoftMaps.Events.addHandler(this._bingMap.current, 'click', this.mapOnClick); + this.MicrosoftMaps.Events.addHandler(this._bingMap.current, 'viewchangeend', undoable(this.mapRecentered, 'Map Layout Change')); + this.MicrosoftMaps.Events.addHandler(this._bingMap.current, 'maptypechanged', undoable(this.updateMapType, 'Map ViewType Change')); + + this._disposers.mapLocation = reaction( + () => this.Document.map, + 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 }) => { + if (!pushpin[Highlight] && this.map_pinHighlighted.get(pushpin)) { + this.recolorPin(pushpin); + this.map_pinHighlighted.delete(pushpin); + } + }); + allConfigPins.forEach(({ doc, pushpin }) => { + if (doc[Highlight] && !this.map_pinHighlighted.get(pushpin)) { + this.recolorPin(pushpin, 'orange'); + this.map_pinHighlighted.set(pushpin, true); + } + }); + }, + { fireImmediately: true } + ); + + this._disposers.location = reaction( + () => ({ lat: this.Document.latitude, lng: this.Document.longitude, zoom: this.Document.map_zoom, mapType: this.Document.map_type }), + locationObject => { + // if (this._bingMap.current) + try { + locationObject?.zoom && + this._bingMap.current?.setView({ + mapTypeId: locationObject.mapType, + zoom: locationObject.zoom, + center: new this.MicrosoftMaps.Location(locationObject.lat, locationObject.lng), + }); + } catch (e) { + console.log(e); + } + }, + { fireImmediately: true } + ); + }; + + dragToggle = (e: React.PointerEvent) => { + let dragClone: HTMLDivElement | undefined; + + setupMoveUpEvents( + e, + e, + e => { + 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)`; + return false; + }, + e => { + if (!dragClone) return; + DragManager.Root().removeChild(dragClone); + let target = document.elementFromPoint(e.x, e.y); + while (target) { + if (target === this._ref.current) { + const cpt = this.ScreenToLocalBoxXf().transformPoint(e.clientX, e.clientY); + const x = cpt[0] - (this._props.PanelWidth() - this.sidebarWidth()) / 2; + const y = cpt[1] - 32 /* height of search bar */ - this._props.PanelHeight() / 2; + const location = this._bingMap.current.tryPixelToLocation(new this.MicrosoftMaps.Point(x, y)); + this.createPushpin(location.latitude, location.longitude); + break; + } + target = target.parentElement; + } + }, + e => { + const createPin = () => this.createPushpin(this.Document.latitude, this.Document.longitude, this.Document.map); + if (this.bingSearchBarContents) { + this.bingSearch().then(createPin); + } else createPin(); + } + ); + }; + + searchbarKeyDown = (e: any) => e.key === 'Enter' && this.bingSearch(); + + static _firstRender = true; + static _rerenderDelay = 500; + _rerenderTimeout: any; + render() { + // bcz: no idea what's going on here, but bings maps have some kind of bug + // such that we need to delay rendering a second map on startup until the first map is rendered. + this.Document[DocCss]; + if (MapBoxContainer._rerenderDelay) { + // prettier-ignore + this._rerenderTimeout = this._rerenderTimeout ?? + setTimeout(action(() => { + if ((window as any).Microsoft?.Maps?.Internal._WorkDispatcher) { + MapBoxContainer._rerenderDelay = 0; + } + this._rerenderTimeout = undefined; + this.Document[DocCss] = this.Document[DocCss] + 1; + }), MapBoxContainer._rerenderDelay); + return null; + } + + const renderAnnotations = (childFilters?: () => string[]) => null; + return ( + <div className="mapBox" ref={this._ref}> + <div + className="mapBox-wrapper" + onWheel={e => e.stopPropagation()} + onPointerDown={async e => { + 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()} + placeholder={this.bingSearchBarContents || 'enter city/zip/...'} + textAlign="center" + /> + <IconButton + icon={ + <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> + </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'} />} /> + </div> + </div> + <MapProvider> + <MapboxMap id="mabox-map" mapStyle={`mapbox://styles/mapbox/streets-v9`} mapboxAccessToken={mapboxApiKey} /> + </MapProvider> + + {/* + <BingMapsReact + onMapReady={this.bingMapReady} // + bingMapsKey={bingApiKey} + height="100%" + mapOptions={this.bingMapOptions} + width="100%" + viewOptions={this.bingViewOptions} + /> */} + <div> + {!this._mapReady + ? null + : this.allAnnotations + .filter(anno => !anno.layout_unrendered) + .map((pushpin, i) => ( + <DocumentView + key={i} + {...this._props} + renderDepth={this._props.renderDepth + 1} + Document={pushpin} + PanelWidth={returnOne} + PanelHeight={returnOne} + NativeWidth={returnOne} + NativeHeight={returnOne} + onKey={undefined} + onDoubleClickScript={undefined} + onBrowseClickScript={undefined} + childFilters={returnEmptyFilter} + childFiltersByRanges={returnEmptyFilter} + searchFilterDocs={returnEmptyDoclist} + isDocumentActive={returnFalse} + isContentActive={returnFalse} + addDocTab={returnFalse} + ScreenToLocalTransform={Transform.Identity} + fitContentsToBox={undefined} + focus={returnOne} + /> + ))} + </div> + {/* <MapBoxInfoWindow + key={Docs.Create.MapMarkerDocument(NumCast(40), NumCast(40), false, [], {})[Id]} + {...OmitKeys(this._props, ['NativeWidth', 'NativeHeight', 'setContentView']).omit} + place={Docs.Create.MapMarkerDocument(NumCast(40), NumCast(40), false, [], {})} + markerMap={this.markerMap} + PanelWidth={this.infoWidth} + PanelHeight={this.infoHeight} + moveDocument={this.moveDocument} + isAnyChildContentActive={this.isAnyChildContentActive} + whenChildContentsActiveChanged={this.whenChildContentsActiveChanged} + /> */} + </div> + {/* </LoadScript > */} + <div className="mapBox-sidebar" style={{ width: `${this.sidebarWidthPercent}`, backgroundColor: `${this.sidebarColor}` }}> + <SidebarAnnos + ref={this._sidebarRef} + {...this._props} + fieldKey={this.fieldKey} + Document={this.Document} + layoutDoc={this.layoutDoc} + dataDoc={this.dataDoc} + usePanelWidth={true} + showSidebar={this.SidebarShown} + nativeWidth={NumCast(this.layoutDoc._nativeWidth)} + whenChildContentsActiveChanged={this.whenChildContentsActiveChanged} + PanelWidth={this.sidebarWidth} + sidebarAddDocument={this.sidebarAddDocument} + moveDocument={this.moveDocument} + removeDocument={this.sidebarRemoveDocument} + /> + </div> + {this.sidebarHandle} + </div> + ); + } +} diff --git a/src/client/views/nodes/PDFBox.scss b/src/client/views/nodes/PDFBox.scss index 8a68f9647..0f5e25a0c 100644 --- a/src/client/views/nodes/PDFBox.scss +++ b/src/client/views/nodes/PDFBox.scss @@ -1,4 +1,4 @@ -@import "../global/globalCssVariables.scss"; +@import '../global/globalCssVariables.module.scss'; .pdfBox, .pdfBox-interactive { @@ -38,7 +38,7 @@ box-shadow: $standard-box-shadow; transition: 0.2s; - &:hover{ + &:hover { filter: brightness(0.85); } } @@ -51,7 +51,8 @@ left: 5px; top: 5px; - .pdfBox-fwdBtn, .pdfBox-backBtn { + .pdfBox-fwdBtn, + .pdfBox-backBtn { background: #121721; height: 25px; width: 25px; @@ -119,7 +120,6 @@ background: none; } - .pdfBox-settingsCont { position: absolute; right: 0; @@ -194,7 +194,7 @@ justify-content: center; align-items: center; overflow: hidden; - transition: left .5s; + transition: left 0.5s; pointer-events: all; .pdfBox-searchBar { @@ -204,7 +204,6 @@ } } - .pdfBox-title-outer { width: 100%; height: 100%; @@ -269,7 +268,6 @@ // CSS adjusted for mobile devices @media only screen and (max-device-width: 480px) { - .pdfBox .pdfBox-ui .pdfBox-settingsCont .pdfBox-settingsButton, .pdfBox-interactive .pdfBox-ui .pdfBox-settingsCont .pdfBox-settingsButton { height: 60px; @@ -288,15 +286,11 @@ } } - - .pdfBox .pdfBox-ui .pdfBox-settingsCont .pdfBox-settingsFlyout, .pdfBox-interactive .pdfBox-ui .pdfBox-settingsCont .pdfBox-settingsFlyout { font-size: 30px; } - - .pdfBox .pdfBox-ui .pdfBox-overlayCont, .pdfBox-interactive .pdfBox-ui .pdfBox-overlayCont { height: 60px; diff --git a/src/client/views/nodes/PDFBox.tsx b/src/client/views/nodes/PDFBox.tsx index 537da5055..1274220b6 100644 --- a/src/client/views/nodes/PDFBox.tsx +++ b/src/client/views/nodes/PDFBox.tsx @@ -1,10 +1,11 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { action, computed, IReactionDisposer, observable, reaction, runInAction } from 'mobx'; +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 { Doc, DocListCast, Opt } from '../../../fields/Doc'; -import { Height, Width } from '../../../fields/DocSymbols'; +import { DocData } from '../../../fields/DocSymbols'; import { Id } from '../../../fields/FieldSymbols'; import { InkTool } from '../../../fields/InkField'; import { ComputedField } from '../../../fields/ScriptField'; @@ -22,20 +23,19 @@ import { CollectionFreeFormView } from '../collections/collectionFreeForm'; import { CollectionStackingView } from '../collections/CollectionStackingView'; import { ContextMenu } from '../ContextMenu'; import { ContextMenuProps } from '../ContextMenuItem'; -import { ViewBoxAnnotatableComponent, ViewBoxAnnotatableProps } from '../DocComponent'; +import { ViewBoxAnnotatableComponent, ViewBoxInterface } from '../DocComponent'; import { Colors } from '../global/globalEnums'; import { CreateImage } from '../nodes/WebBoxRenderer'; import { PDFViewer } from '../pdf/PDFViewer'; import { SidebarAnnos } from '../SidebarAnnos'; -import { DocFocusOptions, DocumentView, OpenWhere } from './DocumentView'; -import { FieldView, FieldViewProps } from './FieldView'; +import { DocumentView, OpenWhere } from './DocumentView'; +import { FocusViewOptions, FieldView, FieldViewProps } from './FieldView'; import { ImageBox } from './ImageBox'; import './PDFBox.scss'; import { PinProps, PresBox } from './trails'; -import React = require('react'); @observer -export class PDFBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps & FieldViewProps>() { +export class PDFBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implements ViewBoxInterface { public static LayoutString(fieldKey: string) { return FieldView.LayoutString(PDFBox, fieldKey); } @@ -49,21 +49,22 @@ export class PDFBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps private _sidebarRef = React.createRef<SidebarAnnos>(); @observable private _searching: boolean = false; - @observable private _pdf: Opt<Pdfjs.PDFDocumentProxy>; + @observable private _pdf: Opt<Pdfjs.PDFDocumentProxy> = undefined; @observable private _pageControls = false; @computed get pdfUrl() { - return Cast(this.dataDoc[this.props.fieldKey], PdfField); + return Cast(this.dataDoc[this._props.fieldKey], PdfField); } @computed get pdfThumb() { return ImageCast(this.layoutDoc['thumb-frozen'], ImageCast(this.layoutDoc.thumb))?.url; } - constructor(props: any) { + constructor(props: FieldViewProps) { super(props); + makeObservable(this); const nw = Doc.NativeWidth(this.Document, this.dataDoc) || 927; const nh = Doc.NativeHeight(this.Document, this.dataDoc) || 1200; - !this.Document._layout_fitWidth && (this.Document._height = this.Document[Width]() * (nh / nw)); + !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))); @@ -97,33 +98,34 @@ export class PDFBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps crop = (region: Doc | undefined, addCrop?: boolean) => { if (!region) return; const cropping = Doc.MakeCopy(region, true); - Doc.GetProto(region).lockedPosition = true; - Doc.GetProto(region).title = 'region:' + this.rootDoc.title; - Doc.GetProto(region).followLinkToggle = true; + const regionData = region[DocData]; + regionData.lockedPosition = true; + regionData.title = 'region:' + this.Document.title; + regionData.followLinkToggle = true; this.addDocument(region); - const docViewContent = this.props.docViewPath().lastElement().ContentDiv!; + const docViewContent = this.DocumentView?.().ContentDiv!; const newDiv = docViewContent.cloneNode(true) as HTMLDivElement; - newDiv.style.width = this.layoutDoc[Width]().toString(); - newDiv.style.height = this.layoutDoc[Height]().toString(); + newDiv.style.width = NumCast(this.layoutDoc._width).toString(); + newDiv.style.height = NumCast(this.layoutDoc._height).toString(); 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 anchw = cropping[Width]() * (this.props.NativeDimScaling?.() || 1); - const anchh = cropping[Height]() * (this.props.NativeDimScaling?.() || 1); + const anchw = NumCast(cropping._width) * (this._props.NativeDimScaling?.() || 1); + const anchh = NumCast(cropping._height) * (this._props.NativeDimScaling?.() || 1); const viewScale = 1; - cropping.title = 'crop: ' + this.rootDoc.title; - cropping.x = NumCast(this.rootDoc.x) + NumCast(this.rootDoc._width); - cropping.y = NumCast(this.rootDoc.y); + cropping.title = 'crop: ' + this.Document.title; + cropping.x = NumCast(this.Document.x) + NumCast(this.layoutDoc._width); + cropping.y = NumCast(this.Document.y); cropping._width = anchw; cropping._height = anchh; cropping.onClick = undefined; - const croppingProto = Doc.GetProto(cropping); + const croppingProto = cropping[DocData]; croppingProto.annotationOn = undefined; croppingProto.isDataDoc = true; - croppingProto.proto = Cast(this.rootDoc.proto, Doc, null)?.proto; // set proto of cropping's data doc to be IMAGE_PROTO + 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')); @@ -132,7 +134,7 @@ export class PDFBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps if (addCrop) { DocUtils.MakeLink(region, cropping, { link_relationship: 'cropped image' }); } - this.props.bringToFront(cropping); + this._props.bringToFront?.(cropping); CreateImage( '', @@ -140,8 +142,8 @@ export class PDFBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps htmlString, anchw, anchh, - (NumCast(region.y) * this.props.PanelWidth()) / NumCast(this.rootDoc[this.fieldKey + '_nativeWidth']), - (NumCast(region.x) * this.props.PanelWidth()) / NumCast(this.rootDoc[this.fieldKey + '_nativeWidth']), + (NumCast(region.y) * this._props.PanelWidth()) / NumCast(this.dataDoc[this.fieldKey + '_nativeWidth']), + (NumCast(region.x) * this._props.PanelWidth()) / NumCast(this.dataDoc[this.fieldKey + '_nativeWidth']), 4 ) .then((data_url: any) => { @@ -163,18 +165,18 @@ export class PDFBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps updateIcon = () => { // currently we render pdf icons as text labels - const docViewContent = this.props.docViewPath().lastElement().ContentDiv!; + const docViewContent = this.DocumentView?.().ContentDiv!; const filename = this.layoutDoc[Id] + '-icon' + new Date().getTime(); this._pdfViewer?._mainCont.current && CollectionFreeFormView.UpdateIcon( filename, docViewContent, - this.layoutDoc[Width](), - this.layoutDoc[Height](), - this.props.PanelWidth(), - this.props.PanelHeight(), + NumCast(this.layoutDoc._width), + NumCast(this.layoutDoc._height), + this._props.PanelWidth(), + this._props.PanelHeight(), NumCast(this.layoutDoc._layout_scrollTop), - NumCast(this.rootDoc[this.fieldKey + '_nativeHeight'], 1), + NumCast(this.dataDoc[this.fieldKey + '_nativeHeight'], 1), true, this.layoutDoc[Id] + '-icon', (iconFile: string, nativeWidth: number, nativeHeight: number) => { @@ -191,43 +193,44 @@ export class PDFBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps Object.values(this._disposers).forEach(disposer => disposer?.()); } componentDidMount() { - this.props.setContentView?.(this); + this._props.setContentViewBox?.(this); this._disposers.select = reaction( - () => this.props.isSelected(), + () => this._props.isSelected(), () => { document.removeEventListener('keydown', this.onKeyDown); - this.props.isSelected(true) && document.addEventListener('keydown', this.onKeyDown); + this._props.isSelected() && document.addEventListener('keydown', this.onKeyDown); }, { fireImmediately: true } ); this._disposers.scroll = reaction( - () => this.rootDoc.layout_scrollTop, + () => this.layoutDoc.layout_scrollTop, () => { - if (!(ComputedField.WithoutComputed(() => FieldValue(this.props.Document[this.SidebarKey + '_panY'])) instanceof ComputedField)) { - this.props.Document[this.SidebarKey + '_panY'] = ComputedField.MakeFunction('this.layout_scrollTop'); + if (!(ComputedField.WithoutComputed(() => FieldValue(this.Document[this.SidebarKey + '_panY'])) instanceof ComputedField)) { + this.Document[this.SidebarKey + '_panY'] = ComputedField.MakeFunction('this.layout_scrollTop'); } - this.props.Document[this.SidebarKey + '_freeform_scale'] = 1; - this.props.Document[this.SidebarKey + '_freeform_panX'] = 0; + this.layoutDoc[this.SidebarKey + '_freeform_scale'] = 1; + this.layoutDoc[this.SidebarKey + '_freeform_panX'] = 0; } ); } - brushView = (view: { width: number; height: number; panX: number; panY: number }, transTime: number) => this._pdfViewer?.brushView(view, transTime); - sidebarAddDocTab = (doc: Doc, where: OpenWhere) => { - if (DocListCast(this.props.Document[this.props.fieldKey + '_sidebar']).includes(doc) && !this.SidebarShown) { + if (DocListCast(this.Document[this._props.fieldKey + '_sidebar']).includes(doc) && !this.SidebarShown) { this.toggleSidebar(false); return true; } - return this.props.addDocTab(doc, where); + return this._props.addDocTab(doc, where); }; - focus = (anchor: Doc, options: DocFocusOptions) => { + focus = (anchor: Doc, options: FocusViewOptions) => { this._initialScrollTarget = anchor; return this._pdfViewer?.scrollFocus(anchor, NumCast(anchor.y, NumCast(anchor.config_scrollTop)), options); }; - getView = async (doc: Doc) => { - if (this._sidebarRef?.current?.makeDocUnfiltered(doc) && !this.SidebarShown) this.toggleSidebar(false); + getView = async (doc: Doc, options: FocusViewOptions) => { + if (this._sidebarRef?.current?.makeDocUnfiltered(doc) && !this.SidebarShown) { + options.didMove = true; + this.toggleSidebar(false); + } return new Promise<Opt<DocumentView>>(res => DocumentManager.Instance.AddViewRenderedCb(doc, dv => res(dv))); }; @@ -239,12 +242,12 @@ export class PDFBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps } const docAnchor = () => Docs.Create.ConfigDocument({ - title: StrCast(this.rootDoc.title + '@' + NumCast(this.layoutDoc._layout_scrollTop)?.toFixed(0)), - annotationOn: this.rootDoc, + title: StrCast(this.Document.title + '@' + NumCast(this.layoutDoc._layout_scrollTop)?.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.rootDoc); + PresBox.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); @@ -254,11 +257,11 @@ export class PDFBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps @action loaded = (nw: number, nh: number, np: number) => { - this.dataDoc[this.props.fieldKey + '_numPages'] = np; + this.dataDoc[this._props.fieldKey + '_numPages'] = np; Doc.SetNativeWidth(this.dataDoc, Math.max(Doc.NativeWidth(this.dataDoc), (nw * 96) / 72)); Doc.SetNativeHeight(this.dataDoc, (nh * 96) / 72); - this.layoutDoc._height = this.layoutDoc[Width]() / (Doc.NativeAspect(this.dataDoc) || 1); - !this.Document._layout_fitWidth && (this.Document._height = this.Document[Width]() * (nh / nw)); + this.layoutDoc._height = NumCast(this.layoutDoc._width) / (Doc.NativeAspect(this.dataDoc) || 1); + !this.Document._layout_fitWidth && (this.Document._height = NumCast(this.Document._width) * (nh / nw)); }; public search = action((searchString: string, bwd?: boolean, clear: boolean = false) => { @@ -279,7 +282,7 @@ export class PDFBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps return true; }; public forwardPage = () => { - this.Document._layout_curPage = Math.min(NumCast(this.dataDoc[this.props.fieldKey + '_numPages']), (NumCast(this.Document._layout_curPage) || 1) + 1); + this.Document._layout_curPage = Math.min(NumCast(this.dataDoc[this._props.fieldKey + '_numPages']), (NumCast(this.Document._layout_curPage) || 1) + 1); return true; }; public gotoPage = (p: number) => (this.Document._layout_curPage = p); @@ -303,7 +306,7 @@ export class PDFBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps setPdfViewer = (pdfViewer: PDFViewer) => { this._pdfViewer = pdfViewer; - const docView = this.props.DocumentView?.(); + const docView = this.DocumentView?.(); if (this._initialScrollTarget && docView) { this.focus(this._initialScrollTarget, { instant: true }); this._initialScrollTarget = undefined; @@ -324,16 +327,16 @@ export class PDFBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps this, e, (e, down, delta) => { - const localDelta = this.props + const localDelta = this._props .ScreenToLocalTransform() - .scale(this.props.NativeDimScaling?.() || 1) + .scale(this._props.NativeDimScaling?.() || 1) .transformDirection(delta[0], delta[1]); const nativeWidth = NumCast(this.layoutDoc[this.fieldKey + '_nativeWidth']); const curNativeWidth = NumCast(this.layoutDoc.nativeWidth, nativeWidth); - const ratio = (curNativeWidth + ((onButton ? 1 : -1) * localDelta[0]) / (this.props.NativeDimScaling?.() || 1)) / nativeWidth; + const ratio = (curNativeWidth + ((onButton ? 1 : -1) * localDelta[0]) / (this._props.NativeDimScaling?.() || 1)) / nativeWidth; if (ratio >= 1) { this.layoutDoc.nativeWidth = nativeWidth * ratio; - onButton && (this.layoutDoc._width = this.layoutDoc[Width]() + localDelta[0]); + onButton && (this.layoutDoc._width = NumCast(this.layoutDoc._width) + localDelta[0]); this.layoutDoc._show_sidebar = nativeWidth !== this.layoutDoc._nativeWidth; } return false; @@ -350,15 +353,15 @@ export class PDFBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps toggleSidebar = action((preview: boolean = false) => { const nativeWidth = NumCast(this.layoutDoc[this.fieldKey + '_nativeWidth']); const sideratio = ((!this.layoutDoc.nativeWidth || this.layoutDoc.nativeWidth === nativeWidth ? PDFBox.openSidebarWidth : 0) + nativeWidth) / nativeWidth; - const pdfratio = ((!this.layoutDoc.nativeWidth || this.layoutDoc.nativeWidth === nativeWidth ? PDFBox.openSidebarWidth + PDFBox.sidebarResizerWidth : 0) + nativeWidth) / nativeWidth; + const pdfratio = ((!this.layoutDoc.nativeWidth || this.layoutDoc.nativeWidth === nativeWidth ? PDFBox.openSidebarWidth + PDFBox.sidebarResizerWidth : 0) + NumCast(this.layoutDoc._width)) / NumCast(this.layoutDoc._width); const curNativeWidth = NumCast(this.layoutDoc.nativeWidth, nativeWidth); if (preview) { this._previewNativeWidth = nativeWidth * sideratio; - this._previewWidth = (this.layoutDoc[Width]() * nativeWidth * sideratio) / curNativeWidth; + this._previewWidth = (NumCast(this.layoutDoc._width) * nativeWidth * sideratio) / curNativeWidth; this._showSidebar = true; } else { this.layoutDoc.nativeWidth = nativeWidth * pdfratio; - this.layoutDoc._width = (this.layoutDoc[Width]() * nativeWidth * pdfratio) / curNativeWidth; + this.layoutDoc._width = (NumCast(this.layoutDoc._width) * nativeWidth * pdfratio) / curNativeWidth; this.layoutDoc._show_sidebar = nativeWidth !== this.layoutDoc._nativeWidth; } }); @@ -375,12 +378,12 @@ export class PDFBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps ); const searchTitle = `${!this._searching ? 'Open' : 'Close'} Search Bar`; const curPage = NumCast(this.Document._layout_curPage) || 1; - return !this.props.isContentActive() || this._pdfViewer?.isAnnotating ? null : ( + return !this._props.isContentActive() || this._pdfViewer?.isAnnotating ? null : ( <div className="pdfBox-ui" onKeyDown={e => ([KeyCodes.BACKSPACE, KeyCodes.DELETE].includes(e.keyCode) ? e.stopPropagation() : true)} onPointerDown={e => e.stopPropagation()} - style={{ display: this.props.isContentActive() ? 'flex' : 'none' }}> + style={{ display: this._props.isContentActive() ? 'flex' : 'none' }}> <div className="pdfBox-overlayCont" onPointerDown={e => e.stopPropagation()} style={{ left: `${this._searching ? 0 : 100}%` }}> <button className="pdfBox-overlayButton" title={searchTitle} /> <input @@ -434,10 +437,10 @@ export class PDFBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps if (!this.SidebarShown) return 0; if (this._previewWidth) return PDFBox.sidebarResizerWidth + PDFBox.openSidebarWidth; // return default sidebar if previewing (as in viewing a link target) const nativeDiff = NumCast(this.layoutDoc.nativeWidth) - Doc.NativeWidth(this.dataDoc); - return PDFBox.sidebarResizerWidth + nativeDiff * (this.props.NativeDimScaling?.() || 1); + return PDFBox.sidebarResizerWidth + nativeDiff * (this._props.NativeDimScaling?.() || 1); }; @undoBatch - toggleSidebarType = () => (this.rootDoc[this.SidebarKey + '_type_collection'] = this.rootDoc[this.SidebarKey + '_type_collection'] === CollectionViewType.Freeform ? CollectionViewType.Stacking : CollectionViewType.Freeform); + toggleSidebarType = () => (this.dataDoc[this.SidebarKey + '_type_collection'] = this.dataDoc[this.SidebarKey + '_type_collection'] === CollectionViewType.Freeform ? CollectionViewType.Stacking : CollectionViewType.Freeform); specificContextMenu = (e: React.MouseEvent): void => { const cm = ContextMenu.Instance; const options = cm.findByDescription('Options...'); @@ -453,11 +456,11 @@ export class PDFBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps }; @computed get renderTitleBox() { - const classname = 'pdfBox' + (this.props.isContentActive() ? '-interactive' : ''); + const classname = 'pdfBox' + (this._props.isContentActive() ? '-interactive' : ''); return ( <div className={classname}> <div className="pdfBox-title-outer"> - <strong className="pdfBox-title">{StrCast(this.props.Document.title)}</strong> + <strong className="pdfBox-title">{StrCast(this.Document.title)}</strong> </div> </div> ); @@ -475,8 +478,8 @@ export class PDFBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps key="sidebar" title="Toggle Sidebar" style={{ - display: !this.props.isContentActive() ? 'none' : undefined, - top: StrCast(this.rootDoc._layout_showTitle) === 'title' ? 20 : 5, + display: !this._props.isContentActive() ? 'none' : undefined, + top: StrCast(this.layoutDoc._layout_showTitle) === 'title' ? 20 : 5, backgroundColor: this.SidebarShown ? Colors.MEDIUM_BLUE : Colors.BLACK, }} onPointerDown={e => this.sidebarBtnDown(e, true)}> @@ -492,27 +495,27 @@ export class PDFBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps const pdfNativeWidth = NumCast(this.layoutDoc[this.fieldKey + '_nativeWidth']); const nativeWidth = NumCast(this.layoutDoc.nativeWidth, pdfNativeWidth); const pdfRatio = pdfNativeWidth / nativeWidth; - return (pdfRatio * this.props.PanelWidth()) / pdfNativeWidth; + return (pdfRatio * this._props.PanelWidth()) / pdfNativeWidth; } @computed get sidebarNativeWidth() { return this.sidebarWidth() / this.pdfScale; } @computed get sidebarNativeHeight() { - return this.props.PanelHeight() / this.pdfScale; + return this._props.PanelHeight() / this.pdfScale; } sidebarNativeWidthFunc = () => this.sidebarNativeWidth; sidebarNativeHeightFunc = () => this.sidebarNativeHeight; sidebarMoveDocument = (doc: Doc | Doc[], targetCollection: Doc | undefined, addDocument: (doc: Doc | Doc[]) => boolean) => this.moveDocument(doc, targetCollection, addDocument, this.SidebarKey); sidebarRemDocument = (doc: Doc | Doc[]) => this.removeDocument(doc, this.SidebarKey); - sidebarScreenToLocal = () => this.props.ScreenToLocalTransform().translate((this.sidebarWidth() - this.props.PanelWidth()) / this.pdfScale, 0); + sidebarScreenToLocal = () => this.ScreenToLocalBoxXf().translate((this.sidebarWidth() - this._props.PanelWidth()) / this.pdfScale, 0); @computed get sidebarCollection() { const renderComponent = (tag: string) => { const ComponentTag = tag === CollectionViewType.Freeform ? CollectionFreeFormView : CollectionStackingView; return ComponentTag === CollectionStackingView ? ( <SidebarAnnos ref={this._sidebarRef} - {...this.props} - rootDoc={this.rootDoc} + {...this._props} + Document={this.Document} layoutDoc={this.layoutDoc} dataDoc={this.dataDoc} setHeight={emptyFunction} @@ -524,13 +527,13 @@ export class PDFBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps removeDocument={this.removeDocument} /> ) : ( - <div onPointerDown={e => setupMoveUpEvents(this, e, returnFalse, emptyFunction, () => SelectionManager.SelectView(this.props.DocumentView?.()!, false), true)}> + <div onPointerDown={e => setupMoveUpEvents(this, e, returnFalse, emptyFunction, () => SelectionManager.SelectView(this.DocumentView?.()!, false), true)}> <ComponentTag - {...this.props} - setContentView={emptyFunction} // override setContentView to do nothing + {...this._props} + setContentViewBox={emptyFunction} // override setContentView to do nothing NativeWidth={this.sidebarNativeWidthFunc} NativeHeight={this.sidebarNativeHeightFunc} - PanelHeight={this.props.PanelHeight} + PanelHeight={this._props.PanelHeight} PanelWidth={this.sidebarWidth} xPadding={0} yPadding={0} @@ -544,7 +547,7 @@ export class PDFBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps moveDocument={this.sidebarMoveDocument} addDocument={this.sidebarAddDocument} ScreenToLocalTransform={this.sidebarScreenToLocal} - renderDepth={this.props.renderDepth + 1} + renderDepth={this._props.renderDepth + 1} noSidebar={true} fieldKey={this.SidebarKey} /> @@ -560,13 +563,13 @@ export class PDFBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps @computed get renderPdfView() { TraceMobx(); const previewScale = this._previewNativeWidth ? 1 - this.sidebarWidth() / this._previewNativeWidth : 1; - const scale = previewScale * (this.props.NativeDimScaling?.() || 1); + const scale = previewScale * (this._props.NativeDimScaling?.() || 1); return !this._pdf ? null : ( <div className="pdfBox" onContextMenu={this.specificContextMenu} style={{ - height: this.props.Document._layout_scrollTop && !this.Document._layout_fitWidth && window.screen.width > 600 ? (NumCast(this.Document._height) * this.props.PanelWidth()) / NumCast(this.Document._width) : undefined, + height: this.Document._layout_scrollTop && !this.Document._layout_fitWidth && window.screen.width > 600 ? (NumCast(this.Document._height) * this._props.PanelWidth()) / NumCast(this.Document._width) : undefined, }}> <div className="pdfBox-background" onPointerDown={e => this.sidebarBtnDown(e, false)} /> <div @@ -579,9 +582,9 @@ export class PDFBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps top: 0, }}> <PDFViewer - {...this.props} + {...this._props} + pdfBox={this} sidebarAddDoc={this.sidebarAddDocument} - rootDoc={this.rootDoc} addDocTab={this.sidebarAddDocTab} layoutDoc={this.layoutDoc} dataDoc={this.dataDoc} @@ -598,7 +601,7 @@ export class PDFBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps crop={this.crop} /> </div> - <div style={{ position: 'absolute', height: '100%', right: 0, top: 0, width: `calc(100 * ${this.sidebarWidth() / this.props.PanelWidth()}%` }}>{this.sidebarCollection}</div> + <div style={{ position: 'absolute', height: '100%', right: 0, top: 0, width: `calc(100 * ${this.sidebarWidth() / this._props.PanelWidth()}%` }}>{this.sidebarCollection}</div> {this.settingsPanel()} </div> ); @@ -608,8 +611,7 @@ export class PDFBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps static pdfpromise = new Map<string, Promise<Pdfjs.PDFDocumentProxy>>(); render() { TraceMobx(); - const pdfView = this.renderPdfView; - + 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)))); diff --git a/src/client/views/nodes/PhysicsBox/PhysicsSimulationBox.tsx b/src/client/views/nodes/PhysicsBox/PhysicsSimulationBox.tsx index cd1ff17dd..ae674d604 100644 --- a/src/client/views/nodes/PhysicsBox/PhysicsSimulationBox.tsx +++ b/src/client/views/nodes/PhysicsBox/PhysicsSimulationBox.tsx @@ -6,8 +6,9 @@ import QuestionMarkIcon from '@mui/icons-material/QuestionMark'; import ReplayIcon from '@mui/icons-material/Replay'; import { Box, Button, Checkbox, Dialog, DialogActions, DialogContent, DialogContentText, DialogTitle, FormControl, FormControlLabel, FormGroup, IconButton, LinearProgress, Stack } from '@mui/material'; import Typography from '@mui/material/Typography'; -import { action, computed, IReactionDisposer, observable, reaction } from 'mobx'; +import { IReactionDisposer, action, computed, observable, reaction } from 'mobx'; import { observer } from 'mobx-react'; +import * as React from 'react'; import { NumListCast } from '../../../../fields/Doc'; import { List } from '../../../../fields/List'; import { BoolCast, NumCast, StrCast } from '../../../../fields/Types'; @@ -15,11 +16,10 @@ import { ViewBoxAnnotatableComponent } from '../../DocComponent'; import { FieldView, FieldViewProps } from './../FieldView'; import './PhysicsSimulationBox.scss'; import InputField from './PhysicsSimulationInputField'; -import * as questions from './PhysicsSimulationQuestions.json'; -import * as tutorials from './PhysicsSimulationTutorial.json'; +import questions from './PhysicsSimulationQuestions.json'; +import tutorials from './PhysicsSimulationTutorial.json'; import Wall from './PhysicsSimulationWall'; import Weight from './PhysicsSimulationWeight'; -import React = require('react'); interface IWallProps { length: number; @@ -81,14 +81,14 @@ export class PhysicsSimulationBox extends ViewBoxAnnotatableComponent<FieldViewP // semi-Constants xMin = 0; yMin = 0; - xMax = this.props.PanelWidth() * 0.6; - yMax = this.props.PanelHeight(); + xMax = this._props.PanelWidth() * 0.6; + yMax = this._props.PanelHeight(); color = `rgba(0,0,0,0.5)`; radius = 50; wallPositions: IWallProps[] = []; @computed get circularMotionRadius() { - return (NumCast(this.dataDoc.circularMotionRadius, 150) * this.props.PanelWidth()) / 1000; + return (NumCast(this.dataDoc.circularMotionRadius, 150) * this._props.PanelWidth()) / 1000; } @computed get gravity() { return NumCast(this.dataDoc.simulation_gravity, -9.81); @@ -191,21 +191,22 @@ export class PhysicsSimulationBox extends ViewBoxAnnotatableComponent<FieldViewP componentDidMount() { // Setup and update simulation - this._widthDisposer = reaction(() => [this.props.PanelWidth(), this.props.PanelHeight()], this.setupSimulation, { fireImmediately: true }); + this._widthDisposer = reaction(() => [this._props.PanelWidth(), this._props.PanelHeight()], this.setupSimulation, { fireImmediately: true }); // Create walls this.wallPositions = [ { length: 100, xPos: 0, yPos: 0, angleInDegrees: 0 }, { length: 100, xPos: 0, yPos: 100, angleInDegrees: 0 }, { length: 100, xPos: 0, yPos: 0, angleInDegrees: 90 }, - { length: 100, xPos: (this.xMax / this.props.PanelWidth()) * 100, yPos: 0, angleInDegrees: 90 }, + { length: 100, xPos: (this.xMax / this._props.PanelWidth()) * 100, yPos: 0, angleInDegrees: 90 }, ]; } - componentDidUpdate() { - 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(); + componentDidUpdate(prevProps: Readonly<FieldViewProps>) { + super.componentDidUpdate(prevProps); + 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(); } } @@ -631,7 +632,7 @@ export class PhysicsSimulationBox extends ViewBoxAnnotatableComponent<FieldViewP // Default setup for pendulum simulation setupPendulum = () => { - const length = (300 * this.props.PanelWidth()) / 1000; + const length = (300 * this._props.PanelWidth()) / 1000; const angle = 30; const x = length * Math.cos(((90 - angle) * Math.PI) / 180); const y = length * Math.sin(((90 - angle) * Math.PI) / 180); @@ -807,8 +808,8 @@ export class PhysicsSimulationBox extends ViewBoxAnnotatableComponent<FieldViewP const commonWeightProps = { pause: this.pause, paused: BoolCast(this.dataDoc.simulation_paused), - panelWidth: this.props.PanelWidth, - panelHeight: this.props.PanelHeight, + panelWidth: this._props.PanelWidth, + panelHeight: this._props.PanelHeight, resetRequest: this.resetRequest, xMax: this.xMax, xMin: this.xMin, @@ -852,9 +853,9 @@ export class PhysicsSimulationBox extends ViewBoxAnnotatableComponent<FieldViewP <div style={{ position: 'fixed', - left: 0.1 * this.props.PanelWidth() + 'px', - top: 0.95 * this.props.PanelHeight() + 'px', - width: 0.5 * this.props.PanelWidth() + 'px', + left: 0.1 * this._props.PanelWidth() + 'px', + top: 0.95 * this._props.PanelHeight() + 'px', + width: 0.5 * this._props.PanelWidth() + 'px', }}> <LinearProgress /> </div> @@ -922,8 +923,8 @@ export class PhysicsSimulationBox extends ViewBoxAnnotatableComponent<FieldViewP </div> <div className="mechanicsSimulationEquationContainer" - onWheel={e => this.props.isContentActive() && e.stopPropagation()} - style={{ overflow: 'auto', height: `${Math.max(1, 800 / this.props.PanelWidth()) * 100}%`, transform: `scale(${Math.min(1, this.props.PanelWidth() / 850)})` }}> + onWheel={e => this._props.isContentActive() && e.stopPropagation()} + 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' && ( diff --git a/src/client/views/nodes/PhysicsBox/PhysicsSimulationInputField.tsx b/src/client/views/nodes/PhysicsBox/PhysicsSimulationInputField.tsx index d595a499e..c704863f5 100644 --- a/src/client/views/nodes/PhysicsBox/PhysicsSimulationInputField.tsx +++ b/src/client/views/nodes/PhysicsBox/PhysicsSimulationInputField.tsx @@ -1,6 +1,6 @@ import { TextField, InputAdornment } from '@mui/material'; import { Doc } from '../../../../fields/Doc'; -import React = require('react'); +import * as React from 'react'; import TaskAltIcon from '@mui/icons-material/TaskAlt'; import ErrorOutlineIcon from '@mui/icons-material/ErrorOutline'; import { isNumber } from 'lodash'; diff --git a/src/client/views/nodes/PhysicsBox/PhysicsSimulationWall.tsx b/src/client/views/nodes/PhysicsBox/PhysicsSimulationWall.tsx index 8cc1d0fbf..696352296 100644 --- a/src/client/views/nodes/PhysicsBox/PhysicsSimulationWall.tsx +++ b/src/client/views/nodes/PhysicsBox/PhysicsSimulationWall.tsx @@ -1,34 +1,33 @@ -import React = require('react'); +import * as React from 'react'; export interface Force { - magnitude: number; - directionInDegrees: number; + magnitude: number; + directionInDegrees: number; } export interface IWallProps { - length: number; - xPos: number; - yPos: number; - angleInDegrees: number; + length: number; + xPos: number; + yPos: number; + angleInDegrees: number; } export default class Wall extends React.Component<IWallProps> { + constructor(props: any) { + super(props); + } - constructor(props: any) { - super(props) - } + wallStyle = { + width: this.props.angleInDegrees == 0 ? this.props.length + '%' : '5px', + height: this.props.angleInDegrees == 0 ? '5px' : this.props.length + '%', + position: 'absolute' as 'absolute', + left: this.props.xPos + '%', + top: this.props.yPos + '%', + backgroundColor: '#6c7b8b', + margin: 0, + padding: 0, + }; - wallStyle = { - width: this.props.angleInDegrees == 0 ? this.props.length + "%" : "5px", - height: this.props.angleInDegrees == 0 ? "5px" : this.props.length + "%", - position: "absolute" as "absolute", - left: this.props.xPos + "%", - top: this.props.yPos + "%", - backgroundColor: "#6c7b8b", - margin: 0, - padding: 0, - }; - - render () { - return (<div style={this.wallStyle}></div>); - } -}; + render() { + return <div style={this.wallStyle}></div>; + } +} diff --git a/src/client/views/nodes/PhysicsBox/PhysicsSimulationWeight.tsx b/src/client/views/nodes/PhysicsBox/PhysicsSimulationWeight.tsx index 2165c8ba9..3b232ddd0 100644 --- a/src/client/views/nodes/PhysicsBox/PhysicsSimulationWeight.tsx +++ b/src/client/views/nodes/PhysicsBox/PhysicsSimulationWeight.tsx @@ -1,7 +1,7 @@ -import { computed, IReactionDisposer, reaction } from 'mobx'; +import { computed, IReactionDisposer, makeObservable, reaction } from 'mobx'; import { observer } from 'mobx-react'; +import * as React from 'react'; import './PhysicsSimulationBox.scss'; -import React = require('react'); interface IWallProps { length: number; @@ -93,6 +93,7 @@ interface IState { export default class Weight extends React.Component<IWeightProps, IState> { constructor(props: any) { super(props); + makeObservable(this); this.state = { angleLabel: 0, clickPositionX: 0, diff --git a/src/client/views/nodes/RadialMenu.scss b/src/client/views/nodes/RadialMenu.scss index 312b51013..9cd1ee23a 100644 --- a/src/client/views/nodes/RadialMenu.scss +++ b/src/client/views/nodes/RadialMenu.scss @@ -1,4 +1,4 @@ -@import "../global/globalCssVariables"; +@import '../global/globalCssVariables.module.scss'; .radialMenu-cont { position: absolute; @@ -26,7 +26,7 @@ -moz-user-select: none; -ms-user-select: none; user-select: none; - transition: all .1s; + transition: all 0.1s; border-style: none; white-space: nowrap; font-size: 13px; @@ -34,8 +34,7 @@ text-transform: uppercase; } -s -.radialMenu-itemSelected { +s .radialMenu-itemSelected { border-style: none; } @@ -50,8 +49,8 @@ s -moz-user-select: none; -ms-user-select: none; user-select: none; - transition: all .1s; - border-width: .11px; + transition: all 0.1s; + border-width: 0.11px; border-style: none; border-color: $medium-gray; // rgb(187, 186, 186); // padding: 10px 0px 10px 0px; @@ -62,9 +61,8 @@ s padding-left: 5px; } - .radialMenu-description { margin-left: 5px; text-align: left; display: inline; //need this? -}
\ No newline at end of file +} diff --git a/src/client/views/nodes/RadialMenu.tsx b/src/client/views/nodes/RadialMenu.tsx index 7f0956e51..16450c359 100644 --- a/src/client/views/nodes/RadialMenu.tsx +++ b/src/client/views/nodes/RadialMenu.tsx @@ -1,17 +1,17 @@ -import React = require("react"); -import { action, computed, IReactionDisposer, observable, reaction, runInAction } from "mobx"; -import { observer } from "mobx-react"; -import "./RadialMenu.scss"; -import { RadialMenuItem, RadialMenuProps } from "./RadialMenuItem"; +import { action, computed, IReactionDisposer, makeObservable, observable, reaction, runInAction } from 'mobx'; +import { observer } from 'mobx-react'; +import * as React from 'react'; +import './RadialMenu.scss'; +import { RadialMenuItem, RadialMenuProps } from './RadialMenuItem'; @observer export class RadialMenu extends React.Component { static Instance: RadialMenu; static readonly buffer = 20; - constructor(props: Readonly<{}>) { + constructor(props: any) { super(props); - + makeObservable(this); RadialMenu.Instance = this; } @@ -23,11 +23,10 @@ export class RadialMenu extends React.Component { public used: boolean = false; - catchTouch = (te: React.TouchEvent) => { te.stopPropagation(); te.preventDefault(); - } + }; @action onPointerDown = (e: PointerEvent) => { @@ -35,8 +34,8 @@ export class RadialMenu extends React.Component { this._mouseX = e.clientX; this._mouseY = e.clientY; this.used = false; - document.addEventListener("pointermove", this.onPointerMove); - } + document.addEventListener('pointermove', this.onPointerMove); + }; @observable private _closest: number = -1; @@ -60,11 +59,10 @@ export class RadialMenu extends React.Component { } } this._closest = closest; - } - else { + } else { this._closest = -1; } - } + }; @action onPointerUp = (e: PointerEvent) => { this.used = true; @@ -75,43 +73,41 @@ export class RadialMenu extends React.Component { this._shouldDisplay = false; } this._shouldDisplay && (this._display = true); - document.removeEventListener("pointermove", this.onPointerMove); + document.removeEventListener('pointermove', this.onPointerMove); if (this._closest !== -1 && this._items?.length > this._closest) { this._items[this._closest].event(); } - } + }; componentWillUnmount() { - document.removeEventListener("pointerdown", this.onPointerDown); + document.removeEventListener('pointerdown', this.onPointerDown); - document.removeEventListener("pointerup", this.onPointerUp); + document.removeEventListener('pointerup', this.onPointerUp); this._reactionDisposer && this._reactionDisposer(); } @action - componentDidMount = () => { - document.addEventListener("pointerdown", this.onPointerDown); - document.addEventListener("pointerup", this.onPointerUp); + 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) + () => 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; } @@ -133,7 +129,7 @@ export class RadialMenu extends React.Component { this._mouseX = x; this._mouseY = y; this._shouldDisplay = true; - } + }; // @computed // get pageX() { // const x = this._pageX; @@ -168,7 +164,7 @@ export class RadialMenu extends React.Component { this.clearItems(); this._display = false; this._shouldDisplay = false; - } + }; @action openMenu = (x: number, y: number) => { @@ -176,56 +172,52 @@ export class RadialMenu extends React.Component { this._pageY = y; this._shouldDisplay; this._display = true; - } + }; @action clearItems() { this._items = []; } - previewcircle() { - if (document.getElementById("newCanvas") !== null) { - const c: any = document.getElementById("newCanvas"); + if (document.getElementById('newCanvas') !== null) { + const c: any = document.getElementById('newCanvas'); if (c.getContext) { - const ctx = c.getContext("2d"); + const ctx = c.getContext('2d'); ctx.beginPath(); ctx.arc(150, 150, 50, 0, 2 * Math.PI); - ctx.fillStyle = "white"; + ctx.fillStyle = 'white'; ctx.fill(); - ctx.font = "12px Arial"; - ctx.fillStyle = "black"; - ctx.textAlign = "center"; - let description = ""; + ctx.font = '12px Arial'; + ctx.fillStyle = 'black'; + ctx.textAlign = 'center'; + let description = ''; if (this._closest !== -1) { description = this._items[this._closest].description; } if (description.length > 15) { description = description.slice(0, 12); - description += "..."; + description += '...'; } ctx.fillText(description, 150, 150, 90); } } } - render() { if (!this._display) { return null; } - const style = this._yRelativeToTop ? { left: this._pageX - 130, top: this._pageY - 130 } : - { left: this._pageX - 130, top: this._pageY - 130 }; + const style = this._yRelativeToTop ? { left: this._pageX - 130, top: this._pageY - 130 } : { left: this._pageX - 130, top: this._pageY - 130 }; return ( - <div className="radialMenu-cont" onTouchStart={this.catchTouch} style={style}> - <canvas id="newCanvas" style={{ position: "absolute" }} height="300" width="300"> Your browser does not support the HTML5 canvas tag.</canvas> + <canvas id="newCanvas" style={{ position: 'absolute' }} height="300" width="300"> + {' '} + Your browser does not support the HTML5 canvas tag. + </canvas> {this.menuItems} </div> - ); } - - -}
\ No newline at end of file +} diff --git a/src/client/views/nodes/RadialMenuItem.tsx b/src/client/views/nodes/RadialMenuItem.tsx index 8876b4879..91dc37d34 100644 --- a/src/client/views/nodes/RadialMenuItem.tsx +++ b/src/client/views/nodes/RadialMenuItem.tsx @@ -1,8 +1,8 @@ -import React = require("react"); import { IconProp } from '@fortawesome/fontawesome-svg-core'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { observer } from "mobx-react"; -import { UndoManager } from "../../util/UndoManager"; +import { observer } from 'mobx-react'; +import * as React from 'react'; +import { UndoManager } from '../../util/UndoManager'; export interface RadialMenuProps { description: string; @@ -15,15 +15,13 @@ export interface RadialMenuProps { selected: number; } - @observer export class RadialMenuItem extends React.Component<RadialMenuProps> { - - componentDidMount = () => { + componentDidMount() { this.setcircle(); } - componentDidUpdate = () => { + componentDidUpdate() { this.setcircle(); } @@ -35,38 +33,36 @@ export class RadialMenuItem extends React.Component<RadialMenuProps> { } await this.props.event({ x: e.clientX, y: e.clientY }); batch && batch.end(); - } - + }; setcircle() { let circlemin = 0; let circlemax = 1; - this.props.min ? circlemin = this.props.min : null; - this.props.max ? circlemax = this.props.max : null; - if (document.getElementById("myCanvas") !== null) { - const c: any = document.getElementById("myCanvas"); - let color = "white"; + this.props.min ? (circlemin = this.props.min) : null; + this.props.max ? (circlemax = this.props.max) : null; + if (document.getElementById('myCanvas') !== null) { + const c: any = document.getElementById('myCanvas'); + let color = 'white'; switch (circlemin % 3) { case 1: - color = "#c2c2c5"; + color = '#c2c2c5'; break; case 0: - color = "#f1efeb"; + color = '#f1efeb'; break; case 2: - color = "lightgray"; + color = 'lightgray'; break; } if (circlemax % 3 === 1 && circlemin === circlemax - 1) { - color = "#c2c2c5"; + color = '#c2c2c5'; } if (this.props.selected === this.props.min) { - color = "#808080"; - + color = '#808080'; } if (c.getContext) { - const ctx = c.getContext("2d"); + const ctx = c.getContext('2d'); ctx.beginPath(); ctx.arc(150, 150, 150, (circlemin / circlemax) * 2 * Math.PI, ((circlemin + 1) / circlemax) * 2 * Math.PI); ctx.arc(150, 150, 50, ((circlemin + 1) / circlemax) * 2 * Math.PI, (circlemin / circlemax) * 2 * Math.PI, true); @@ -79,35 +75,36 @@ export class RadialMenuItem extends React.Component<RadialMenuProps> { calculatorx() { let circlemin = 0; let circlemax = 1; - this.props.min ? circlemin = this.props.min : null; - this.props.max ? circlemax = this.props.max : null; - const avg = ((circlemin / circlemax) + ((circlemin + 1) / circlemax)) / 2; + this.props.min ? (circlemin = this.props.min) : null; + this.props.max ? (circlemax = this.props.max) : null; + 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); + const x = 100 * Math.cos((degrees * Math.PI) / 180); + const y = -125 * Math.sin((degrees * Math.PI) / 180); return x; } calculatory() { - let circlemin = 0; let circlemax = 1; - this.props.min ? circlemin = this.props.min : null; - this.props.max ? circlemax = this.props.max : null; - const avg = ((circlemin / circlemax) + ((circlemin + 1) / circlemax)) / 2; + this.props.min ? (circlemin = this.props.min) : null; + 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); + const x = 125 * Math.cos((degrees * Math.PI) / 180); + const y = -100 * Math.sin((degrees * Math.PI) / 180); return y; } - render() { return ( - <div className={"radialMenu-item" + (this.props.selected ? " radialMenu-itemSelected" : "")} onPointerUp={this.handleEvent}> - <canvas id="myCanvas" height="300" width="300"> Your browser does not support the HTML5 canvas tag.</canvas> - <FontAwesomeIcon icon={this.props.icon} size="3x" style={{ position: "absolute", left: this.calculatorx() + 150 - 19, top: this.calculatory() + 150 - 19 }} /> + <div className={'radialMenu-item' + (this.props.selected ? ' radialMenu-itemSelected' : '')} onPointerUp={this.handleEvent}> + <canvas id="myCanvas" height="300" width="300"> + {' '} + Your browser does not support the HTML5 canvas tag. + </canvas> + <FontAwesomeIcon icon={this.props.icon} size="3x" style={{ position: 'absolute', left: this.calculatorx() + 150 - 19, top: this.calculatory() + 150 - 19 }} /> </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 116069cbd..f6d94ce05 100644 --- a/src/client/views/nodes/RecordingBox/RecordingBox.tsx +++ b/src/client/views/nodes/RecordingBox/RecordingBox.tsx @@ -1,4 +1,4 @@ -import { action, observable } from 'mobx'; +import { action, makeObservable, observable } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; import { DateField } from '../../../../fields/DateField'; @@ -8,20 +8,20 @@ import { List } from '../../../../fields/List'; import { BoolCast, DocCast } from '../../../../fields/Types'; import { VideoField } from '../../../../fields/URLField'; import { Upload } from '../../../../server/SharedMediaTypes'; -import { Docs } from '../../../documents/Documents'; import { DocumentType } from '../../../documents/DocumentTypes'; +import { Docs } from '../../../documents/Documents'; import { DocumentManager } from '../../../util/DocumentManager'; import { DragManager } from '../../../util/DragManager'; import { ScriptingGlobals } from '../../../util/ScriptingGlobals'; -import { SelectionManager } from '../../../util/SelectionManager'; import { Presentation } from '../../../util/TrackMovements'; import { undoBatch } from '../../../util/UndoManager'; -import { CollectionFreeFormView } from '../../collections/collectionFreeForm/CollectionFreeFormView'; import { ViewBoxBaseComponent } from '../../DocComponent'; +import { CollectionFreeFormView } from '../../collections/collectionFreeForm/CollectionFreeFormView'; import { media_state } from '../AudioBox'; 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>() { @@ -31,8 +31,13 @@ export class RecordingBox extends ViewBoxBaseComponent<FieldViewProps>() { private _ref: React.RefObject<HTMLDivElement> = React.createRef(); + constructor(props: FieldViewProps) { + super(props); + makeObservable(this); + } + componentDidMount() { - this.props.setContentView?.(this); + this._props.setContentViewBox?.(this); Doc.SetNativeWidth(this.dataDoc, 1280); Doc.SetNativeHeight(this.dataDoc, 720); } @@ -50,7 +55,7 @@ export class RecordingBox extends ViewBoxBaseComponent<FieldViewProps>() { 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); + this.dataDoc[this._props.fieldKey] = new VideoField(this.result.accessPaths.client); this.dataDoc[this.fieldKey + '_recorded'] = true; // stringify the presentation and store it if (presentation?.movements) { @@ -62,7 +67,7 @@ export class RecordingBox extends ViewBoxBaseComponent<FieldViewProps>() { @undoBatch @action public static WorkspaceStopRecording() { - const remDoc = RecordingBox.screengrabber?.rootDoc; + 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 RecordingBox.screengrabber?.Pause?.(); @@ -101,7 +106,7 @@ export class RecordingBox extends ViewBoxBaseComponent<FieldViewProps>() { }); screengrabber.overlayX = 70; //was -400 screengrabber.overlayY = 590; //was 0 - Doc.GetProto(screengrabber)[Doc.LayoutFieldKey(screengrabber) + '_trackScreen'] = true; + screengrabber[DocData][Doc.LayoutFieldKey(screengrabber) + '_trackScreen'] = true; Doc.AddToMyOverlay(screengrabber); //just adds doc to overlay DocumentManager.Instance.AddViewRenderedCb(screengrabber, docView => { RecordingBox.screengrabber = docView.ComponentView as RecordingBox; @@ -122,7 +127,7 @@ export class RecordingBox extends ViewBoxBaseComponent<FieldViewProps>() { value.overlayY = window.innerHeight - 180; Doc.AddToMyOverlay(value); DocumentManager.Instance.AddViewRenderedCb(value, docView => { - Doc.UserDoc().currentRecording = docView.rootDoc; + Doc.UserDoc().currentRecording = docView.Document; docView.select(false); RecordingBox.resumeWorkspaceReplaying(value); }); @@ -136,9 +141,9 @@ export class RecordingBox extends ViewBoxBaseComponent<FieldViewProps>() { @action public static addRecToWorkspace(value: RecordingBox) { let ffView = Array.from(DocumentManager.Instance.DocumentViews).find(view => view.ComponentView instanceof CollectionFreeFormView); - (ffView?.ComponentView as CollectionFreeFormView).props.addDocument?.(value.rootDoc); - Doc.RemoveDocFromList(Doc.UserDoc(), 'workspaceRecordings', value.rootDoc); - Doc.RemFromMyOverlay(value.rootDoc); + (ffView?.ComponentView as CollectionFreeFormView)._props.addDocument?.(value.Document); + Doc.RemoveDocFromList(Doc.UserDoc(), 'workspaceRecordings', value.Document); + Doc.RemFromMyOverlay(value.Document); Doc.UserDoc().currentRecording = undefined; Doc.UserDoc().workspaceReplayingState = undefined; Doc.UserDoc().workspaceRecordingState = undefined; @@ -193,14 +198,14 @@ export class RecordingBox extends ViewBoxBaseComponent<FieldViewProps>() { render() { return ( - <div className="recordingBox" ref={this._ref}> + <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.rootDoc.proto)?.[Id] || ''} + id={DocCast(this.Document.proto)?.[Id] || ''} /> )} </div> diff --git a/src/client/views/nodes/ScreenshotBox.tsx b/src/client/views/nodes/ScreenshotBox.tsx index ebb8a3374..1e3933ac3 100644 --- a/src/client/views/nodes/ScreenshotBox.tsx +++ b/src/client/views/nodes/ScreenshotBox.tsx @@ -1,12 +1,11 @@ -import React = require('react'); import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import * as React from 'react'; // import { Canvas } from '@react-three/fiber'; -import { computed, observable, runInAction } from 'mobx'; +import { computed, makeObservable, observable, runInAction } from 'mobx'; import { observer } from 'mobx-react'; // import { BufferAttribute, Camera, Vector2, Vector3 } from 'three'; import { DateField } from '../../../fields/DateField'; import { Doc } from '../../../fields/Doc'; -import { Height, Width } from '../../../fields/DocSymbols'; import { Id } from '../../../fields/FieldSymbols'; import { ComputedField } from '../../../fields/ScriptField'; import { Cast, DocCast, NumCast } from '../../../fields/Types'; @@ -18,16 +17,17 @@ import { DocumentType } from '../../documents/DocumentTypes'; import { Networking } from '../../Network'; 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, ViewBoxAnnotatableProps } from '../DocComponent'; +import { ViewBoxAnnotatableComponent } from '../DocComponent'; +import { media_state } from './AudioBox'; import { FieldView, FieldViewProps } from './FieldView'; import { FormattedTextBox } from './formattedText/FormattedTextBox'; import './ScreenshotBox.scss'; import { VideoBox } from './VideoBox'; -import { TrackMovements } from '../../util/TrackMovements'; -import { media_state } from './AudioBox'; +import { DocData } from '../../../fields/DocSymbols'; declare class MediaRecorder { constructor(e: any, options?: any); // whatever MediaRecorder has @@ -38,18 +38,18 @@ declare class MediaRecorder { // setRaised: (r: { coord: Vector2, off: Vector3 }[]) => void; // x: number; // y: number; -// rootDoc: Doc; +// doc: Doc; // color: string; // } // @observer // export class VideoTile extends React.Component<VideoTileProps> { -// @observable _videoRef: HTMLVideoElement | undefined; +// @observable _videoRef: HTMLVideoElement | undefined = undefined; // _mesh: any = undefined; // render() { -// const topLeft = [this.props.x, this.props.y]; -// const raised = this.props.raised; +// const topLeft = [this._props.x, this._props.y]; +// const raised = this._props.raised; // const find = (raised: { coord: Vector2, off: Vector3 }[], what: Vector2) => raised.find(r => r.coord.x === what.x && r.coord.y === what.y); // const tl1 = find(raised, new Vector2(topLeft[0], topLeft[1] + 1)); // const tl2 = find(raised, new Vector2(topLeft[0] + 1, topLeft[1] + 1)); @@ -70,11 +70,11 @@ declare class MediaRecorder { // const normals = new Float32Array(quad_normals); // const uvs = new Float32Array(quad_uvs); // Each vertex has one uv coordinate for texture mapping // const indices = new Uint32Array(quad_indices); // Use the four vertices to draw the two triangles that make up the square. -// const popOut = () => NumCast(this.props.rootDoc.popOut); -// const popOff = () => NumCast(this.props.rootDoc.popOff); +// const popOut = () => NumCast(this.Document.popOut); +// const popOff = () => NumCast(this.Document.popOff); // return ( // <mesh key={`mesh${topLeft[0]}${topLeft[1]}`} onClick={action(async e => { -// this.props.setRaised([ +// this._props.setRaised([ // { coord: new Vector2(topLeft[0], topLeft[1]), off: new Vector3(-popOff(), -popOff(), popOut()) }, // { coord: new Vector2(topLeft[0] + 1, topLeft[1]), off: new Vector3(popOff(), -popOff(), popOut()) }, // { coord: new Vector2(topLeft[0], topLeft[1] + 1), off: new Vector3(-popOff(), popOff(), popOut()) }, @@ -100,7 +100,7 @@ declare class MediaRecorder { // r?.setAttribute('uv', new BufferAttribute(uvs, 2)); // r?.setIndex(new BufferAttribute(indices, 1)); // }} /> -// {!this._videoRef ? <meshStandardMaterial color={this.props.color} /> : +// {!this._videoRef ? <meshStandardMaterial color={this._props.color} /> : // <meshBasicMaterial > // <videoTexture attach="map" args={[this._videoRef]} /> // </meshBasicMaterial>} @@ -110,7 +110,7 @@ declare class MediaRecorder { // } @observer -export class ScreenshotBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps & FieldViewProps>() { +export class ScreenshotBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { public static LayoutString(fieldKey: string) { return FieldView.LayoutString(ScreenshotBox, fieldKey); } @@ -119,23 +119,17 @@ export class ScreenshotBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatabl @observable private _videoRef: HTMLVideoElement | null = null; @observable _screenCapture = false; @computed get recordingStart() { - return Cast(this.dataDoc[this.props.fieldKey + '_recordingStart'], DateField)?.date.getTime(); + return Cast(this.dataDoc[this._props.fieldKey + '_recordingStart'], DateField)?.date.getTime(); } - constructor(props: any) { + constructor(props: FieldViewProps) { super(props); - if (this.rootDoc.videoWall) { - this.rootDoc.nativeWidth = undefined; - this.rootDoc.nativeHeight = undefined; - this.layoutDoc.popOff = 0; - this.layoutDoc.popOut = 1; - } else { - this.setupDictation(); - } + makeObservable(this); + this.setupDictation(); } getAnchor = (addAsAnnotation: boolean) => { const startTime = Cast(this.layoutDoc._layout_currentTimecode, 'number', null) || (this._videoRec ? (Date.now() - (this.recordingStart || 0)) / 1000 : undefined); - return CollectionStackedTimeline.createAnchor(this.rootDoc, this.dataDoc, this.annotationKey, startTime, startTime === undefined ? undefined : startTime + 3, undefined, addAsAnnotation) || this.rootDoc; + return CollectionStackedTimeline.createAnchor(this.Document, this.dataDoc, this.annotationKey, startTime, startTime === undefined ? undefined : startTime + 3, undefined, addAsAnnotation) || this.Document; }; videoLoad = () => { @@ -145,14 +139,14 @@ export class ScreenshotBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatabl if (!nativeWidth || !nativeHeight) { if (!nativeWidth) Doc.SetNativeWidth(this.dataDoc, 1200); Doc.SetNativeHeight(this.dataDoc, (nativeWidth || 1200) / aspect); - this.layoutDoc._height = (this.layoutDoc[Width]() || 0) / aspect; + this.layoutDoc._height = NumCast(this.layoutDoc._width) / aspect; } }; componentDidMount() { this.dataDoc.nativeWidth = this.dataDoc.nativeHeight = 0; - this.props.setContentView?.(this); // this tells the DocumentView that this ScreenshotBox is the "content" of the document. this allows the DocumentView to indirectly call getAnchor() on the AudioBox when making a link. - // this.rootDoc.videoWall && reaction(() => ({ width: this.props.PanelWidth(), height: this.props.PanelHeight() }), + this._props.setContentViewBox?.(this); // this tells the DocumentView that this ScreenshotBox is the "content" of the document. this allows the DocumentView to indirectly call getAnchor() on the AudioBox when making a link. + // this.layoutDoc.videoWall && reaction(() => ({ width: this._props.PanelWidth(), height: this._props.PanelHeight() }), // ({ width, height }) => { // if (this._camera) { // const angle = -Math.abs(1 - width / height); @@ -174,7 +168,6 @@ export class ScreenshotBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatabl }; @computed get content() { - if (this.rootDoc.videoWall) return null; return ( <video className={'videoBox-content'} @@ -182,7 +175,7 @@ export class ScreenshotBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatabl ref={r => { this._videoRef = r; setTimeout(() => { - if (this.rootDoc.mediaState === media_state.PendingRecording && this._videoRef) { + if (this.layoutDoc.mediaState === media_state.PendingRecording && this._videoRef) { this.toggleRecording(); } }, 100); @@ -203,13 +196,13 @@ export class ScreenshotBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatabl // @observable _raised = [] as { coord: Vector2, off: Vector3 }[]; // @action setRaised = (r: { coord: Vector2, off: Vector3 }[]) => this._raised = r; @computed get threed() { - // if (this.rootDoc.videoWall) { + // if (this.layoutDoc.videoWall) { // const screens: any[] = []; // const colors = ["yellow", "red", "orange", "brown", "maroon", "gray"]; // let count = 0; // numberRange(this._numScreens).forEach(x => numberRange(this._numScreens).forEach(y => screens.push( - // <VideoTile rootDoc={this.rootDoc} color={colors[count++ % colors.length]} x={x} y={y} raised={this._raised} setRaised={this.setRaised} />))); - // return <Canvas key="canvas" id="CANCAN" style={{ width: this.props.PanelWidth(), height: this.props.PanelHeight() }} gl={{ antialias: false }} colorManagement={false} onCreated={props => { + // <VideoTile doc={this.layoutDoc} color={colors[count++ % colors.length]} x={x} y={y} raised={this._raised} setRaised={this.setRaised} />))); + // return <Canvas key="canvas" id="CANCAN" style={{ width: this._props.PanelWidth(), height: this._props.PanelHeight() }} gl={{ antialias: false }} colorManagement={false} onCreated={props => { // this._camera = props.camera; // props.camera.position.set(this._numScreens / 2, this._numScreens / 2, this._numScreens - 2); // props.camera.lookAt(this._numScreens / 2, this._numScreens / 2, 0); @@ -232,15 +225,15 @@ export class ScreenshotBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatabl this._audioRec.onstop = async (e: any) => { const [{ result }] = await Networking.UploadFilesToServer(aud_chunks.map((file: any) => ({ file }))); if (!(result instanceof Error)) { - this.dataDoc[this.props.fieldKey + '_audio'] = new AudioField(result.accessPaths.agnostic.client); + 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 = []; this._videoRec.onstart = () => { - if (this.dataDoc[this.props.fieldKey + '_trackScreen']) TrackMovements.Instance.start(); - this.dataDoc[this.props.fieldKey + '_recordingStart'] = new DateField(new Date()); + 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) => { @@ -251,7 +244,7 @@ export class ScreenshotBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatabl this.dataDoc[this.fieldKey + '_presentation'] = JSON.stringify(presCopy); } TrackMovements.Instance.finish(); - const file = new File(vid_chunks, `${this.rootDoc[Id]}.mkv`, { type: vid_chunks[0].type, lastModified: Date.now() }); + const file = new File(vid_chunks, `${this.Document[Id]}.mkv`, { type: vid_chunks[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)) { @@ -260,7 +253,7 @@ export class ScreenshotBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatabl this.layoutDoc.layout = VideoBox.LayoutString(this.fieldKey); this.dataDoc.nativeWidth = this.dataDoc.nativeHeight = undefined; this.layoutDoc._layout_fitWidth = undefined; - this.dataDoc[this.props.fieldKey] = new VideoField(result.accessPaths.agnostic.client); + this.dataDoc[this._props.fieldKey] = new VideoField(result.accessPaths.agnostic.client); } else alert('video conversion failed'); }; this._audioRec.start(); @@ -280,23 +273,23 @@ export class ScreenshotBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatabl const ind = DocUtils.ActiveRecordings.indexOf(this); ind !== -1 && DocUtils.ActiveRecordings.splice(ind, 1); - CaptureManager.Instance.open(this.rootDoc); + CaptureManager.Instance.open(this.Document); } }; setupDictation = () => { if (this.dataDoc[this.fieldKey + '_dictation']) return; - const dictationText = DocUtils.GetNewTextDoc('dictation', NumCast(this.rootDoc.x), NumCast(this.rootDoc.y) + NumCast(this.layoutDoc._height) + 10, NumCast(this.layoutDoc._width), 2 * NumCast(this.layoutDoc._height)); + const dictationText = DocUtils.GetNewTextDoc('dictation', NumCast(this.Document.x), NumCast(this.Document.y) + NumCast(this.layoutDoc._height) + 10, NumCast(this.layoutDoc._width), 2 * NumCast(this.layoutDoc._height)); const textField = Doc.LayoutFieldKey(dictationText); dictationText._layout_autoHeight = false; - const dictationTextProto = Doc.GetProto(dictationText); + const dictationTextProto = dictationText[DocData]; dictationTextProto[`${textField}_recordingSource`] = this.dataDoc; - dictationTextProto[`${textField}_recordingStart`] = ComputedField.MakeFunction(`self.${textField}_recordingSource.${this.fieldKey}_recordingStart`); - dictationTextProto.mediaState = ComputedField.MakeFunction(`self.${textField}_recordingSource.mediaState`); + dictationTextProto[`${textField}_recordingStart`] = ComputedField.MakeFunction(`this.${textField}_recordingSource.${this.fieldKey}_recordingStart`); + dictationTextProto.mediaState = ComputedField.MakeFunction(`this.${textField}_recordingSource.mediaState`); this.dataDoc[this.fieldKey + '_dictation'] = dictationText; }; - videoPanelHeight = () => (NumCast(this.dataDoc[this.fieldKey + '_nativeHeight'], this.layoutDoc[Height]()) / NumCast(this.dataDoc[this.fieldKey + '_nativeWidth'], this.layoutDoc[Width]())) * this.props.PanelWidth(); - formattedPanelHeight = () => Math.max(0, this.props.PanelHeight() - this.videoPanelHeight()); + videoPanelHeight = () => (NumCast(this.dataDoc[this.fieldKey + '_nativeHeight'], NumCast(this.layoutDoc._height)) / NumCast(this.dataDoc[this.fieldKey + '_nativeWidth'], NumCast(this.layoutDoc._width))) * this._props.PanelWidth(); + formattedPanelHeight = () => Math.max(0, this._props.PanelHeight() - this.videoPanelHeight()); render() { TraceMobx(); return ( @@ -304,14 +297,14 @@ export class ScreenshotBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatabl <div className="videoBox-viewer"> <div style={{ position: 'relative', height: this.videoPanelHeight() }}> <CollectionFreeFormView - {...this.props} - setContentView={emptyFunction} + {...this._props} + setContentViewBox={emptyFunction} NativeWidth={returnZero} NativeHeight={returnZero} PanelHeight={this.videoPanelHeight} - PanelWidth={this.props.PanelWidth} - focus={this.props.focus} - isSelected={this.props.isSelected} + PanelWidth={this._props.PanelWidth} + focus={this._props.focus} + isSelected={this._props.isSelected} isAnnotationOverlay={true} select={emptyFunction} isContentActive={returnFalse} @@ -321,8 +314,7 @@ export class ScreenshotBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatabl removeDocument={returnFalse} moveDocument={returnFalse} addDocument={returnFalse} - ScreenToLocalTransform={this.props.ScreenToLocalTransform} - renderDepth={this.props.renderDepth + 1}> + renderDepth={this._props.renderDepth + 1}> <> {this.threed} {this.content} @@ -332,7 +324,7 @@ export class ScreenshotBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatabl <div style={{ background: SettingsManager.userColor, position: 'relative', height: this.formattedPanelHeight() }}> {!(this.dataDoc[this.fieldKey + '_dictation'] instanceof Doc) ? null : ( <FormattedTextBox - {...this.props} + {...this._props} Document={DocCast(this.dataDoc[this.fieldKey + '_dictation'])} fieldKey={'text'} PanelHeight={this.formattedPanelHeight} @@ -345,12 +337,12 @@ export class ScreenshotBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatabl removeDocument={returnFalse} moveDocument={returnFalse} addDocument={returnFalse} - renderDepth={this.props.renderDepth + 1} + renderDepth={this._props.renderDepth + 1} /> )} </div> </div> - {!this.props.isSelected() ? null : ( + {!this._props.isSelected() ? null : ( <div className="screenshotBox-uiButtons" style={{ background: SettingsManager.userColor }}> <div className="screenshotBox-recorder" style={{ color: SettingsManager.userBackgroundColor, background: SettingsManager.userVariantColor }} key="snap" onPointerDown={this.toggleRecording}> <FontAwesomeIcon icon="file" size="lg" /> diff --git a/src/client/views/nodes/ScriptingBox.scss b/src/client/views/nodes/ScriptingBox.scss index 8d76f2b1d..9789da55a 100644 --- a/src/client/views/nodes/ScriptingBox.scss +++ b/src/client/views/nodes/ScriptingBox.scss @@ -32,7 +32,7 @@ .scriptingBox-wrapper { width: 100%; height: 100%; - max-height: calc(100%-30px); + max-height: calc(100% - 30px); display: flex; flex-direction: row; overflow: auto; @@ -42,7 +42,8 @@ overflow: hidden; } - .scriptingBox-textArea, .scriptingBox-textArea-inputs { + .scriptingBox-textArea, + .scriptingBox-textArea-inputs { flex: 70; height: 100%; max-width: 95%; @@ -110,7 +111,7 @@ cursor: pointer; } - .rta__entity>* { + .rta__entity > * { padding-left: 4px; padding-right: 4px; } @@ -125,7 +126,7 @@ .scriptingBox-textArea-inputs { max-width: 100%; height: 40%; - width: 100%; + width: 100%; resize: none; } .scriptingBox-textArea-script { @@ -194,8 +195,8 @@ .scriptingBox-errorMessage { overflow: auto; - background: "red"; - background-color: "red"; + background: 'red'; + background-color: 'red'; height: 45px; } @@ -221,4 +222,4 @@ width: 25%; } } -}
\ No newline at end of file +} diff --git a/src/client/views/nodes/ScriptingBox.tsx b/src/client/views/nodes/ScriptingBox.tsx index 7c8a1849e..89650889d 100644 --- a/src/client/views/nodes/ScriptingBox.tsx +++ b/src/client/views/nodes/ScriptingBox.tsx @@ -1,33 +1,30 @@ let ReactTextareaAutocomplete = require('@webscopeio/react-textarea-autocomplete').default; -import { action, computed, observable } from 'mobx'; +import { action, computed, makeObservable, observable } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; +import { returnAlways, returnEmptyString } from '../../../Utils'; import { Doc } from '../../../fields/Doc'; import { List } from '../../../fields/List'; import { listSpec } from '../../../fields/Schema'; import { ScriptField } from '../../../fields/ScriptField'; import { BoolCast, Cast, DocCast, NumCast, ScriptCast, StrCast } from '../../../fields/Types'; import { TraceMobx } from '../../../fields/util'; -import { returnAlways, returnEmptyString, returnTrue } from '../../../Utils'; import { DragManager } from '../../util/DragManager'; -import { InteractionUtils } from '../../util/InteractionUtils'; +import { ScriptManager } from '../../util/ScriptManager'; import { CompileScript, ScriptParam } from '../../util/Scripting'; import { ScriptingGlobals } from '../../util/ScriptingGlobals'; -import { ScriptManager } from '../../util/ScriptManager'; import { ContextMenu } from '../ContextMenu'; -import { ViewBoxAnnotatableComponent, ViewBoxAnnotatableProps } from '../DocComponent'; +import { ViewBoxAnnotatableComponent } from '../DocComponent'; import { EditableView } from '../EditableView'; -import { FieldView, FieldViewProps } from '../nodes/FieldView'; import { OverlayView } from '../OverlayView'; +import { FieldView, FieldViewProps } from '../nodes/FieldView'; import { DocumentIconContainer } from './DocumentIcon'; -import { DocFocusOptions, DocumentView } from './DocumentView'; import './ScriptingBox.scss'; const _global = (window /* browser */ || global) /* node */ as any; @observer -export class ScriptingBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps & FieldViewProps>() { +export class ScriptingBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { private dropDisposer?: DragManager.DragDropDisposer; - protected _multiTouchDisposer?: InteractionUtils.MultiTouchEventDisposer | undefined; public static LayoutString(fieldStr: string) { return FieldView.LayoutString(ScriptingBox, fieldStr); } @@ -59,10 +56,11 @@ export class ScriptingBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatable @observable private _scriptSuggestedParams: any = ''; @observable private _scriptParamsText: any = ''; - constructor(props: any) { + constructor(props: FieldViewProps) { super(props); + makeObservable(this); if (!this.compileParams.length) { - const params = ScriptCast(this.rootDoc[this.props.fieldKey])?.script.options.params as { [key: string]: any }; + const params = ScriptCast(this.dataDoc[this._props.fieldKey])?.script.options.params as { [key: string]: any }; if (params) { this.compileParams = Array.from(Object.keys(params)) .filter(p => !p.startsWith('_')) @@ -79,30 +77,30 @@ export class ScriptingBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatable return this.compileParams.map(p => p.split(':')[1].trim()); } @computed({ keepAlive: true }) get rawScript() { - return ScriptCast(this.rootDoc[this.fieldKey])?.script.originalScript ?? ''; + return ScriptCast(this.dataDoc[this.fieldKey])?.script.originalScript ?? ''; } @computed({ keepAlive: true }) get functionName() { - return StrCast(this.rootDoc[this.props.fieldKey + '-functionName'], ''); + return StrCast(this.dataDoc[this.fieldKey + '-functionName'], ''); } @computed({ keepAlive: true }) get functionDescription() { - return StrCast(this.rootDoc[this.props.fieldKey + '-functionDescription'], ''); + return StrCast(this.dataDoc[this.fieldKey + '-functionDescription'], ''); } @computed({ keepAlive: true }) get compileParams() { - return Cast(this.rootDoc[this.props.fieldKey + '-params'], listSpec('string'), []); + return Cast(this.dataDoc[this.fieldKey + '-params'], listSpec('string'), []); } set rawScript(value) { - Doc.SetInPlace(this.rootDoc, this.props.fieldKey, new ScriptField(undefined, undefined, value), true); + this.dataDoc[this.fieldKey] = new ScriptField(undefined, undefined, value); } set functionName(value) { - Doc.SetInPlace(this.rootDoc, this.props.fieldKey + '-functionName', value, true); + this.dataDoc[this.fieldKey + '-functionName'] = value; } set functionDescription(value) { - Doc.SetInPlace(this.rootDoc, this.props.fieldKey + '-functionDescription', value, true); + this.dataDoc[this.fieldKey + '-functionDescription'] = value; } set compileParams(value) { - Doc.SetInPlace(this.rootDoc, this.props.fieldKey + '-params', new List<string>(value), true); + this.dataDoc[this.fieldKey + '-params'] = new List<string>(value); } getValue(result: any, descrip: boolean) { @@ -118,7 +116,7 @@ export class ScriptingBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatable @action componentDidMount() { - this.props.setContentView?.(this); + this._props.setContentViewBox?.(this); this.rawText = this.rawScript; const observer = new _global.ResizeObserver( action((entries: any) => { @@ -166,9 +164,7 @@ export class ScriptingBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatable // only included in buttons, transforms scripting UI to a button @action - onFinish = () => { - this.rootDoc.layout_fieldKey = 'layout'; - }; + onFinish = () => (this.layoutDoc.layout_fieldKey = 'layout'); // displays error message @action @@ -190,7 +186,7 @@ export class ScriptingBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatable params, typecheck: false, }); - Doc.SetInPlace(this.rootDoc, this.fieldKey, result.compiled ? new ScriptField(result, undefined, this.rawText) : undefined, true); + this.dataDoc[this.fieldKey] = result.compiled ? new ScriptField(result, undefined, this.rawText) : undefined; this.onError(result.compiled ? undefined : result.errors); return result.compiled; }; @@ -200,9 +196,9 @@ export class ScriptingBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatable onRun = () => { if (this.onCompile()) { const bindings: { [name: string]: any } = {}; - this.paramsNames.forEach(key => (bindings[key] = this.rootDoc[key])); - // binds vars so user doesnt have to refer to everything as self.<var> - ScriptCast(this.rootDoc[this.fieldKey], null)?.script.run({ ...bindings, self: this.rootDoc, this: this.layoutDoc }, this.onError); + 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); } }; @@ -271,7 +267,7 @@ export class ScriptingBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatable @action onDrop = (e: Event, de: DragManager.DropEvent, fieldKey: string) => { if (de.complete.docDragData) { - de.complete.docDragData.droppedDocuments.forEach(doc => Doc.SetInPlace(this.rootDoc, fieldKey, doc, true)); + de.complete.docDragData.droppedDocuments.forEach(doc => (this.dataDoc[fieldKey] = doc)); e.stopPropagation(); return true; } @@ -281,7 +277,7 @@ export class ScriptingBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatable // deletes a param from all areas in which it is stored @action onDelete = (num: number) => { - Doc.SetInPlace(this.rootDoc, this.paramsNames[num], undefined, true); + this.dataDoc[this.paramsNames[num]] = undefined; this.compileParams.splice(num, 1); return true; }; @@ -291,14 +287,14 @@ export class ScriptingBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatable viewChanged = (e: React.ChangeEvent, name: string) => { //@ts-ignore const val = e.target.selectedOptions[0].value; - Doc.SetInPlace(this.rootDoc, name, val[0] === 'S' ? val.substring(1) : val[0] === 'N' ? parseInt(val.substring(1)) : val.substring(1) === 'true', true); + this.dataDoc[name] = val[0] === 'S' ? val.substring(1) : val[0] === 'N' ? parseInt(val.substring(1)) : val.substring(1) === 'true'; }; // creates a copy of the script document onCopy = () => { - const copy = Doc.MakeCopy(this.rootDoc, true); - copy.x = NumCast(this.dataDoc.x) + NumCast(this.dataDoc._width); - this.props.addDocument?.(copy); + const copy = Doc.MakeCopy(this.Document, true); + copy.x = NumCast(this.Document.x) + NumCast(this.dataDoc._width); + this._props.addDocument?.(copy); }; // adds option to create a copy to the context menu @@ -314,7 +310,7 @@ export class ScriptingBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatable 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()}> + <div className="scriptingBox-inputDiv" onPointerDown={e => this._props.isSelected() && e.stopPropagation()}> <div className="scriptingBox-wrapper" style={{ maxWidth: '100%' }}> <div className="container" style={{ maxWidth: '100%' }}> <div className="descriptor" style={{ textAlign: 'center', display: 'inline-block', maxWidth: '100%' }}> @@ -347,8 +343,8 @@ export class ScriptingBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatable maxHeight={72} height={35} fontSize={14} - contents={StrCast(DocCast(this.rootDoc[parameter])?.title, 'undefined')} - GetValue={() => StrCast(DocCast(this.rootDoc[parameter])?.title, 'undefined')} + contents={StrCast(DocCast(this.dataDoc[parameter])?.title, 'undefined')} + GetValue={() => StrCast(DocCast(this.dataDoc[parameter])?.title, 'undefined')} SetValue={action((value: string) => { const script = CompileScript(value, { addReturn: true, @@ -358,7 +354,7 @@ export class ScriptingBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatable const results = script.compiled && script.run(); if (results && results.success) { this._errorMessage = ''; - Doc.SetInPlace(this.rootDoc, parameter, results.result, true); + this.dataDoc[parameter] = results.result; return true; } this._errorMessage = 'invalid document'; @@ -371,7 +367,7 @@ export class ScriptingBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatable // rendering when a string's value can be set in applied UI renderBasicType(parameter: string, isNum: boolean) { - const strVal = isNum ? NumCast(this.rootDoc[parameter]).toString() : StrCast(this.rootDoc[parameter]); + const strVal = isNum ? NumCast(this.dataDoc[parameter]).toString() : StrCast(this.dataDoc[parameter]); return ( <div className="scriptingBox-paramInputs" style={{ overflowY: 'hidden' }}> <EditableView @@ -385,7 +381,7 @@ export class ScriptingBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatable const setValue = isNum ? parseInt(value) : value; if (setValue !== undefined && setValue !== ' ') { this._errorMessage = ''; - Doc.SetInPlace(this.rootDoc, parameter, setValue, true); + this.dataDoc[parameter] = setValue; return true; } this._errorMessage = 'invalid input'; @@ -406,7 +402,7 @@ export class ScriptingBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatable className="scriptingBox-viewPicker" onPointerDown={e => e.stopPropagation()} onChange={e => this.viewChanged(e, parameter)} - value={typeof this.rootDoc[parameter] === 'string' ? 'S' + StrCast(this.rootDoc[parameter]) : typeof this.rootDoc[parameter] === 'number' ? 'N' + NumCast(this.rootDoc[parameter]) : 'B' + BoolCast(this.rootDoc[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) => ( <option key={i} className="scriptingBox-viewOption" value={(typeof type === 'string' ? 'S' : typeof type === 'number' ? 'N' : 'B') + type}> {' '} @@ -691,7 +687,7 @@ export class ScriptingBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatable ); return ( - <div className="scriptingBox-inputDiv" onPointerDown={e => this.props.isSelected() && e.stopPropagation()}> + <div className="scriptingBox-inputDiv" onPointerDown={e => this._props.isSelected() && e.stopPropagation()}> <div className="scriptingBox-wrapper"> {this.renderScriptingBox} {definedParameters} @@ -704,7 +700,7 @@ export class ScriptingBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatable // toolbar (with compile and apply buttons) for scripting UI renderScriptingTools() { - const buttonStyle = 'scriptingBox-button' + (StrCast(this.rootDoc.layout_fieldKey).startsWith('layout_on') ? '-finish' : ''); + const buttonStyle = 'scriptingBox-button' + (StrCast(this.Document.layout_fieldKey).startsWith('layout_on') ? '-finish' : ''); return ( <div className="scriptingBox-toolbar"> <button @@ -732,7 +728,7 @@ export class ScriptingBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatable Save </button> - {!StrCast(this.rootDoc.layout_fieldKey).startsWith('layout_on') ? null : ( // onClick, onChecked, etc need a Finish button to return to their default layout + {!StrCast(this.Document.layout_fieldKey).startsWith('layout_on') ? null : ( // onClick, onChecked, etc need a Finish button to return to their default layout <button className={buttonStyle} onPointerDown={e => { @@ -749,7 +745,7 @@ export class ScriptingBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatable // inputs UI for params which allows you to set values for each displayed in a list renderParamsInputs() { return ( - <div className="scriptingBox-inputDiv" onPointerDown={e => this.props.isSelected(true) && e.stopPropagation()}> + <div className="scriptingBox-inputDiv" onPointerDown={e => this._props.isSelected() && e.stopPropagation()}> {!this.compileParams.length || !this.paramsNames ? null : ( <div className="scriptingBox-plist"> {this.paramsNames.map((parameter: string, i: number) => ( @@ -778,7 +774,7 @@ export class ScriptingBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatable // toolbar (with edit and run buttons and error message) for params UI renderTools(toolSet: string, func: () => void) { - const buttonStyle = 'scriptingBox-button' + (StrCast(this.rootDoc.layout_fieldKey).startsWith('layout_on') ? '-finish' : ''); + const buttonStyle = 'scriptingBox-button' + (StrCast(this.Document.layout_fieldKey).startsWith('layout_on') ? '-finish' : ''); return ( <div className="scriptingBox-toolbar"> <button @@ -797,7 +793,7 @@ export class ScriptingBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatable }}> {toolSet} </button> - {!StrCast(this.rootDoc.layout_fieldKey).startsWith('layout_on') ? null : ( + {!StrCast(this.Document.layout_fieldKey).startsWith('layout_on') ? null : ( <button className={buttonStyle} onPointerDown={e => { @@ -816,7 +812,7 @@ export class ScriptingBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatable TraceMobx(); return ( <div className={`scriptingBox`} onContextMenu={this.specificContextMenu} onPointerUp={!this._function ? this.suggestionPos : undefined}> - <div className="scriptingBox-outerDiv" onWheel={e => this.props.isSelected(true) && e.stopPropagation()}> + <div className="scriptingBox-outerDiv" onWheel={e => this._props.isSelected() && e.stopPropagation()}> {this._paramSuggestion ? ( <div className="boxed" ref={this._suggestionRef} style={{ left: this._suggestionBoxX + 20, top: this._suggestionBoxY - 15, display: 'inline' }}> {' '} diff --git a/src/client/views/nodes/TaskCompletedBox.tsx b/src/client/views/nodes/TaskCompletedBox.tsx index 2a3dd8d2d..c9e15d314 100644 --- a/src/client/views/nodes/TaskCompletedBox.tsx +++ b/src/client/views/nodes/TaskCompletedBox.tsx @@ -1,30 +1,33 @@ -import React = require("react"); -import { observer } from "mobx-react"; -import "./TaskCompletedBox.scss"; -import { observable, action } from "mobx"; -import { Fade } from "@material-ui/core"; - +import { Fade } from '@mui/material'; +import { action, observable } from 'mobx'; +import { observer } from 'mobx-react'; +import * as React from 'react'; +import './TaskCompletedBox.scss'; @observer export class TaskCompletionBox extends React.Component<{}> { - @observable public static taskCompleted: boolean = false; @observable public static popupX: number = 500; @observable public static popupY: number = 150; - @observable public static textDisplayed: string; + @observable public static textDisplayed: string = ''; @action public static toggleTaskCompleted = () => { TaskCompletionBox.taskCompleted = !TaskCompletionBox.taskCompleted; - } + }; render() { - return <Fade in={TaskCompletionBox.taskCompleted}> - <div className="taskCompletedBox-fade" - style={{ - left: TaskCompletionBox.popupX ? TaskCompletionBox.popupX : 500, - top: TaskCompletionBox.popupY ? TaskCompletionBox.popupY : 150, - }}>{TaskCompletionBox.textDisplayed}</div> - </Fade>; + return ( + <Fade in={TaskCompletionBox.taskCompleted}> + <div + className="taskCompletedBox-fade" + style={{ + left: TaskCompletionBox.popupX ? TaskCompletionBox.popupX : 500, + top: TaskCompletionBox.popupY ? TaskCompletionBox.popupY : 150, + }}> + {TaskCompletionBox.textDisplayed} + </div> + </Fade> + ); } -}
\ No newline at end of file +} diff --git a/src/client/views/nodes/VideoBox.scss b/src/client/views/nodes/VideoBox.scss index f803715ad..460155446 100644 --- a/src/client/views/nodes/VideoBox.scss +++ b/src/client/views/nodes/VideoBox.scss @@ -1,4 +1,4 @@ -@import '../global/globalCssVariables.scss'; +@import '../global/globalCssVariables.module.scss'; .mini-viewer { cursor: grab; @@ -100,6 +100,7 @@ padding: 0 10px 0 7px; transition: opacity 0.3s; z-index: 10001; + transform-origin: top left; .timecode-controls { display: flex; @@ -171,13 +172,17 @@ top: 90%; transform: translate(-50%, -50%); width: 80%; - transition: top 0s, width 0s, opacity 0.3s, visibility 0.3s; + transition: + top 0s, + width 0s, + opacity 0.3s, + visibility 0.3s; } } -video::-webkit-media-controls { - display: none !important; -} +// video::-webkit-media-controls { +// display: none !important; +// } input[type='range'] { -webkit-appearance: none; diff --git a/src/client/views/nodes/VideoBox.tsx b/src/client/views/nodes/VideoBox.tsx index d7f7c9b73..40647feff 100644 --- a/src/client/views/nodes/VideoBox.tsx +++ b/src/client/views/nodes/VideoBox.tsx @@ -1,40 +1,36 @@ -import React = require('react'); import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { action, computed, IReactionDisposer, observable, ObservableMap, reaction, runInAction, untracked } from 'mobx'; +import { action, computed, IReactionDisposer, makeObservable, observable, ObservableMap, reaction, runInAction } from 'mobx'; import { observer } from 'mobx-react'; import { basename } from 'path'; -import { Doc, StrListCast } from '../../../fields/Doc'; -import { Height, Width } from '../../../fields/DocSymbols'; +import * as React from 'react'; +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, DocCast, NumCast, StrCast } 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 { DocumentType } from '../../documents/DocumentTypes'; -import { Networking } from '../../Network'; import { DocumentManager } from '../../util/DocumentManager'; import { FollowLinkScript } from '../../util/LinkFollower'; import { LinkManager } from '../../util/LinkManager'; import { ReplayMovements } from '../../util/ReplayMovements'; -import { SelectionManager } from '../../util/SelectionManager'; -import { SnappingManager } from '../../util/SnappingManager'; import { undoBatch } from '../../util/UndoManager'; import { CollectionFreeFormView } from '../collections/collectionFreeForm/CollectionFreeFormView'; import { CollectionStackedTimeline, TrimScope } from '../collections/CollectionStackedTimeline'; import { ContextMenu } from '../ContextMenu'; import { ContextMenuProps } from '../ContextMenuItem'; -import { ViewBoxAnnotatableComponent, ViewBoxAnnotatableProps } from '../DocComponent'; +import { ViewBoxAnnotatableComponent, ViewBoxInterface } from '../DocComponent'; import { MarqueeAnnotator } from '../MarqueeAnnotator'; import { AnchorMenu } from '../pdf/AnchorMenu'; import { StyleProp } from '../StyleProvider'; -import { DocFocusOptions, DocumentView, OpenWhere } from './DocumentView'; -import { FieldView, FieldViewProps } from './FieldView'; +import { DocumentView } from './DocumentView'; +import { FocusViewOptions, FieldView, FieldViewProps } from './FieldView'; import { RecordingBox } from './RecordingBox'; import { PinProps, PresBox } from './trails'; import './VideoBox.scss'; -const path = require('path'); /** * VideoBox @@ -42,48 +38,47 @@ const path = require('path'); * Supporting Components: CollectionStackedTimeline * * VideoBox is a node that supports the playback of video files in Dash. - * When a video file or YouTube video is importeed into Dash, it is immediately rendered as a VideoBox document. + * When a video file is importeed into Dash, it is immediately rendered as a VideoBox document. * CollectionStackedTimline handles AudioBox and VideoBox shared behavior, but VideoBox handles playing, pausing, etc because it contains <video> element * User can trim video: nondestructive, just sets new bounds for playback and rendering timeline * Like images, users can zoom and pan and it has an overlay layer allowing for annotations on top of the video at different times */ @observer -export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps & FieldViewProps>() { +export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implements ViewBoxInterface { public static LayoutString(fieldKey: string) { return FieldView.LayoutString(VideoBox, fieldKey); } - - static _youtubeIframeCounter: number = 0; static heightPercent = 80; // height of video relative to videoBox when timeline is open static numThumbnails = 20; private unmounting = false; private _disposers: { [name: string]: IReactionDisposer } = {}; - private _youtubePlayer: YT.Player | undefined = undefined; private _videoRef: HTMLVideoElement | null = null; // <video> ref private _contentRef: HTMLDivElement | null = null; // ref to div that wraps video and controls for full screen - private _youtubeIframeId: number = -1; - private _youtubeContentCreated = false; private _audioPlayer: HTMLAudioElement | null = null; + private _marqueeref = React.createRef<MarqueeAnnotator>(); private _mainCont: React.RefObject<HTMLDivElement> = React.createRef(); // outermost div private _annotationLayer: React.RefObject<HTMLDivElement> = React.createRef(); private _playRegionTimer: any = null; // timeout for playback private _controlsFadeTimer: any = null; // timeout for controls fade - @observable _stackedTimeline: any; // CollectionStackedTimeline ref - @observable static _nativeControls: boolean; // default html controls - @observable _marqueeing: number[] | undefined; // coords for marquee selection + constructor(props: FieldViewProps) { + super(props); + makeObservable(this); + this._props.setContentViewBox?.(this); + } + + @observable _stackedTimeline: CollectionStackedTimeline | undefined = undefined; // CollectionStackedTimeline ref @observable _savedAnnotations = new ObservableMap<number, HTMLDivElement[]>(); @observable _screenCapture = false; @observable _clicking = false; // used for transition between showing/hiding timeline - @observable _forceCreateYouTubeIFrame = false; @observable _playTimer?: NodeJS.Timeout = undefined; @observable _fullScreen = false; @observable _playing = false; @observable _finished: boolean = false; // has playback reached end of clip @observable _volume: number = 1; @observable _muted: boolean = false; - @observable _controlsTransform?: { X: number; Y: number }; + @observable _controlsTransform?: { X: number; Y: number } = undefined; @observable _controlsVisible: boolean = true; @observable _scrubbing: boolean = false; @@ -96,14 +91,9 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp // @computed get rawDuration() { return NumCast(this.dataDoc[this.fieldKey + "_duration"]); } @observable rawDuration: number = 0; - @computed get youtubeVideoId() { - const field = Cast(this.dataDoc[this.props.fieldKey], VideoField); - return field && field.url.href.indexOf('youtube') !== -1 ? ((arr: string[]) => arr[arr.length - 1])(field.url.href.split('/')) : ''; - } - // returns the path of the audio file @computed get audiopath() { - const field = Cast(this.props.Document[this.props.fieldKey + '_audio'], AudioField, null); + const field = Cast(this.Document[this._props.fieldKey + '_audio'], AudioField, null); const vfield = Cast(this.dataDoc[this.fieldKey], VideoField, null); return field?.url.href ?? vfield?.url.href ?? ''; } @@ -126,18 +116,8 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp componentDidMount() { this.unmounting = false; - this.props.setContentView?.(this); // this tells the DocumentView that this VideoBox is the "content" of the document. this allows the DocumentView to indirectly call getAnchor() on the VideoBox when making a link. - if (this.youtubeVideoId) { - const youtubeaspect = 400 / 315; - const nativeWidth = Doc.NativeWidth(this.layoutDoc); - const nativeHeight = Doc.NativeHeight(this.layoutDoc); - if (!nativeWidth || !nativeHeight) { - if (!nativeWidth) Doc.SetNativeWidth(this.dataDoc, 600); - Doc.SetNativeHeight(this.dataDoc, (nativeWidth || 600) / youtubeaspect); - this.layoutDoc._height = NumCast(this.layoutDoc._width) / youtubeaspect; - } - } - this.player && this.setPlayheadTime(this.timeline.clipStart || 0); + this._props.setContentViewBox?.(this); // this tells the DocumentView that this VideoBox is the "content" of the document. this allows the DocumentView to indirectly call getAnchor() on the VideoBox when making a link. + this.player && this.setPlayheadTime(this.timeline?.clipStart || 0); document.addEventListener('keydown', this.keyEvents, true); if (this.presentation) { @@ -163,7 +143,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp if ( // need to include range inputs because after dragging time slider it becomes target element !(e.target instanceof HTMLInputElement && !(e.target.type === 'range')) && - this.props.isSelected(true) + this._props.isSelected() ) { switch (e.key) { case 'ArrowLeft': @@ -198,8 +178,6 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp this._audioPlayer && this.player && (this._audioPlayer.currentTime = this.player?.currentTime); update && this.player && this.playFrom(start, undefined, true); update && this._audioPlayer?.play(); - update && this._youtubePlayer?.playVideo(); - this._youtubePlayer && !this._playTimer && (this._playTimer = setInterval(this.updateTimecode, 5)); } catch (e) { console.log('Video Play Exception:', e); } @@ -209,11 +187,6 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp // goes to time @action public Seek(time: number) { - try { - this._youtubePlayer?.seekTo(Math.round(time), true); - } catch (e) { - console.log('Video Seek Exception:', e); - } this.player && (this.player.currentTime = time); this._audioPlayer && (this._audioPlayer.currentTime = time); // TODO: revisit this and clean it @@ -239,13 +212,9 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp try { update && this.player?.pause(); update && this._audioPlayer?.pause(); - update && this._youtubePlayer?.pauseVideo(); - this._youtubePlayer && this._playTimer && clearInterval(this._playTimer); - this._youtubePlayer?.seekTo(this._youtubePlayer?.getCurrentTime(), true); } catch (e) { console.log('Video Pause Exception:', e); } - this._youtubePlayer && SelectionManager.DeselectAll(); // if we don't deselect the player, then we get an annoying YouTube spinner I guess telling us we're paused. this._playTimer = undefined; this.updateTimecode(); if (!this._finished) { @@ -267,11 +236,6 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp this._fullScreen = true; this.player && this._contentRef && this._contentRef.requestFullscreen(); } - try { - this._youtubePlayer && this.props.addDocTab(this.rootDoc, OpenWhere.add); - } catch (e) { - console.log('Video FullScreen Exception:', e); - } }; // fades out controls in fullscreen after mouse stops moving @@ -329,24 +293,14 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp title: (this.layoutDoc._layout_currentTimecode || 0).toString(), onClick: FollowLinkScript(), }); - this.props.addDocument?.(b); - DocUtils.MakeLink(b, this.rootDoc, { link_relationship: 'video snapshot' }); - Networking.PostToServer('/youtubeScreenshot', { - id: this.youtubeVideoId, - timecode: this.layoutDoc._layout_currentTimecode, - }).then(response => { - const resolved = response?.accessPaths?.agnostic?.client; - if (resolved) { - this.props.removeDocument?.(b); - this.createSnapshotLink(resolved); - } - }); + this._props.addDocument?.(b); + DocUtils.MakeLink(b, this.Document, { link_relationship: 'video snapshot' }); } else { //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.rootDoc.title).replace(/[ -\.:]/g, ''); - const encodedFilename = encodeURIComponent('snapshot' + retitled + '_' + (this.layoutDoc._layout_currentTimecode || 0).toString().replace(/\./, '_')); + 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)); } @@ -355,8 +309,8 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp updateIcon = () => { const makeIcon = (returnedfilename: string) => { this.dataDoc.icon = new ImageField(returnedfilename); - this.dataDoc.icon_nativeWidth = this.layoutDoc[Width](); - this.dataDoc.icon_nativeHeight = this.layoutDoc[Height](); + this.dataDoc.icon_nativeWidth = NumCast(this.layoutDoc._width); + this.dataDoc.icon_nativeHeight = NumCast(this.layoutDoc._height); }; this.Snapshot(undefined, undefined, makeIcon); }; @@ -376,11 +330,11 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp _height: (height / width) * 150, title: '--snapshot' + NumCast(this.layoutDoc._layout_currentTimecode) + ' image-', }); - Doc.SetNativeWidth(Doc.GetProto(imageSnapshot), Doc.NativeWidth(this.layoutDoc)); - Doc.SetNativeHeight(Doc.GetProto(imageSnapshot), Doc.NativeHeight(this.layoutDoc)); - this.props.addDocument?.(imageSnapshot); + 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' }); - link && (Doc.GetProto(link.link_anchor_2 as Doc).timecodeToHide = NumCast((link.link_anchor_2 as Doc).timecodeToShow) + 3); + link && (DocCast(link.link_anchor_2)[DocData].timecodeToHide = NumCast(DocCast(link.link_anchor_2).timecodeToShow) + 3); setTimeout(() => downX !== undefined && downY !== undefined && DocumentManager.Instance.getFirstDocumentView(imageSnapshot)?.startDragging(downX, downY, 'move', true)); }; @@ -389,9 +343,9 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp const marquee = AnchorMenu.Instance.GetAnchor?.(undefined, addAsAnnotation); if (!addAsAnnotation && marquee) marquee.backgroundColor = 'transparent'; const anchor = addAsAnnotation - ? CollectionStackedTimeline.createAnchor(this.rootDoc, this.dataDoc, this.annotationKey, timecode ? timecode : undefined, undefined, marquee, addAsAnnotation) || this.rootDoc - : Docs.Create.ConfigDocument({ title: '#' + timecode, _timecodeToShow: timecode, annotationOn: this.rootDoc }); - PresBox.pinDocView(anchor, { pinDocLayout: pinProps?.pinDocLayout, pinData: { ...(pinProps?.pinData ?? {}), temporal: true } }, this.rootDoc); + ? CollectionStackedTimeline.createAnchor(this.Document, this.dataDoc, this.annotationKey, timecode ? 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 } }, this.Document); return anchor; }; @@ -413,16 +367,18 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp @action updateTimecode = () => { !this.unmounting && this.player && (this.layoutDoc._layout_currentTimecode = this.player.currentTime); - try { - this._youtubePlayer && (this.layoutDoc._layout_currentTimecode = this._youtubePlayer.getCurrentTime?.()); - } catch (e) { - console.log('Video Timecode Exception:', e); - } }; - // getView = async (doc: Doc) => { - // return new Promise<Opt<DocumentView>>(res => DocumentManager.Instance.AddViewRenderedCb(doc, dv => res(dv))); - // }; + getView = (doc: Doc, options: FocusViewOptions) => { + if (this._stackedTimeline?.makeDocUnfiltered(doc)) { + if (this.heightPercent === 100) { + 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))); + }; // extracts video thumbnails and saves them as field of doc getVideoThumbnails = () => { @@ -438,7 +394,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp 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.rootDoc.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); @@ -493,13 +449,13 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp // context menu specificContextMenu = (e: React.MouseEvent): void => { - const field = Cast(this.dataDoc[this.props.fieldKey], VideoField); + const field = Cast(this.dataDoc[this._props.fieldKey], VideoField); if (field) { const url = field.url.href; const subitems: ContextMenuProps[] = []; subitems.push({ description: 'Full Screen', event: this.FullScreen, icon: 'expand' }); subitems.push({ description: 'Take Snapshot', event: this.Snapshot, icon: 'expand-arrows-alt' }); - this.rootDoc.type === DocumentType.SCREENSHOT && + this.Document.type === DocumentType.SCREENSHOT && subitems.push({ description: 'Screen Capture', event: async () => { @@ -515,7 +471,6 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp 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: "Toggle Native Controls", event: action(() => VideoBox._nativeControls = !VideoBox._nativeControls), 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" }); @@ -533,7 +488,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp event: () => { this.dataDoc.layout = RecordingBox.LayoutString(this.fieldKey); // delete assoicated video data - this.dataDoc[this.props.fieldKey] = ''; + this.dataDoc[this._props.fieldKey] = ''; this.dataDoc[this.fieldKey + '_duration'] = ''; // delete assoicated presentation data this.dataDoc[this.fieldKey + '_presentation'] = ''; @@ -551,7 +506,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp // renders the video and audio @computed get content() { const field = Cast(this.dataDoc[this.fieldKey], VideoField); - const interactive = Doc.ActiveTool !== InkTool.None || !this.props.isSelected() ? '' : '-interactive'; + const interactive = Doc.ActiveTool !== InkTool.None || !this._props.isSelected() ? '' : '-interactive'; const classname = 'videoBox-content' + (this._fullScreen ? '-fullScreen' : '') + interactive; const opacity = this._scrubbing ? 0.3 : this._controlsVisible ? 1 : 0; return !field ? ( @@ -576,9 +531,9 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp key="video" autoPlay={this._screenCapture} ref={this.setVideoRef} - style={this._fullScreen ? this.fullScreenSize() : this.isCropped ? { width: 'max-content', height: 'max-content', transform: `scale(${1 / NumCast(this.rootDoc._freeform_scale)})`, transformOrigin: 'top left' } : {}} + style={this._fullScreen ? this.fullScreenSize() : this.isCropped ? { width: 'max-content', height: 'max-content', transform: `scale(${1 / NumCast(this.layoutDoc._freeform_scale)})`, transformOrigin: 'top left' } : {}} onCanPlay={this.videoLoad} - controls={VideoBox._nativeControls} + controls={false} onPlay={() => this.Play()} onSeeked={this.updateTimecode} onPause={() => this.Pause()} @@ -587,7 +542,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp Not supported. </video> {!this.audiopath || this.audiopath === field.url.href ? null : ( - <audio ref={this.setAudioRef} className={`audiobox-control${this.props.isContentActive() ? '-interactive' : ''}`}> + <audio ref={this.setAudioRef} className={`audiobox-control${this._props.isContentActive() ? '-interactive' : ''}`}> <source src={this.audiopath} type="audio/mpeg" /> Not supported. </audio> @@ -597,54 +552,6 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp ); } - @action youtubeIframeLoaded = (e: any) => { - if (!this._youtubeContentCreated) { - this._forceCreateYouTubeIFrame = !this._forceCreateYouTubeIFrame; - return; - } else this._youtubeContentCreated = false; - - this.loadYouTube(e.target); - }; - - loadYouTube = (iframe: any) => { - let started = true; - const onYoutubePlayerStateChange = (event: any) => - runInAction(() => { - if (started && event.data === YT.PlayerState.PLAYING) { - started = false; - this._youtubePlayer?.unMute(); - //this.Pause(); - return; - } - if (event.data === YT.PlayerState.PLAYING && !this._playing) this.Play(false); - if (event.data === YT.PlayerState.PAUSED && this._playing) this.Pause(false); - }); - const onYoutubePlayerReady = (event: any) => { - this._disposers.reactionDisposer?.(); - this._disposers.youtubeReactionDisposer?.(); - this._disposers.reactionDisposer = reaction( - () => this.layoutDoc._layout_currentTimecode, - () => !this._playing && this.Seek(NumCast(this.layoutDoc._layout_currentTimecode)) - ); - this._disposers.youtubeReactionDisposer = reaction( - () => Doc.ActiveTool === InkTool.None && this.props.isSelected(true) && !SnappingManager.GetIsDragging() && !DocumentView.Interacting, - interactive => (iframe.style.pointerEvents = interactive ? 'all' : 'none'), - { fireImmediately: true } - ); - }; - if (typeof YT === undefined) setTimeout(() => this.loadYouTube(iframe), 100); - else { - (YT as any)?.ready(() => { - this._youtubePlayer = new YT.Player(`${this.youtubeVideoId + this._youtubeIframeId}-player`, { - events: { - onReady: this.props.dontRegisterView ? undefined : onYoutubePlayerReady, - onStateChange: this.props.dontRegisterView ? undefined : onYoutubePlayerStateChange, - }, - }); - }); - } - }; - // for play button onPlayDown = () => (this._playing ? this.Pause() : this.Play()); @@ -678,9 +585,9 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp e, action(encodeURIComponent => { this._clicking = false; - if (this.props.isContentActive()) { - // const local = this.props.ScreenToLocalTransform().scale(this.props.scaling?.() || 1).transformPoint(e.clientX, e.clientY); - // this.layoutDoc._layout_timelineHeightPercent = Math.max(0, Math.min(100, local[1] / this.props.PanelHeight() * 100)); + if (this._props.isContentActive()) { + // const local = this.ScreenToLocalTransform().scale(this._props.scaling?.() || 1).transformPoint(e.clientX, e.clientY); + // this.layoutDoc._layout_timelineHeightPercent = Math.max(0, Math.min(100, local[1] / this._props.PanelHeight() * 100)); this.layoutDoc._layout_timelineHeightPercent = 80; } @@ -694,15 +601,15 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp 500 ); }, - this.props.isContentActive(), - this.props.isContentActive() + this._props.isContentActive(), + this._props.isContentActive() ); }; // removes from currently playing display @action removeCurrentlyPlaying = () => { - const docView = this.props.DocumentView?.(); + const docView = this.DocumentView?.(); if (CollectionStackedTimeline.CurrentlyPlaying && docView) { const index = CollectionStackedTimeline.CurrentlyPlaying.indexOf(docView); index !== -1 && CollectionStackedTimeline.CurrentlyPlaying.splice(index, 1); @@ -711,7 +618,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp // adds doc to currently playing display @action addCurrentlyPlaying = () => { - const docView = this.props.DocumentView?.(); + const docView = this.DocumentView?.(); if (!CollectionStackedTimeline.CurrentlyPlaying) { CollectionStackedTimeline.CurrentlyPlaying = []; } @@ -720,25 +627,6 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp } }; - @computed get youtubeContent() { - this._youtubeIframeId = VideoBox._youtubeIframeCounter++; - this._youtubeContentCreated = this._forceCreateYouTubeIFrame ? true : true; - const classname = 'videoBox-content-YouTube' + (this._fullScreen ? '-fullScreen' : ''); - const start = untracked(() => Math.round(NumCast(this.layoutDoc._layout_currentTimecode))); - return ( - <iframe - key={this._youtubeIframeId} - id={`${this.youtubeVideoId + this._youtubeIframeId}-player`} - onPointerLeave={this.updateTimecode} - onLoad={this.youtubeIframeLoaded} - className={classname} - width={Doc.NativeWidth(this.layoutDoc) || 640} - height={Doc.NativeHeight(this.layoutDoc) || 390} - src={`https://www.youtube.com/embed/${this.youtubeVideoId}?enablejsapi=1&rel=0&showinfo=1&autoplay=0&mute=1&start=${start}&modestbranding=1&controls=${VideoBox._nativeControls ? 1 : 0}`} - /> - ); - } - // for annotating, adds doc with time info @action.bound addDocWithTimecode(doc: Doc | Doc[]): boolean { @@ -851,7 +739,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp zoom = (zoom: number) => this.timeline?.setZoom(zoom); // plays link - playLink = (doc: Doc, options: DocFocusOptions) => { + playLink = (doc: Doc, options: FocusViewOptions) => { const startTime = Math.max(0, NumCast(doc.config_clipStart, this._stackedTimeline?.anchorStart(doc) || 0)); const endTime = this.timeline?.anchorEnd(doc); if (startTime !== undefined) { @@ -862,13 +750,13 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp // starts marquee selection marqueeDown = (e: React.PointerEvent) => { - if (!e.altKey && e.button === 0 && NumCast(this.layoutDoc._freeform_scale, 1) === 1 && this.props.isContentActive(true) && ![InkTool.Highlighter, InkTool.Pen].includes(Doc.ActiveTool)) { + if (!e.altKey && e.button === 0 && NumCast(this.layoutDoc._freeform_scale, 1) === 1 && this._props.isContentActive() && ![InkTool.Highlighter, InkTool.Pen].includes(Doc.ActiveTool)) { setupMoveUpEvents( this, e, action(e => { MarqueeAnnotator.clearAnnotations(this._savedAnnotations); - this._marqueeing = [e.clientX, e.clientY]; + this._marqueeref.current?.onInitiateSelection([e.clientX, e.clientY]); return true; }), returnFalse, @@ -882,48 +770,47 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp // ends marquee selection @action finishMarquee = () => { - this._marqueeing = undefined; - this.props.select(true); + this._marqueeref.current?.onTerminateSelection(); + this._props.select(true); }; - timelineWhenChildContentsActiveChanged = action((isActive: boolean) => this.props.whenChildContentsActiveChanged((this._isAnyChildContentActive = isActive))); + timelineWhenChildContentsActiveChanged = action((isActive: boolean) => this._props.whenChildContentsActiveChanged((this._isAnyChildContentActive = isActive))); timelineScreenToLocal = () => - this.props + this._props .ScreenToLocalTransform() .scale(this.scaling()) - .translate(0, (-this.heightPercent / 100) * this.props.PanelHeight()); + .translate(0, (-this.heightPercent / 100) * this._props.PanelHeight()); setPlayheadTime = (time: number) => (this.player!.currentTime = this.layoutDoc._layout_currentTimecode = time); - timelineHeight = () => (this.props.PanelHeight() * (100 - this.heightPercent)) / 100; + timelineHeight = () => (this._props.PanelHeight() * (100 - this.heightPercent)) / 100; playing = () => this._playing; - scaling = () => this.props.NativeDimScaling?.() || 1; + scaling = () => this._props.NativeDimScaling?.() || 1; - panelWidth = () => (this.props.PanelWidth() * this.heightPercent) / 100; - panelHeight = () => (this.layoutDoc._layout_fitWidth ? this.panelWidth() / (Doc.NativeAspect(this.rootDoc) || 1) : (this.props.PanelHeight() * this.heightPercent) / 100); + panelWidth = () => (this._props.PanelWidth() * this.heightPercent) / 100; + panelHeight = () => (this.layoutDoc._layout_fitWidth ? this.panelWidth() / (Doc.NativeAspect(this.dataDoc) || 1) : (this._props.PanelHeight() * this.heightPercent) / 100); screenToLocalTransform = () => { - const offset = (this.props.PanelWidth() - this.panelWidth()) / 2 / this.scaling(); - return this.props + const offset = (this._props.PanelWidth() - this.panelWidth()) / 2 / this.scaling(); + return this._props .ScreenToLocalTransform() .translate(-offset, 0) .scale(100 / this.heightPercent); }; - marqueeFitScaling = () => ((this.props.NativeDimScaling?.() || 1) * this.heightPercent) / 100; marqueeOffset = () => [((this.panelWidth() / 2) * (1 - this.heightPercent / 100)) / (this.heightPercent / 100), 0]; timelineDocFilter = () => [`_isTimelineLabel:true,${Utils.noRecursionHack}:x`]; // renders video controls componentUI = (boundsLeft: number, boundsTop: number) => { - const xf = this.props.ScreenToLocalTransform().inverse(); - const height = this.props.PanelHeight(); + const xf = this.ScreenToLocalBoxXf().inverse(); + const height = this._props.PanelHeight(); const vidHeight = (height * this.heightPercent) / 100 / this.scaling(); - const vidWidth = this.props.PanelWidth() / this.scaling(); + const vidWidth = this._props.PanelWidth() / this.scaling(); const uiHeight = 25; const uiMargin = 10; const yBot = xf.transformPoint(0, vidHeight)[1]; @@ -938,8 +825,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp <div className="videoBox-ui" style={{ - transformOrigin: 'top left', - transform: `rotate(${NumCast(this.rootDoc.rotation)}deg) translate(${-(xRight - xPos) + 10}px, ${yBot - yMid - uiHeight - uiMargin}px)`, + transform: `rotate(${this.ScreenToLocalBoxXf().inverse().RotateDeg}deg) translate(${-(xRight - xPos) + 10}px, ${yBot - yMid - uiHeight - uiMargin}px)`, left: xPos, top: yMid, height: uiHeight, @@ -953,22 +839,22 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp ); }; + thumbnails = () => StrListCast(this.dataDoc[this.fieldKey + '_thumbnails']); // renders CollectionStackedTimeline @computed get renderTimeline() { 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))} - {...this.props} + {...this._props} dataFieldKey={this.fieldKey} fieldKey={this.annotationKey} dictationKey={this.fieldKey + '_dictation'} mediaPath={this.audiopath} - thumbnails={() => StrListCast(this.dataDoc[this.fieldKey + '_thumbnails'])} - renderDepth={this.props.renderDepth + 1} + thumbnails={this.thumbnails} + renderDepth={this._props.renderDepth + 1} startTag={'_timecodeToShow' /* videoStart */} endTag={'_timecodeToHide' /* videoEnd */} - bringToFront={emptyFunction} playFrom={this.playFrom} setTime={this.setPlayheadTime} playing={this.playing} @@ -999,32 +885,33 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp crop = (region: Doc | undefined, addCrop?: boolean) => { if (!region) return; const cropping = Doc.MakeCopy(region, true); - Doc.GetProto(region).backgroundColor = 'transparent'; - Doc.GetProto(region).lockedPosition = true; - Doc.GetProto(region).title = 'region:' + this.rootDoc.title; - Doc.GetProto(region).followLinkToggle = true; + const regionData = region[DocData]; + regionData.backgroundColor = 'transparent'; + regionData.lockedPosition = true; + regionData.title = 'region:' + this.Document.title; + regionData.followLinkToggle = true; region._timecodeToHide = NumCast(region._timecodeToShow) + 0.0001; this.addDocument(region); const anchx = NumCast(cropping.x); const anchy = NumCast(cropping.y); const anchw = NumCast(cropping._width); const anchh = NumCast(cropping._height); - const viewScale = NumCast(this.rootDoc[this.fieldKey + '_nativeWidth']) / anchw; - cropping.title = 'crop: ' + this.rootDoc.title; - cropping.x = NumCast(this.rootDoc.x) + NumCast(this.rootDoc._width); - cropping.y = NumCast(this.rootDoc.y); - cropping._width = anchw * (this.props.NativeDimScaling?.() || 1); - cropping._height = anchh * (this.props.NativeDimScaling?.() || 1); + const viewScale = NumCast(this.dataDoc[this.fieldKey + '_nativeWidth']) / anchw; + cropping.title = 'crop: ' + this.Document.title; + cropping.x = NumCast(this.Document.x) + NumCast(this.layoutDoc._width); + cropping.y = NumCast(this.Document.y); + cropping._width = anchw * (this._props.NativeDimScaling?.() || 1); + cropping._height = anchh * (this._props.NativeDimScaling?.() || 1); cropping.timecodeToHide = undefined; cropping.timecodeToShow = undefined; cropping.onClick = undefined; - const croppingProto = Doc.GetProto(cropping); + const croppingProto = cropping[DocData]; croppingProto.annotationOn = undefined; croppingProto.isDataDoc = true; - croppingProto.proto = Cast(this.rootDoc.proto, Doc, null)?.proto; // set proto of cropping's data doc to be IMAGE_PROTO + croppingProto.proto = Cast(this.Document.proto, Doc, null)?.proto; // set proto of cropping's data doc to be IMAGE_PROTO croppingProto.type = DocumentType.VID; croppingProto.layout = VideoBox.LayoutString('data'); - croppingProto.data = ObjectField.MakeCopy(this.rootDoc[this.fieldKey] as ObjectField); + croppingProto.data = ObjectField.MakeCopy(this.dataDoc[this.fieldKey] as ObjectField); croppingProto['data_nativeWidth'] = anchw; croppingProto['data_nativeHeight'] = anchh; croppingProto.videoCrop = true; @@ -1040,12 +927,12 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp if (addCrop) { DocUtils.MakeLink(region, cropping, { link_relationship: 'cropped image' }); } - this.props.bringToFront(cropping); + this._props.bringToFront?.(cropping); return cropping; }; savedAnnotations = () => this._savedAnnotations; render() { - const borderRad = this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.BorderRounding); + const borderRad = this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.BorderRounding); const borderRadius = borderRad?.includes('px') ? `${Number(borderRad.split('px')[0]) / this.scaling()}px` : borderRad; return ( <div @@ -1055,11 +942,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp style={{ pointerEvents: this.layoutDoc._lockedPosition ? 'none' : undefined, borderRadius, - overflow: this.props.docViewPath?.().slice(-1)[0].layout_fitWidth ? 'auto' : undefined, - }} - onWheel={e => { - e.stopPropagation(); - e.preventDefault(); + overflow: this.DocumentView?.().layout_fitWidth ? 'auto' : undefined, }}> <div className="videoBox-viewer" onPointerDown={this.marqueeDown}> <div @@ -1069,19 +952,19 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp width: this.panelWidth(), height: this.panelHeight(), top: 0, - left: (this.props.PanelWidth() - this.panelWidth()) / 2, + left: (this._props.PanelWidth() - this.panelWidth()) / 2, }}> <CollectionFreeFormView - {...this.props} - setContentView={emptyFunction} + {...this._props} + setContentViewBox={emptyFunction} NativeWidth={returnZero} NativeHeight={returnZero} - renderDepth={this.props.renderDepth + 1} + renderDepth={this._props.renderDepth + 1} fieldKey={this.annotationKey} isAnnotationOverlay={true} annotationLayerHostsContent={true} - PanelWidth={this.props.PanelWidth} - PanelHeight={this.props.PanelHeight} + PanelWidth={this._props.PanelWidth} + PanelHeight={this._props.PanelHeight} isAnyChildContentActive={returnFalse} ScreenToLocalTransform={this.screenToLocalTransform} childFilters={this.timelineDocFilter} @@ -1092,24 +975,26 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp removeDocument={this.removeDocument} moveDocument={this.moveDocument} addDocument={this.addDocWithTimecode}> - {this.youtubeVideoId ? this.youtubeContent : this.content} + {this.content} </CollectionFreeFormView> </div> {this.annotationLayer} - {!this._marqueeing || !this._mainCont.current || !this._annotationLayer.current ? null : ( + {!this._mainCont.current || !this.DocumentView || !this._annotationLayer.current ? null : ( <MarqueeAnnotator - rootDoc={this.rootDoc} + ref={this._marqueeref} + Document={this.Document} scrollTop={0} - down={this._marqueeing} - scaling={this.marqueeFitScaling} - docView={this.props.docViewPath().slice(-1)[0]} + annotationLayerScrollTop={0} + scaling={returnOne} + annotationLayerScaling={this._props.NativeDimScaling} + docView={this.DocumentView} containerOffset={this.marqueeOffset} addDocument={this.addDocWithTimecode} finishMarquee={this.finishMarquee} savedAnnotations={this.savedAnnotations} selectionText={returnEmptyString} annotationLayer={this._annotationLayer.current} - mainCont={this._mainCont.current} + marqueeContainer={this._mainCont.current} anchorMenuCrop={this.crop} /> )} @@ -1120,7 +1005,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp } @computed get UIButtons() { - const bounds = this.props.docViewPath().lastElement().getBounds(); + const bounds = this.DocumentView?.().getBounds; const width = (bounds?.right || 0) - (bounds?.left || 0); const curTime = NumCast(this.layoutDoc._layout_currentTimecode); return ( @@ -1225,5 +1110,3 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp ); } } - -VideoBox._nativeControls = false; diff --git a/src/client/views/nodes/WebBox.scss b/src/client/views/nodes/WebBox.scss index 511c91da0..a1686adaf 100644 --- a/src/client/views/nodes/WebBox.scss +++ b/src/client/views/nodes/WebBox.scss @@ -1,4 +1,4 @@ -@import '../global/globalCssVariables.scss'; +@import '../global/globalCssVariables.module.scss'; .webBox { height: 100%; diff --git a/src/client/views/nodes/WebBox.tsx b/src/client/views/nodes/WebBox.tsx index 58a765d61..5a07540da 100644 --- a/src/client/views/nodes/WebBox.tsx +++ b/src/client/views/nodes/WebBox.tsx @@ -1,22 +1,22 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { action, computed, IReactionDisposer, observable, ObservableMap, reaction, runInAction } from 'mobx'; +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 { Height, Width } from '../../../fields/DocSymbols'; 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, ImageCast, NumCast, StrCast, WebCast } from '../../../fields/Types'; +import { Cast, NumCast, StrCast, WebCast } from '../../../fields/Types'; import { ImageField, WebField } from '../../../fields/URLField'; import { TraceMobx } from '../../../fields/util'; import { addStyleSheet, addStyleSheetRule, clearStyleSheetRules, emptyFunction, getWordAtPoint, lightOrDark, returnFalse, returnOne, returnZero, setupMoveUpEvents, smoothScroll, Utils } from '../../../Utils'; import { Docs, DocUtils } from '../../documents/Documents'; import { DocumentManager } from '../../util/DocumentManager'; -import { DragManager } from '../../util/DragManager'; import { ScriptingGlobals } from '../../util/ScriptingGlobals'; import { SnappingManager } from '../../util/SnappingManager'; import { undoBatch, UndoManager } from '../../util/UndoManager'; @@ -24,7 +24,7 @@ import { MarqueeOptionsMenu } from '../collections/collectionFreeForm'; import { CollectionFreeFormView } from '../collections/collectionFreeForm/CollectionFreeFormView'; import { ContextMenu } from '../ContextMenu'; import { ContextMenuProps } from '../ContextMenuItem'; -import { ViewBoxAnnotatableComponent, ViewBoxAnnotatableProps } from '../DocComponent'; +import { ViewBoxAnnotatableComponent, ViewBoxInterface } from '../DocComponent'; import { Colors } from '../global/globalEnums'; import { LightboxView } from '../LightboxView'; import { MarqueeAnnotator } from '../MarqueeAnnotator'; @@ -33,17 +33,15 @@ import { Annotation } from '../pdf/Annotation'; import { GPTPopup } from '../pdf/GPTPopup/GPTPopup'; import { SidebarAnnos } from '../SidebarAnnos'; import { StyleProp } from '../StyleProvider'; -import { DocComponentView, DocFocusOptions, DocumentView, DocumentViewProps, OpenWhere } from './DocumentView'; -import { FieldView, FieldViewProps } from './FieldView'; -import { LinkDocPreview } from './LinkDocPreview'; +import { DocumentView, OpenWhere } from './DocumentView'; +import { FocusViewOptions, FieldView, FieldViewProps } from './FieldView'; +import { LinkInfo } from './LinkDocPreview'; import { PinProps, PresBox } from './trails'; import './WebBox.scss'; -import React = require('react'); const { CreateImage } = require('./WebBoxRenderer'); const _global = (window /* browser */ || global) /* node */ as any; -const htmlToText = require('html-to-text'); @observer -export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps & FieldViewProps>() { +export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implements ViewBoxInterface { public static LayoutString(fieldKey: string) { return FieldView.LayoutString(WebBox, fieldKey); } @@ -51,9 +49,9 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps public static sidebarResizerWidth = 5; static webStyleSheet = addStyleSheet(); private _setPreviewCursor: undefined | ((x: number, y: number, drag: boolean, hide: boolean, doc: Opt<Doc>) => void); - private _setBrushViewer: undefined | ((view: { width: number; height: number; panX: number; panY: number }, transTime: number) => void); private _mainCont: React.RefObject<HTMLDivElement> = React.createRef(); private _outerRef: React.RefObject<HTMLDivElement> = React.createRef(); + private _marqueeref = React.createRef<MarqueeAnnotator>(); private _disposers: { [name: string]: IReactionDisposer } = {}; private _annotationLayer: React.RefObject<HTMLDivElement> = React.createRef(); private _keyInput = React.createRef<HTMLInputElement>(); @@ -69,10 +67,15 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps @observable private _searching: boolean = false; @observable private _showSidebar = false; @observable private _webPageHasBeenRendered = false; - @observable private _overlayAnnoInfo: Opt<Doc>; - @observable private _marqueeing: number[] | undefined; - @observable private _isAnnotating = false; - @observable private _iframeClick: HTMLIFrameElement | undefined = undefined; + @observable private _marqueeing: number[] | undefined = undefined; + get marqueeing() { + return this._marqueeing; + } + set marqueeing(val) { + val && this._marqueeref.current?.onInitiateSelection(val); + !val && this._marqueeref.current?.onTerminateSelection(); + this._marqueeing = val; + } @observable private _iframe: HTMLIFrameElement | null = null; @observable private _savedAnnotations = new ObservableMap<number, HTMLDivElement[]>(); @observable private _scrollHeight = NumCast(this.layoutDoc.scrollHeight); @@ -83,7 +86,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps return this._url ? WebBox.urlHash(this._url) + '' : ''; } @computed get scrollHeight() { - return Math.max(this.layoutDoc[Height](), this._scrollHeight); + return Math.max(NumCast(this.layoutDoc._height), this._scrollHeight); } @computed get allAnnotations() { return DocListCast(this.dataDoc[this.annotationKey]); @@ -92,12 +95,13 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps return this.allAnnotations.filter(a => a.text_inlineAnnotations); } @computed get webField() { - return Cast(this.rootDoc[this.props.fieldKey], WebField)?.url; + return Cast(this.Document[this._props.fieldKey], WebField)?.url; } - constructor(props: any) { + constructor(props: FieldViewProps) { super(props); - runInAction(() => (this._webUrl = this._url)); // setting the weburl will change the src parameter of the embedded iframe and force a navigation to it. + makeObservable(this); + this._webUrl = this._url; // setting the weburl will change the src parameter of the embedded iframe and force a navigation to it. } @action @@ -136,13 +140,13 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps if (!this._iframe) return; const scrollTop = NumCast(this.layoutDoc._layout_scrollTop); const nativeWidth = NumCast(this.layoutDoc.nativeWidth); - const nativeHeight = (nativeWidth * this.props.PanelHeight()) / this.props.PanelWidth(); + const nativeHeight = (nativeWidth * this._props.PanelHeight()) / this._props.PanelWidth(); var htmlString = this._iframe.contentDocument && new XMLSerializer().serializeToString(this._iframe.contentDocument); if (!htmlString) { htmlString = await (await fetch(Utils.CorsProxy(this.webField!.href))).text(); } this.layoutDoc.thumb = undefined; - this.rootDoc.thumbLockout = true; // lock to prevent multiple thumb updates. + 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')) { @@ -152,7 +156,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps Utils.convertDataUri(data_url, this.layoutDoc[Id] + '-icon' + new Date().getTime(), true, this.layoutDoc[Id] + '-icon').then(returnedfilename => setTimeout( action(() => { - this.rootDoc.thumbLockout = false; + this.Document.thumbLockout = false; this.layoutDoc.thumb = new ImageField(returnedfilename); this.layoutDoc.thumbScrollTop = scrollTop; this.layoutDoc.thumbNativeWidth = nativeWidth; @@ -167,8 +171,8 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps }); }; - async componentDidMount() { - this.props.setContentView?.(this); // this tells the DocumentView that this WebBox is the "content" of the document. this allows the DocumentView to call WebBox relevant methods to configure the UI (eg, show back/forward buttons) + componentDidMount() { + this._props.setContentViewBox?.(this); // this tells the DocumentView that this WebBox is the "content" of the document. this allows the DocumentView to call WebBox relevant methods to configure the UI (eg, show back/forward buttons) runInAction(() => { this._annotationKeySuffix = () => (this._urlHash ? this._urlHash + '_' : '') + 'annotations'; @@ -182,11 +186,11 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps } }); this._disposers.urlchange = reaction( - () => WebCast(this.rootDoc.data), + () => WebCast(this.dataDoc.data), url => this.submitURL(false, false) ); this._disposers.titling = reaction( - () => StrCast(this.rootDoc.title), + () => StrCast(this.Document.title), url => { url.startsWith('www') && this.setData('http://' + url); url.startsWith('http') && this.setData(url); @@ -197,8 +201,8 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps () => this.layoutDoc._layout_autoHeight, layout_autoHeight => { if (layout_autoHeight) { - this.layoutDoc._nativeHeight = NumCast(this.props.Document[this.props.fieldKey + '_nativeHeight']); - this.props.setHeight?.(NumCast(this.props.Document[this.props.fieldKey + '_nativeHeight']) * (this.props.NativeDimScaling?.() || 1)); + this.layoutDoc._nativeHeight = NumCast(this.Document[this._props.fieldKey + '_nativeHeight']); + this._props.setHeight?.(NumCast(this.Document[this._props.fieldKey + '_nativeHeight']) * (this._props.NativeDimScaling?.() || 1)); } } ); @@ -211,14 +215,12 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps if (!nativeWidth || !nativeHeight || Math.abs(nativeWidth / nativeHeight - youtubeaspect) > 0.05) { if (!nativeWidth) Doc.SetNativeWidth(this.layoutDoc, 600); Doc.SetNativeHeight(this.layoutDoc, (nativeWidth || 600) / youtubeaspect); - this.layoutDoc._height = this.layoutDoc[Width]() / youtubeaspect; + this.layoutDoc._height = NumCast(this.layoutDoc._width) / youtubeaspect; } } // else it's an HTMLfield } else if (this.webField && !this.dataDoc.text) { - const result = await WebRequest.get(Utils.CorsProxy(this.webField.href)); - if (result) { - this.dataDoc.text = htmlToText.fromString(result.content); - } + WebRequest.get(Utils.CorsProxy(this.webField.href)) // + .then(result => result && (this.dataDoc.text = htmlToText(result.content))); } this._disposers.scrollReaction = reaction( @@ -233,7 +235,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps { fireImmediately: true } ); } - @action componentWillUnmount() { + componentWillUnmount() { this._iframetimeout && clearTimeout(this._iframetimeout); this._iframetimeout = undefined; Object.values(this._disposers).forEach(disposer => disposer?.()); @@ -248,6 +250,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps @action createTextAnnotation = (sel: Selection, selRange: Range | undefined) => { if (this._mainCont.current && selRange) { + if (this.dataDoc[this._props.fieldKey] instanceof HtmlField) this._mainCont.current.style.transform = `rotate(${NumCast(this.DocumentView!().screenToContentsTransform().RotateDeg)}deg)`; const clientRects = selRange.getClientRects(); for (let i = 0; i < clientRects.length; i++) { const rect = clientRects.item(i); @@ -255,7 +258,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps if (rect && rect.width !== this._mainCont.current.clientWidth) { const annoBox = document.createElement('div'); annoBox.className = 'marqueeAnnotator-annotationBox'; - const scale = this._url ? 1 : this.props.ScreenToLocalTransform().Scale; + const scale = this._url ? 1 : this.ScreenToLocalBoxXf().Scale; // transforms the positions from screen onto the pdf div annoBox.style.top = ((rect.top - mainrect.translateY) * scale + (this._url ? this._mainCont.current.scrollTop : NumCast(this.layoutDoc.layout_scrollTop))).toString(); annoBox.style.left = ((rect.left - mainrect.translateX) * scale).toString(); @@ -264,23 +267,23 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps this._annotationLayer.current && MarqueeAnnotator.previewNewAnnotation(this._savedAnnotations, this._annotationLayer.current, annoBox, 1); } } + this._mainCont.current.style.transform = ''; } - //this._selectionText = selRange.cloneContents().textContent || ""; this._selectionContent = selRange?.cloneContents(); this._selectionText = this._selectionContent?.textContent || ''; // clear selection - if (sel.empty) sel.empty(); // Chrome + this._textAnnotationCreator = undefined; + if (sel.empty) + sel.empty(); // Chrome else if (sel.removeAllRanges) sel.removeAllRanges(); // Firefox return this._savedAnnotations; }; - setBrushViewer = (func?: (view: { width: number; height: number; panX: number; panY: number }, transTime: number) => void) => (this._setBrushViewer = func); - brushView = (view: { width: number; height: number; panX: number; panY: number }, transTime: number) => this._setBrushViewer?.(view, transTime); - focus = (anchor: Doc, options: DocFocusOptions) => { - if (anchor !== this.rootDoc && this._outerRef.current) { - const windowHeight = this.props.PanelHeight() / (this.props.NativeDimScaling?.() || 1); - const scrollTo = Utils.scrollIntoView(NumCast(anchor.y), anchor[Height](), NumCast(this.layoutDoc._layout_scrollTop), windowHeight, windowHeight * 0.1, Math.max(NumCast(anchor.y) + anchor[Height](), this._scrollHeight)); + 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)); if (scrollTo !== undefined) { if (this._initialScroll === undefined) { const focusTime = options.zoomTime ?? 500; @@ -294,9 +297,9 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps }; @action - getView = (doc: Doc) => { - if (Doc.AreProtosEqual(doc, this.rootDoc)) return new Promise<Opt<DocumentView>>(res => res(this.props.DocumentView?.())); - if (this.rootDoc.layout_fieldKey === 'layout_icon') this.props.DocumentView?.().iconify(); + 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); @@ -304,11 +307,11 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps }; sidebarAddDocTab = (doc: Doc, where: OpenWhere) => { - if (DocListCast(this.props.Document[this.props.fieldKey + '_sidebar']).includes(doc) && !this.SidebarShown) { + if (DocListCast(this.Document[this._props.fieldKey + '_sidebar']).includes(doc) && !this.SidebarShown) { this.toggleSidebar(false); return true; } - return this.props.addDocTab(doc, where); + return this._props.addDocTab(doc, where); }; getAnchor = (addAsAnnotation: boolean, pinProps?: PinProps) => { let ele: Opt<HTMLDivElement> = undefined; @@ -319,15 +322,15 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps ele.append(contents); } } catch (e) {} - const visibleAnchor = this._getAnchor(this._savedAnnotations, false); + const visibleAnchor = this._getAnchor(this._savedAnnotations, true); const anchor = visibleAnchor ?? Docs.Create.ConfigDocument({ - title: StrCast(this.rootDoc.title + ' ' + this.layoutDoc._layout_scrollTop), + title: StrCast(this.Document.title + ' ' + this.layoutDoc._layout_scrollTop), y: NumCast(this.layoutDoc._layout_scrollTop), - annotationOn: this.rootDoc, + annotationOn: this.Document, }); - PresBox.pinDocView(anchor, { pinDocLayout: pinProps?.pinDocLayout, pinData: { ...(pinProps?.pinData ?? {}), scrollable: pinProps?.pinData ? true : false, pannable: true } }, this.rootDoc); + PresBox.pinDocView(anchor, { pinDocLayout: pinProps?.pinDocLayout, pinData: { ...(pinProps?.pinData ?? {}), scrollable: pinProps?.pinData ? true : false, pannable: true } }, this.Document); anchor.text = ele?.textContent ?? ''; anchor.text_html = ele?.innerHTML ?? this._selectionText; addAsAnnotation && this.addDocumentWrapper(anchor); @@ -339,11 +342,12 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps @action iframeUp = (e: PointerEvent) => { + this._getAnchor = AnchorMenu.Instance?.GetAnchor; // need to save AnchorMenu's getAnchor since a subsequent selection on another doc will overwrite this value this._textAnnotationCreator = undefined; - this.props.docViewPath().lastElement()?.docView?.cleanupPointerEvents(); // pointerup events aren't generated on containing document view, so we have to invoke it here. + 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 scale = (this.props.NativeDimScaling?.() || 1) * mainContBounds.scale; + const scale = (this._props.NativeDimScaling?.() || 1) * mainContBounds.scale; const sel = this._iframe.contentWindow.getSelection(); if (sel) { this._selectionText = sel.toString(); @@ -351,33 +355,35 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps this._textAnnotationCreator = () => this.createTextAnnotation(sel, !sel.isCollapsed ? sel.getRangeAt(0) : undefined); AnchorMenu.Instance.jumpTo(e.clientX * scale + mainContBounds.translateX, e.clientY * scale + mainContBounds.translateY - NumCast(this.layoutDoc._layout_scrollTop) * scale); // Changing which document to add the annotation to (the currently selected WebBox) - GPTPopup.Instance.setSidebarId(`${this.props.fieldKey}_${this._urlHash ? this._urlHash + '_' : ''}sidebar`); + GPTPopup.Instance.setSidebarId(`${this._props.fieldKey}_${this._urlHash ? this._urlHash + '_' : ''}sidebar`); GPTPopup.Instance.addDoc = this.sidebarAddDocument; } } }; @action webClipDown = (e: React.PointerEvent) => { - const mainContBounds = Utils.GetScreenTransform(this._mainCont.current!); + e.stopPropagation(); + const sel = window.getSelection(); + this._textAnnotationCreator = undefined; + if (sel?.empty) + sel.empty(); // Chrome + else if (sel?.removeAllRanges) sel.removeAllRanges(); // Firefox + // bcz: NEED TO unrotate e.clientX and e.clientY const word = getWordAtPoint(e.target, e.clientX, e.clientY); - this._setPreviewCursor?.(e.clientX, e.clientY, false, true, this.rootDoc); + this._setPreviewCursor?.(e.clientX, e.clientY, false, true, this.Document); MarqueeAnnotator.clearAnnotations(this._savedAnnotations); - e.button !== 2 && (this._marqueeing = [e.clientX, e.clientY]); - if (word || (e.target as any)?.className?.includes('rangeslider') || (e.target as any)?.onclick || (e.target as any)?.parentNode?.onclick) { - e.stopPropagation(); - setTimeout( - 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. - } else { - this._isAnnotating = true; - this.props.select(false); - e.stopPropagation(); + + if (!word && !(e.target as any)?.className?.includes('rangeslider') && !(e.target as any)?.onclick && !(e.target as any)?.parentNode?.onclick) { + if (e.button !== 2) this.marqueeing = [e.clientX, e.clientY]; e.preventDefault(); } document.addEventListener('pointerup', this.webClipUp); }; + @action webClipUp = (e: PointerEvent) => { + if (window.getSelection()?.isCollapsed && this._marqueeref.current?.isEmpty) { + this.marqueeing = undefined; + } document.removeEventListener('pointerup', this.webClipUp); this._getAnchor = AnchorMenu.Instance?.GetAnchor; // need to save AnchorMenu's getAnchor since a subsequent selection on another doc will overwrite this value const sel = window.getSelection(); @@ -386,44 +392,27 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps this._selectionText = sel.toString(); AnchorMenu.Instance.setSelectedText(sel.toString()); this._textAnnotationCreator = () => this.createTextAnnotation(sel, selRange); - AnchorMenu.Instance.jumpTo(e.clientX, e.clientY); + (!sel.isCollapsed || this.marqueeing) && AnchorMenu.Instance.jumpTo(e.clientX, e.clientY); // Changing which document to add the annotation to (the currently selected WebBox) - GPTPopup.Instance.setSidebarId(`${this.props.fieldKey}_${this._urlHash ? this._urlHash + '_' : ''}sidebar`); + GPTPopup.Instance.setSidebarId(`${this._props.fieldKey}_${this._urlHash ? this._urlHash + '_' : ''}sidebar`); GPTPopup.Instance.addDoc = this.sidebarAddDocument; } }; @action iframeDown = (e: PointerEvent) => { - const mainContBounds = Utils.GetScreenTransform(this._mainCont.current!); - const scale = (this.props.NativeDimScaling?.() || 1) * mainContBounds.scale; - const word = getWordAtPoint(e.target, e.clientX, e.clientY); - this._setPreviewCursor?.(e.clientX, e.clientY, false, true, this.rootDoc); + this._props.select(false); + const theclick = this.props + .ScreenToLocalTransform() + .inverse() + .transformPoint(e.clientX, e.clientY - NumCast(this.layoutDoc.layout_scrollTop)); MarqueeAnnotator.clearAnnotations(this._savedAnnotations); - e.button !== 2 && (this._marqueeing = [e.clientX * scale + mainContBounds.translateX, e.clientY * scale + mainContBounds.translateY - NumCast(this.layoutDoc._layout_scrollTop) * scale]); - if (word || (e.target as any)?.className?.includes('rangeslider') || (e.target as any)?.onclick || (e.target as any)?.parentNode?.onclick) { - setTimeout( - 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. - } else { - this._iframeClick = this._iframe ?? undefined; - this._isAnnotating = true; - this.props.select(false); - e.stopPropagation(); + const word = getWordAtPoint(e.target, e.clientX, e.clientY); + if (!word && !(e.target as any)?.className?.includes('rangeslider') && !(e.target as any)?.onclick && !(e.target as any)?.parentNode?.onclick) { + this.marqueeing = theclick; e.preventDefault(); } - - // bcz: hack - iframe grabs all events which messes up how we handle contextMenus. So this super naively simulates the event stack to get the specific menu items and the doc view menu items. - if (e.button === 2 || (e.button === 0 && e.altKey)) { - e.preventDefault(); - //e.stopPropagation(); - ContextMenu.Instance.closeMenu(); - ContextMenu.Instance.setIgnoreEvents(true); - } }; isFirefox = () => 'InstallTrigger' in window; // navigator.userAgent.indexOf("Chrome") !== -1; - iframeClick = () => this._iframeClick; - iframeScaling = () => 1 / this.props.ScreenToLocalTransform().Scale; addWebStyleSheet(document: any, styleType: string = 'text/css') { if (document) { @@ -493,16 +482,16 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps const initHeights = () => { this._scrollHeight = Math.max(this._scrollHeight, iframeContent.body.scrollHeight || 0); if (this._scrollHeight) { - this.rootDoc.nativeHeight = Math.min(NumCast(this.rootDoc.nativeHeight), this._scrollHeight); - this.layoutDoc.height = Math.min(this.layoutDoc[Height](), (this.layoutDoc[Width]() * NumCast(this.rootDoc.nativeHeight)) / NumCast(this.rootDoc.nativeWidth)); + this.Document.nativeHeight = Math.min(NumCast(this.Document.nativeHeight), this._scrollHeight); + this.layoutDoc.height = Math.min(NumCast(this.layoutDoc._height), (NumCast(this.layoutDoc._width) * NumCast(this.Document.nativeHeight)) / NumCast(this.Document.nativeWidth)); } }; - const swidth = Math.max(NumCast(this.layoutDoc.nativeWidth), iframeContent.body.scrollWidth || 0); + const swidth = Math.max(NumCast(this.Document.nativeWidth), iframeContent.body.scrollWidth || 0); if (swidth) { - const aspectResize = swidth / NumCast(this.rootDoc.nativeWidth); - this.rootDoc.nativeWidth = swidth; - this.rootDoc.nativeHeight = NumCast(this.rootDoc.nativeHeight) * aspectResize; - this.layoutDoc.height = this.layoutDoc[Height]() * aspectResize; + const aspectResize = swidth / NumCast(this.Document.nativeWidth, swidth); + this.layoutDoc.height = NumCast(this.layoutDoc._height) * aspectResize; + this.Document.nativeWidth = swidth; + this.Document.nativeHeight = (swidth * NumCast(this.layoutDoc._height)) / NumCast(this.layoutDoc._width); } initHeights(); this._iframetimeout && clearTimeout(this._iframetimeout); @@ -569,7 +558,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps clearStyleSheetRules(WebBox.webStyleSheet); this._scrollTimer = undefined; const newScrollTop = scrollTop > iframeHeight ? iframeHeight : scrollTop; - if (!LinkDocPreview.LinkInfo && this._outerRef.current && newScrollTop !== this.layoutDoc.thumbScrollTop && (!LightboxView.LightboxDoc || LightboxView.IsLightboxDocView(this.props.docViewPath()))) { + if (!LinkInfo.Instance?.LinkInfo && this._outerRef.current && newScrollTop !== this.layoutDoc.thumbScrollTop && (!LightboxView.LightboxDoc || LightboxView.Contains(this.DocumentView?.()))) { this.layoutDoc.thumb = undefined; this.layoutDoc.thumbScrollTop = undefined; this.layoutDoc.thumbNativeWidth = undefined; @@ -591,8 +580,8 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps }; forward = (checkAvailable?: boolean) => { - const future = Cast(this.rootDoc[this.fieldKey + '_future'], listSpec('string'), []); - const history = Cast(this.rootDoc[this.fieldKey + '_history'], listSpec('string'), []); + const future = Cast(this.dataDoc[this.fieldKey + '_future'], listSpec('string'), []); + const history = Cast(this.dataDoc[this.fieldKey + '_history'], listSpec('string'), []); if (checkAvailable) return future.length; runInAction(() => { if (future.length) { @@ -613,8 +602,8 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps }; back = (checkAvailable?: boolean) => { - const future = Cast(this.rootDoc[this.fieldKey + '_future'], listSpec('string')); - const history = Cast(this.rootDoc[this.fieldKey + '_history'], listSpec('string'), []); + const future = Cast(this.dataDoc[this.fieldKey + '_future'], listSpec('string')); + const history = Cast(this.dataDoc[this.fieldKey + '_history'], listSpec('string'), []); if (checkAvailable) return history.length; runInAction(() => { if (history.length) { @@ -682,7 +671,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps if (Field.toString(data) === this._url) return false; this._scrollHeight = 0; const oldUrl = this._url; - const history = Cast(this.rootDoc[this.fieldKey + '_history'], listSpec('string'), []); + const history = Cast(this.dataDoc[this.fieldKey + '_history'], listSpec('string'), []); const weburl = new WebField(Field.toString(data)); this.dataDoc[this.fieldKey + '_future'] = new List<string>([]); this.dataDoc[this.fieldKey + '_history'] = new List<string>([...(history || []), oldUrl]); @@ -712,10 +701,10 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps icon: 'snowflake', }); funcs.push({ - description: (!this.layoutDoc.layout_forceReflow ? 'Force' : 'Prevent') + ' Reflow', + description: (!this.layoutDoc.layout_reflowHorizontal ? 'Force' : 'Prevent') + ' Reflow', event: () => { - const nw = !this.layoutDoc.layout_forceReflow ? undefined : Doc.NativeWidth(this.layoutDoc) - this.sidebarWidth() / (this.props.NativeDimScaling?.() || 1); - this.layoutDoc.layout_forceReflow = !nw; + const nw = !this.layoutDoc.layout_reflowHorizontal ? undefined : Doc.NativeWidth(this.layoutDoc) - this.sidebarWidth() / (this._props.NativeDimScaling?.() || 1); + this.layoutDoc.layout_reflowHorizontal = !nw; if (nw) { Doc.SetInPlace(this.layoutDoc, this.fieldKey + '_nativeWidth', nw, true); } @@ -727,38 +716,58 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps } }; + /** + * This gets called when some other child of the webbox is selected and a pointer down occurs on the webbox. + * it's also called for html clippings when you click outside the bounds of the clipping + * @param e + */ @action onMarqueeDown = (e: React.PointerEvent) => { - if (!e.altKey && e.button === 0 && this.props.isContentActive(true) && ![InkTool.Highlighter, InkTool.Pen, InkTool.Write].includes(Doc.ActiveTool)) { + const sel = this._url ? this._iframe?.contentDocument?.getSelection() : window.document.getSelection(); + this._textAnnotationCreator = undefined; + if (sel?.empty) + sel.empty(); // Chrome + else if (sel?.removeAllRanges) sel.removeAllRanges(); // Firefox + this.marqueeing = [e.clientX, e.clientY]; + if (!e.altKey && e.button === 0 && this._props.isContentActive() && ![InkTool.Highlighter, InkTool.Pen, InkTool.Write].includes(Doc.ActiveTool)) { setupMoveUpEvents( this, e, action(e => { MarqueeAnnotator.clearAnnotations(this._savedAnnotations); - this._marqueeing = [e.clientX, e.clientY]; return true; }), returnFalse, - () => MarqueeAnnotator.clearAnnotations(this._savedAnnotations), + action(() => { + this.marqueeing = undefined; + MarqueeAnnotator.clearAnnotations(this._savedAnnotations); + }), false ); + } else { + this.marqueeing = undefined; } }; @action finishMarquee = (x?: number, y?: number, e?: PointerEvent) => { this._getAnchor = AnchorMenu.Instance?.GetAnchor; - this._marqueeing = undefined; - this._isAnnotating = false; - this._iframeClick = undefined; + this.marqueeing = undefined; + this._textAnnotationCreator = undefined; const sel = this._url ? this._iframe?.contentDocument?.getSelection() : window.document.getSelection(); - if (sel?.empty) sel.empty(); // Chrome + if (sel?.empty) + sel.empty(); // Chrome else if (sel?.removeAllRanges) sel.removeAllRanges(); // Firefox + this._setPreviewCursor?.(x ?? 0, y ?? 0, false, !this._marqueeref.current?.isEmpty, this.Document); if (x !== undefined && y !== undefined) { - this._setPreviewCursor?.(x, y, false, false, this.rootDoc); ContextMenu.Instance.closeMenu(); ContextMenu.Instance.setIgnoreEvents(false); if (e?.button === 2 || e?.altKey) { - this.specificContextMenu(undefined as any); - this.props.docViewPath().lastElement().docView?.onContextMenu(undefined, x, y); + e?.preventDefault(); + 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.DocumentView?.().onContextMenu(undefined, x, y); + }); } } }; @@ -766,6 +775,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps @observable lighttext = false; @computed get urlContent() { + if (this.ScreenToLocalBoxXf().Scale > 25) return <div></div>; setTimeout( action(() => { if (this._initialScroll === undefined && !this._webPageHasBeenRendered) { @@ -774,7 +784,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps this._webPageHasBeenRendered = true; }) ); - const field = this.rootDoc[this.props.fieldKey]; + const field = this.dataDoc[this._props.fieldKey]; if (field instanceof HtmlField) { return ( <span @@ -800,7 +810,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps key={this._warning} className="webBox-iframe" ref={action((r: HTMLIFrameElement | null) => (this._iframe = r))} - style={{ pointerEvents: this._isAnyChildContentActive || DocumentView.Interacting ? 'none' : undefined }} + style={{ pointerEvents: SnappingManager.IsResizing ? 'none' : undefined }} src={url} onLoad={this.iframeLoaded} scrolling="no" // ugh.. on windows, I get an inner scroll bar for the iframe's body even though the scrollHeight should be set to the full height of the document. @@ -831,18 +841,18 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps e, action((e, down, delta) => { this._draggingSidebar = true; - const localDelta = this.props + const localDelta = this._props .ScreenToLocalTransform() - .scale(this.props.NativeDimScaling?.() || 1) + .scale(this._props.NativeDimScaling?.() || 1) .transformDirection(delta[0], delta[1]); const nativeWidth = NumCast(this.layoutDoc[this.fieldKey + '_nativeWidth']); const nativeHeight = NumCast(this.layoutDoc[this.fieldKey + '_nativeHeight']); const curNativeWidth = NumCast(this.layoutDoc.nativeWidth, nativeWidth); - const ratio = (curNativeWidth + ((onButton ? 1 : -1) * localDelta[0]) / (this.props.NativeDimScaling?.() || 1)) / nativeWidth; + const ratio = (curNativeWidth + ((onButton ? 1 : -1) * localDelta[0]) / (this._props.NativeDimScaling?.() || 1)) / nativeWidth; if (ratio >= 1) { this.layoutDoc.nativeWidth = nativeWidth * ratio; this.layoutDoc.nativeHeight = nativeHeight * (1 + ratio); - onButton && (this.layoutDoc._width = this.layoutDoc[Width]() + localDelta[0]); + onButton && (this.layoutDoc._width = NumCast(this.layoutDoc._width) + localDelta[0]); this.layoutDoc._layout_showSidebar = nativeWidth !== this.layoutDoc._nativeWidth; } return false; @@ -864,7 +874,6 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps key="sidebar" title="Toggle Sidebar" style={{ - display: !this.props.isContentActive() ? 'none' : undefined, top: StrCast(this.layoutDoc._layout_showTitle) === 'title' ? 20 : 5, backgroundColor: this.SidebarShown ? Colors.MEDIUM_BLUE : Colors.BLACK, }} @@ -878,31 +887,32 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps toggleSidebar = action((preview: boolean = false) => { var nativeWidth = NumCast(this.layoutDoc[this.fieldKey + '_nativeWidth']); if (!nativeWidth) { - const defaultNativeWidth = this.rootDoc[this.fieldKey] instanceof WebField ? 850 : this.Document[Width](); + 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); - Doc.SetNativeHeight(this.dataDoc, Doc.NativeHeight(this.dataDoc) || (this.Document[Height]() / this.Document[Width]()) * defaultNativeWidth); + Doc.SetNativeHeight(this.dataDoc, Doc.NativeHeight(this.dataDoc) || (NumCast(this.Document._height) / NumCast(this.Document._width)) * defaultNativeWidth); nativeWidth = NumCast(this.layoutDoc[this.fieldKey + '_nativeWidth']); } const sideratio = ((!this.layoutDoc.nativeWidth || this.layoutDoc.nativeWidth === nativeWidth ? WebBox.openSidebarWidth : 0) + nativeWidth) / nativeWidth; - const pdfratio = ((!this.layoutDoc.nativeWidth || this.layoutDoc.nativeWidth === nativeWidth ? WebBox.openSidebarWidth + WebBox.sidebarResizerWidth : 0) + nativeWidth) / nativeWidth; + const pdfratio = ((!this.layoutDoc.nativeWidth || this.layoutDoc.nativeWidth === nativeWidth ? WebBox.openSidebarWidth + WebBox.sidebarResizerWidth : 0) + NumCast(this.layoutDoc.width)) / NumCast(this.layoutDoc.width); const curNativeWidth = NumCast(this.layoutDoc.nativeWidth, nativeWidth); if (preview) { this._previewNativeWidth = nativeWidth * sideratio; - this._previewWidth = (this.layoutDoc[Width]() * nativeWidth * sideratio) / curNativeWidth; + this._previewWidth = (NumCast(this.layoutDoc._width) * nativeWidth * sideratio) / curNativeWidth; this._showSidebar = true; } else { this.layoutDoc._layout_showSidebar = !this.layoutDoc._layout_showSidebar; - this.layoutDoc._width = (this.layoutDoc[Width]() * nativeWidth * pdfratio) / curNativeWidth; + this.layoutDoc._width = (NumCast(this.layoutDoc._width) * nativeWidth * pdfratio) / curNativeWidth; if (!this.layoutDoc._layout_showSidebar && !(this.dataDoc[this.fieldKey] instanceof WebField)) { this.layoutDoc.nativeWidth = this.dataDoc[this.fieldKey + '_nativeWidth'] = undefined; } else { + !this.layoutDoc._layout_showSidebar && (this.dataDoc[this.fieldKey + '_nativeWidth'] = this.dataDoc[this.fieldKey + '_nativeHeight'] = undefined); this.layoutDoc.nativeWidth = nativeWidth * pdfratio; } } }); @action onZoomWheel = (e: React.WheelEvent) => { - if (this.props.isContentActive(true)) { + if (this._props.isContentActive()) { e.stopPropagation(); } }; @@ -910,20 +920,20 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps if (!this.SidebarShown) return 0; if (this._previewWidth) return WebBox.sidebarResizerWidth + WebBox.openSidebarWidth; // return default sidebar if previewing (as in viewing a link target) const nativeDiff = NumCast(this.layoutDoc.nativeWidth) - Doc.NativeWidth(this.dataDoc); - return WebBox.sidebarResizerWidth + nativeDiff * (this.props.NativeDimScaling?.() || 1); + return WebBox.sidebarResizerWidth + nativeDiff * (this._props.NativeDimScaling?.() || 1); }; _innerCollectionView: CollectionFreeFormView | undefined; zoomScaling = () => this._innerCollectionView?.zoomScaling() ?? 1; - setInnerContent = (component: DocComponentView) => (this._innerCollectionView = component as CollectionFreeFormView); + setInnerContent = (component: ViewBoxInterface) => (this._innerCollectionView = component as CollectionFreeFormView); @computed get content() { - const interactive = this.props.isContentActive() && this.props.pointerEvents?.() !== 'none' && Doc.ActiveTool === InkTool.None; + const interactive = this._props.isContentActive() && this._props.pointerEvents?.() !== 'none' && Doc.ActiveTool === InkTool.None; return ( <div className={'webBox-cont' + (interactive ? '-interactive' : '')} onKeyDown={e => e.stopPropagation()} style={{ - width: !this.layoutDoc.layout_forceReflow ? NumCast(this.layoutDoc[this.fieldKey + '_nativeWidth']) || `100%` : '100%', + width: !this.layoutDoc.layout_reflowHorizontal ? NumCast(this.layoutDoc[this.fieldKey + '_nativeWidth']) || `100%` : '100%', transform: `scale(${this.zoomScaling()}) translate(${-NumCast(this.layoutDoc.freeform_panX)}px, ${-NumCast(this.layoutDoc.freeform_panY)}px)`, }}> {this._hackHide ? null : this.urlContent} @@ -945,7 +955,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps {this.inlineTextAnnotations .sort((a, b) => NumCast(a.y) - NumCast(b.y)) .map(anno => ( - <Annotation {...this.props} fieldKey={this.annotationKey} pointerEvents={this.pointerEvents} showInfo={this.showInfo} dataDoc={this.dataDoc} anno={anno} key={`${anno[Id]}-annotation`} /> + <Annotation {...this._props} fieldKey={this.annotationKey} pointerEvents={this.pointerEvents} dataDoc={this.dataDoc} anno={anno} key={`${anno[Id]}-annotation`} /> ))} </div> ); @@ -953,43 +963,47 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps @computed get SidebarShown() { return this._showSidebar || this.layoutDoc._layout_showSidebar ? true : false; } + renderAnnotations = (childFilters: () => string[]) => ( + <CollectionFreeFormView + {...this._props} + setContentViewBox={this.setInnerContent} + NativeWidth={returnZero} + NativeHeight={returnZero} + originTopLeft={false} + isAnnotationOverlayScrollable={true} + renderDepth={this._props.renderDepth + 1} + isAnnotationOverlay={true} + fieldKey={this.annotationKey} + setPreviewCursor={this.setPreviewCursor} + PanelWidth={this.panelWidth} + PanelHeight={this.panelHeight} + ScreenToLocalTransform={this.scrollXf} + NativeDimScaling={returnOne} + focus={this.focus} + childFilters={childFilters} + select={emptyFunction} + isAnyChildContentActive={returnFalse} + styleProvider={this.childStyleProvider} + whenChildContentsActiveChanged={this.whenChildContentsActiveChanged} + removeDocument={this.removeDocument} + moveDocument={this.moveDocument} + addDocument={this.addDocumentWrapper} + childPointerEvents={this.childPointerEvents} + pointerEvents={this.annotationPointerEvents} + /> + ); - childPointerEvents = () => (this.props.isContentActive() ? 'all' : undefined); + @computed get renderOpaqueAnnotations() { + return this.renderAnnotations(this.opaqueFilter); + } + @computed get renderTransparentAnnotations() { + return this.renderAnnotations(this.transparentFilter); + } + childPointerEvents = () => (this._props.isContentActive() ? 'all' : undefined); @computed get webpage() { const previewScale = this._previewNativeWidth ? 1 - this.sidebarWidth() / this._previewNativeWidth : 1; - const pointerEvents = this.layoutDoc._lockedPosition ? 'none' : (this.props.pointerEvents?.() as any); - const scale = previewScale * (this.props.NativeDimScaling?.() || 1); - const renderAnnotations = (childFilters: () => string[]) => ( - <CollectionFreeFormView - {...this.props} - setContentView={this.setInnerContent} - NativeWidth={returnZero} - NativeHeight={returnZero} - originTopLeft={false} - isAnnotationOverlayScrollable={true} - renderDepth={this.props.renderDepth + 1} - isAnnotationOverlay={true} - fieldKey={this.annotationKey} - setPreviewCursor={this.setPreviewCursor} - setBrushViewer={this.setBrushViewer} - PanelWidth={this.panelWidth} - PanelHeight={this.panelHeight} - ScreenToLocalTransform={this.scrollXf} - NativeDimScaling={returnOne} - focus={this.focus} - childFilters={childFilters} - select={emptyFunction} - isAnyChildContentActive={returnFalse} - bringToFront={emptyFunction} - styleProvider={this.childStyleProvider} - whenChildContentsActiveChanged={this.whenChildContentsActiveChanged} - removeDocument={this.removeDocument} - moveDocument={this.moveDocument} - addDocument={this.addDocumentWrapper} - childPointerEvents={this.childPointerEvents} - pointerEvents={this.annotationPointerEvents} - /> - ); + const pointerEvents = this.layoutDoc._lockedPosition ? 'none' : (this._props.pointerEvents?.() as any); + const scale = previewScale * (this._props.NativeDimScaling?.() || 1); return ( <div className="webBox-outerContent" @@ -1002,10 +1016,10 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps onWheel={this.onZoomWheel} onScroll={e => this.setDashScrollTop(this._outerRef.current?.scrollTop || 0)} onPointerDown={this.onMarqueeDown}> - <div className="webBox-innerContent" style={{ height: (this._webPageHasBeenRendered && this._scrollHeight) || '100%', pointerEvents }}> + <div className="webBox-innerContent" style={{ height: (this._webPageHasBeenRendered && this._scrollHeight > this._props.PanelHeight() && this._scrollHeight) || '100%', pointerEvents }}> {this.content} - {<div style={{ display: DragManager.docsBeingDragged.length ? 'none' : undefined, mixBlendMode: 'multiply' }}>{renderAnnotations(this.transparentFilter)}</div>} - {renderAnnotations(this.opaqueFilter)} + <div style={{ display: SnappingManager.CanEmbed ? 'none' : undefined, mixBlendMode: 'multiply' }}>{this.renderTransparentAnnotations}</div> + {this.renderOpaqueAnnotations} {this.annotationLayer} </div> </div> @@ -1014,7 +1028,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps @computed get searchUI() { return ( - <div className="webBox-ui" onPointerDown={e => e.stopPropagation()} style={{ display: this.props.isContentActive() ? 'flex' : 'none' }}> + <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'} /> <input @@ -1047,38 +1061,37 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps ); } searchStringChanged = (e: React.ChangeEvent<HTMLInputElement>) => (this._searchString = e.currentTarget.value); - showInfo = action((anno: Opt<Doc>) => (this._overlayAnnoInfo = anno)); 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.props.ScreenToLocalTransform().translate(0, NumCast(this.layoutDoc._layout_scrollTop)); + 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.IsTransparentFilter()]; - opaqueFilter = () => [...this.props.childFilters(), Utils.noDragsDocFilter, ...(DragManager.docsBeingDragged.length ? [] : [Utils.IsOpaqueFilter()])]; - childStyleProvider = (doc: Doc | undefined, props: Opt<DocumentViewProps>, property: string): any => { + transparentFilter = () => [...this._props.childFilters(), Utils.TransparentBackgroundFilter]; + opaqueFilter = () => [...this._props.childFilters(), Utils.noDragDocsFilter, ...(SnappingManager.CanEmbed ? [] : [Utils.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'; } - return this.props.styleProvider?.(doc, props, property); + return this._props.styleProvider?.(doc, props, property); }; pointerEvents = () => - !this._draggingSidebar && this.props.isContentActive() && !MarqueeOptionsMenu.Instance?.isShown() + !this._draggingSidebar && this._props.isContentActive() && !MarqueeOptionsMenu.Instance?.isShown() ? 'all' // : 'none'; - annotationPointerEvents = () => (this.props.isContentActive() && (this._isAnnotating || SnappingManager.GetIsDragging() || Doc.ActiveTool !== InkTool.None) ? 'all' : 'none'); + annotationPointerEvents = () => (this._props.isContentActive() && (SnappingManager.IsDragging || Doc.ActiveTool !== InkTool.None) ? 'all' : 'none'); render() { const previewScale = this._previewNativeWidth ? 1 - this.sidebarWidth() / this._previewNativeWidth : 1; - const pointerEvents = this.layoutDoc._lockedPosition ? 'none' : (this.props.pointerEvents?.() as any); - const scale = previewScale * (this.props.NativeDimScaling?.() || 1); + const pointerEvents = this.layoutDoc._lockedPosition ? 'none' : (this._props.pointerEvents?.() as any); + const scale = previewScale * (this._props.NativeDimScaling?.() || 1); return ( <div className="webBox" ref={this._mainCont} style={{ pointerEvents: this.pointerEvents(), // - position: SnappingManager.GetIsDragging() ? 'absolute' : undefined, + position: SnappingManager.IsDragging ? 'absolute' : undefined, }}> - <div className="webBox-background" style={{ backgroundColor: this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.BackgroundColor) }} /> + <div className="webBox-background" style={{ backgroundColor: this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.BackgroundColor) }} /> <div className="webBox-container" style={{ @@ -1088,27 +1101,26 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps }} onContextMenu={this.specificContextMenu}> {this.webpage} - {!this._marqueeing || !this._mainCont.current || !this._annotationLayer.current ? null : ( - <div style={{ transformOrigin: 'top left', transform: `scale(${1 / scale})` }}> - <MarqueeAnnotator - rootDoc={this.rootDoc} - iframe={this.isFirefox() ? this.iframeClick : undefined} - iframeScaling={this.isFirefox() ? this.iframeScaling : undefined} - anchorMenuClick={this.anchorMenuClick} - scrollTop={0} - down={this._marqueeing} - scaling={returnOne} - addDocument={this.addDocumentWrapper} - docView={this.props.docViewPath().lastElement()} - finishMarquee={this.finishMarquee} - savedAnnotations={this.savedAnnotationsCreator} - selectionText={this.selectionText} - annotationLayer={this._annotationLayer.current} - mainCont={this._mainCont.current} - /> - </div> - )} </div> + {!this._mainCont.current || !this.DocumentView || !this._annotationLayer.current ? null : ( + <div style={{ position: 'absolute', height: '100%', width: '100%', pointerEvents: this.marqueeing ? 'all' : 'none' }}> + <MarqueeAnnotator + ref={this._marqueeref} + Document={this.Document} + anchorMenuClick={this.anchorMenuClick} + scrollTop={NumCast(this.layoutDoc.layout_scrollTop)} + annotationLayerScrollTop={0} + scaling={this._props.NativeDimScaling} + addDocument={this.addDocumentWrapper} + docView={this.DocumentView} + finishMarquee={this.finishMarquee} + savedAnnotations={this.savedAnnotationsCreator} + selectionText={this.selectionText} + annotationLayer={this._annotationLayer.current} + marqueeContainer={this._mainCont.current} + /> + </div> + )} <div className="webBox-sideResizer" style={{ @@ -1118,25 +1130,25 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps }} onPointerDown={e => this.sidebarBtnDown(e, false)} /> - <div style={{ position: 'absolute', height: '100%', right: 0, top: 0, width: `calc(100 * ${this.sidebarWidth() / this.props.PanelWidth()}%` }}> + <div style={{ position: 'absolute', height: '100%', right: 0, top: 0, width: `calc(100 * ${this.sidebarWidth() / this._props.PanelWidth()}%` }}> <SidebarAnnos ref={this._sidebarRef} - {...this.props} + {...this._props} whenChildContentsActiveChanged={this.whenChildContentsActiveChanged} fieldKey={this.fieldKey + '_' + this._urlHash} - rootDoc={this.rootDoc} + Document={this.Document} layoutDoc={this.layoutDoc} dataDoc={this.dataDoc} setHeight={emptyFunction} - nativeWidth={this._previewNativeWidth ?? NumCast(this.layoutDoc._nativeWidth) - WebBox.sidebarResizerWidth / (this.props.NativeDimScaling?.() || 1)} + nativeWidth={this._previewNativeWidth ?? NumCast(this.layoutDoc._nativeWidth) - WebBox.sidebarResizerWidth / (this._props.NativeDimScaling?.() || 1)} showSidebar={this.SidebarShown} sidebarAddDocument={this.sidebarAddDocument} moveDocument={this.moveDocument} removeDocument={this.removeDocument} /> </div> - {this.sidebarHandle} - {!this.props.isContentActive() ? null : this.searchUI} + {!this._props.isContentActive() || SnappingManager.IsDragging ? null : this.sidebarHandle} + {!this._props.isContentActive() || SnappingManager.IsDragging ? null : this.searchUI} </div> ); } diff --git a/src/client/views/nodes/WebBoxRenderer.js b/src/client/views/nodes/WebBoxRenderer.js index 425ef3e54..914adb404 100644 --- a/src/client/views/nodes/WebBoxRenderer.js +++ b/src/client/views/nodes/WebBoxRenderer.js @@ -2,7 +2,7 @@ * * @param {StyleSheetList} styleSheets */ -var ForeignHtmlRenderer = function (styleSheets) { +const ForeignHtmlRenderer = function (styleSheets) { const self = this; /** @@ -10,7 +10,7 @@ var ForeignHtmlRenderer = function (styleSheets) { * @param {String} binStr */ const binaryStringToBase64 = function (binStr) { - return new Promise(function (resolve) { + return new Promise(resolve => { const reader = new FileReader(); reader.readAsDataURL(binStr); reader.onloadend = function () { @@ -31,17 +31,17 @@ var ForeignHtmlRenderer = function (styleSheets) { * @returns {Promise} */ const getResourceAsBase64 = function (webUrl, inurl) { - return new Promise(function (resolve, reject) { + return new Promise((resolve, reject) => { 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; - var url = inurl; + // 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; + let url = inurl; if (inurl.startsWith('/static')) { url = new URL(webUrl).origin + inurl; } else if (inurl.startsWith('/') && !inurl.startsWith('//')) { url = CorsProxy(new URL(webUrl).origin + inurl); } else if (!inurl.startsWith('http') && !inurl.startsWith('//')) { - url = CorsProxy(webUrl + '/' + inurl); + url = CorsProxy(`${webUrl}/${inurl}`); } else if (inurl.startsWith('https') && !inurl.startsWith(window.location.origin)) { url = CorsProxy(inurl); } @@ -57,7 +57,7 @@ var ForeignHtmlRenderer = function (styleSheets) { resourceBase64: resBase64, }); } else if (xhr.readyState === XMLHttpRequest.DONE) { - console.log("COULDN'T FIND: " + (inurl.startsWith('/') ? webUrl + inurl : inurl)); + console.log(`COULDN'T FIND: ${inurl.startsWith('/') ? webUrl + inurl : inurl}`); resolve({ resourceUrl: '', resourceBase64: inurl, @@ -76,7 +76,7 @@ var ForeignHtmlRenderer = function (styleSheets) { */ const getMultipleResourcesAsBase64 = function (webUrl, urls) { const promises = []; - for (let i = 0; i < urls.length; i++) { + for (let i = 0; i < urls.length; i += 1) { promises.push(getResourceAsBase64(webUrl, urls[i])); } return Promise.all(promises); @@ -98,7 +98,7 @@ var ForeignHtmlRenderer = function (styleSheets) { } let val = ''; - for (let i = idx + prefixToken.length; i < str.length; i++) { + for (let i = idx + prefixToken.length; i < str.length; i += 1) { if (suffixTokens.indexOf(str[i]) !== -1) { break; } @@ -114,6 +114,15 @@ var ForeignHtmlRenderer = function (styleSheets) { /** * + * @param {String} str + * @returns {String} + */ + const removeQuotes = function (str) { + return str.replace(/["']/g, ''); + }; + + /** + * * @param {String} cssRuleStr * @returns {String[]} */ @@ -127,13 +136,12 @@ var ForeignHtmlRenderer = function (styleSheets) { break; } searchStartIndex = url.foundAtIndex + url.value.length + 1; - if (mustEndWithQuote && url.value[url.value.length - 1] !== '"') continue; - const unquoted = removeQuotes(url.value); - if (!unquoted /* || (!unquoted.startsWith('http')&& !unquoted.startsWith("/") )*/ || unquoted === 'http://' || unquoted === 'https://') { - continue; + if (!mustEndWithQuote || url.value[url.value.length - 1] === '"') { + const unquoted = removeQuotes(url.value); + if (unquoted /* || (!unquoted.startsWith('http')&& !unquoted.startsWith("/") ) */ && unquoted !== 'http://' && unquoted !== 'https://') { + if (unquoted) urlsFound.push(unquoted); + } } - - unquoted && urlsFound.push(unquoted); } return urlsFound; @@ -151,15 +159,6 @@ var ForeignHtmlRenderer = function (styleSheets) { return getUrlsFromCssString(html, 'source=', [' ', '>', '\t'], true); }; - /** - * - * @param {String} str - * @returns {String} - */ - const removeQuotes = function (str) { - return str.replace(/["']/g, ''); - }; - const escapeRegExp = function (string) { return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string }; @@ -172,8 +171,8 @@ var ForeignHtmlRenderer = function (styleSheets) { * * @returns {Promise<String>} */ - const buildSvgDataUri = async function (webUrl, contentHtml, width, height, scroll, xoff) { - return new Promise(async function (resolve, reject) { + 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.) @@ -181,17 +180,19 @@ var ForeignHtmlRenderer = function (styleSheets) { // copy styles let cssStyles = ''; - let urlsFoundInCss = []; + const urlsFoundInCss = []; - for (let i = 0; i < styleSheets.length; i++) { + for (let i = 0; i < styleSheets.length; i += 1) { try { const rules = styleSheets[i].cssRules; - for (let j = 0; j < rules.length; j++) { + for (let j = 0; j < rules.length; j += 1) { const cssRuleStr = rules[j].cssText; urlsFoundInCss.push(...getUrlsFromCssString(cssRuleStr)); cssStyles += cssRuleStr; } - } catch (e) {} + } catch (e) { + /* empty */ + } } // const fetchedResourcesFromStylesheets = await getMultipleResourcesAsBase64(webUrl, urlsFoundInCss); @@ -202,15 +203,15 @@ var ForeignHtmlRenderer = function (styleSheets) { // } // } - contentHtml = contentHtml + 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 - let urlsFoundInHtml = getImageUrlsFromFromHtml(contentHtml).filter(url => !url.startsWith('data:')); + const urlsFoundInHtml = getImageUrlsFromFromHtml(contentHtml).filter(url => !url.startsWith('data:')); const fetchedResources = webUrl ? await getMultipleResourcesAsBase64(webUrl, urlsFoundInHtml) : []; - for (let i = 0; i < fetchedResources.length; i++) { + for (let i = 0; i < fetchedResources.length; i += 1) { const r = fetchedResources[i]; if (r.resourceUrl) { contentHtml = contentHtml.replace(new RegExp(escapeRegExp(r.resourceUrl), 'g'), r.resourceBase64); @@ -230,7 +231,7 @@ var ForeignHtmlRenderer = function (styleSheets) { // contentRootElem.style.transform = "scale(0.08)" contentRootElem.innerHTML = styleElemString + contentHtml; contentRootElem.setAttribute('xmlns', 'http://www.w3.org/1999/xhtml'); - //document.body.appendChild(contentRootElem); + // document.body.appendChild(contentRootElem); const contentRootElemString = new XMLSerializer().serializeToString(contentRootElem); @@ -256,13 +257,13 @@ var ForeignHtmlRenderer = function (styleSheets) { * @return {Promise<Image>} */ this.renderToImage = async function (webUrl, html, width, height, scroll, xoff) { - return new Promise(async function (resolve, reject) { + return new Promise(async (resolve, reject) => { const img = new Image(); - console.log('BUILDING SVG for:' + webUrl); + 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); + console.log(`IMAGE SVG created: ${webUrl}`); resolve(img); }; }); @@ -276,7 +277,7 @@ var ForeignHtmlRenderer = function (styleSheets) { * @return {Promise<Image>} */ this.renderToCanvas = async function (webUrl, html, width, height, scroll, xoff, oversample) { - return new Promise(async function (resolve, reject) { + return new Promise(async (resolve, reject) => { const img = await self.renderToImage(webUrl, html, width, height, scroll, xoff); const canvas = document.createElement('canvas'); @@ -298,7 +299,7 @@ var ForeignHtmlRenderer = function (styleSheets) { * @return {Promise<String>} */ this.renderToBase64Png = async function (webUrl, html, width, height, scroll, xoff, oversample) { - return new Promise(async function (resolve, reject) { + return new Promise(async (resolve, reject) => { const canvas = await self.renderToCanvas(webUrl, html, width, height, scroll, xoff, oversample); resolve(canvas.toDataURL('image/png')); }); @@ -320,8 +321,8 @@ export function CreateImage(webUrl, styleSheets, html, width, height, scroll, xo ); } -var ClipboardUtils = new (function () { - var permissions = { +const ClipboardUtils = new (function () { + const permissions = { 'image/bmp': true, 'image/gif': true, 'image/png': true, @@ -330,8 +331,8 @@ var ClipboardUtils = new (function () { }; function getType(types) { - for (var j = 0; j < types.length; ++j) { - var type = types[j]; + for (let j = 0; j < types.length; j += 1) { + const type = types[j]; if (permissions[type]) { return type; } @@ -339,10 +340,10 @@ var ClipboardUtils = new (function () { return null; } function getItem(items) { - for (var i = 0; i < items.length; ++i) { - var item = items[i]; + for (let i = 0; i < items.length; i += 1) { + const item = items[i]; if (item) { - var type = getType(item.types); + const type = getType(item.types); if (type) { return item.getType(type); } @@ -352,7 +353,7 @@ var ClipboardUtils = new (function () { } function loadFile(file, callback) { if (window.FileReader) { - var reader = new FileReader(); + const reader = new FileReader(); reader.onload = function () { callback(reader.result, null); }; @@ -366,23 +367,23 @@ var ClipboardUtils = new (function () { } this.readImage = function (callback) { if (navigator.clipboard) { - var promise = navigator.clipboard.read(); + const promise = navigator.clipboard.read(); promise - .then(function (items) { - var promise = getItem(items); - if (promise == null) { + .then(items => { + const promise2 = getItem(items); + if (promise2 == null) { callback(null, null); return; } - promise - .then(function (result) { + promise2 + .then(result => { loadFile(result, callback); }) - .catch(function (error) { + .catch(error => { callback(null, 'Reading clipboard error.'); }); }) - .catch(function (error) { + .catch(error => { callback(null, 'Reading clipboard error.'); }); } else { diff --git a/src/client/views/nodes/audio/AudioWaveform.scss b/src/client/views/nodes/audio/AudioWaveform.scss new file mode 100644 index 000000000..6cbd1759a --- /dev/null +++ b/src/client/views/nodes/audio/AudioWaveform.scss @@ -0,0 +1,17 @@ +.audioWaveform { + position: relative; + width: 100%; + height: 200%; + overflow: hidden; + z-index: -1000; + bottom: 0; + pointer-events: none; + div { + height: 100% !important; + width: 100% !important; + } + canvas { + height: 100% !important; + width: 100% !important; + } +} diff --git a/src/client/views/nodes/audio/AudioWaveform.tsx b/src/client/views/nodes/audio/AudioWaveform.tsx new file mode 100644 index 000000000..7fd799952 --- /dev/null +++ b/src/client/views/nodes/audio/AudioWaveform.tsx @@ -0,0 +1,121 @@ +import axios from 'axios'; +import { computed, IReactionDisposer, makeObservable, reaction } from 'mobx'; +import { observer } from 'mobx-react'; +import * as React from 'react'; +import { Doc, NumListCast } from '../../../../fields/Doc'; +import { List } from '../../../../fields/List'; +import { listSpec } from '../../../../fields/Schema'; +import { Cast } from '../../../../fields/Types'; +import { numberRange } from '../../../../Utils'; +import { ObservableReactComponent } from '../../ObservableReactComponent'; +import { Colors } from './../../global/globalEnums'; +import './AudioWaveform.scss'; +import { WaveCanvas } from './WaveCanvas'; + +/** + * AudioWaveform + * + * Used in CollectionStackedTimeline to render a canvas with a visual of an audio waveform for AudioBox and VideoBox documents. + * Bins the audio data into audioBuckets which are passed to package to render the lines. + * Calculates new buckets each time a new zoom factor or new set of trim bounds is created and stores it in a field on the layout doc with a title indicating the bounds and zoom for that list (see audioBucketField) + */ + +export interface AudioWaveformProps { + duration: number; // length of media clip + rawDuration: number; // length of underlying media data + mediaPath: string; + layoutDoc: Doc; + clipStart: number; + clipEnd: number; + zoomFactor: number; + PanelHeight: number; + PanelWidth: number; + fieldKey: string; + progress?: number; +} + +@observer +export class AudioWaveform extends ObservableReactComponent<AudioWaveformProps> { + public static NUMBER_OF_BUCKETS = 100; // number of buckets data is divided into to draw waveform lines + _disposer: IReactionDisposer | undefined; + + constructor(props: any) { + super(props); + makeObservable(this); + } + + get waveHeight() { + return Math.max(50, this._props.PanelHeight); + } + + get clipStart() { + return this._props.clipStart; + } + get clipEnd() { + return this._props.clipEnd; + } + get zoomFactor() { + return this._props.zoomFactor; + } + + @computed get audioBuckets() { + return NumListCast(this._props.layoutDoc[this.audioBucketField(this.clipStart, this.clipEnd, this.zoomFactor)]); + } + + audioBucketField = (start: number, end: number, zoomFactor: number) => this._props.fieldKey + '_audioBuckets/' + '/' + start.toFixed(2).replace('.', '_') + '/' + end.toFixed(2).replace('.', '_') + '/' + zoomFactor * 10; + + componentWillUnmount() { + this._disposer?.(); + } + + componentDidMount() { + this._disposer = reaction( + () => ({ clipStart: this.clipStart, clipEnd: this.clipEnd, fieldKey: this.audioBucketField(this.clipStart, this.clipEnd, this.zoomFactor), zoomFactor: this._props.zoomFactor }), + ({ clipStart, clipEnd, fieldKey, zoomFactor }) => { + if (!this._props.layoutDoc[fieldKey] && this._props.layoutDoc.layout_fieldKey != 'layout_icon') { + // setting these values here serves as a "lock" to prevent multiple attempts to create the waveform at nerly the same time. + const waveform = Cast(this._props.layoutDoc[this.audioBucketField(0, this._props.rawDuration, 1)], listSpec('number')); + this._props.layoutDoc[fieldKey] = waveform && new List<number>(waveform.slice((clipStart / this._props.rawDuration) * waveform.length, (clipEnd / this._props.rawDuration) * waveform.length)); + setTimeout(() => this.createWaveformBuckets(fieldKey, clipStart, clipEnd, zoomFactor)); + } + }, + { fireImmediately: true } + ); + } + + // decodes the audio file into peaks for generating the waveform + createWaveformBuckets = (fieldKey: string, clipStart: number, clipEnd: number, zoomFactor: number) => { + axios({ url: this._props.mediaPath, responseType: 'arraybuffer' }).then(response => + new window.AudioContext().decodeAudioData(response.data, buffer => { + const rawDecodedAudioData = buffer.getChannelData(0); + const startInd = clipStart / this._props.rawDuration; + const endInd = clipEnd / this._props.rawDuration; + const decodedAudioData = rawDecodedAudioData.slice(Math.floor(startInd * rawDecodedAudioData.length), Math.floor(endInd * rawDecodedAudioData.length)); + const numBuckets = Math.floor(AudioWaveform.NUMBER_OF_BUCKETS * zoomFactor); + + const bucketDataSize = Math.floor(decodedAudioData.length / numBuckets); + const brange = Array.from(Array(bucketDataSize)); + const bucketList = numberRange(numBuckets).map((i: number) => brange.reduce((p, x, j) => Math.abs(Math.max(p, decodedAudioData[i * bucketDataSize + j])), 0) / 2); + this._props.layoutDoc[fieldKey] = new List<number>(bucketList); + }) + ); + }; + + render() { + return ( + <div className="audioWaveform"> + <WaveCanvas + color={Colors.LIGHT_GRAY} + progressColor={Colors.MEDIUM_BLUE_ALT} + progress={this._props.progress ?? 1} + barWidth={200 / this.audioBuckets.length} + //gradientColors={this._props.gradientColors} + peaks={this.audioBuckets} + width={(this._props.PanelWidth ?? 0) * window.devicePixelRatio} + height={this.waveHeight * window.devicePixelRatio} + pixelRatio={window.devicePixelRatio} + /> + </div> + ); + } +} diff --git a/src/client/views/nodes/audio/WaveCanvas.tsx b/src/client/views/nodes/audio/WaveCanvas.tsx new file mode 100644 index 000000000..d3f5669a2 --- /dev/null +++ b/src/client/views/nodes/audio/WaveCanvas.tsx @@ -0,0 +1,100 @@ +import React from 'react'; + +interface WaveCanvasProps { + barWidth: number; + color: string; + progress: number; + progressColor: string; + gradientColors?: { stopPosition: number; color: string }[]; // stopPosition between 0 and 1 + peaks: number[]; + width: number; + height: number; + pixelRatio: number; +} + +export class WaveCanvas extends React.Component<WaveCanvasProps> { + // If the first value of peaks is negative, addToIndices will be 1 + posPeaks = (peaks: number[], addToIndices: number) => peaks.filter((_, index) => (index + addToIndices) % 2 == 0); + + drawBars = (waveCanvasCtx: CanvasRenderingContext2D, width: number, halfH: number, peaks: number[]) => { + // Bar wave draws the bottom only as a reflection of the top, + // so we don't need negative values + const posPeaks = peaks.some(val => val < 0) ? this.posPeaks(peaks, peaks[0] < 0 ? 1 : 0) : peaks; + + // A half-pixel offset makes lines crisp + const $ = 0.5 / this.props.pixelRatio; + const bar = this.props.barWidth * this.props.pixelRatio; + const gap = Math.max(this.props.pixelRatio, 2); + + const max = Math.max(...posPeaks); + const scale = posPeaks.length / width; + + for (let i = 0; i < width; i += bar + gap) { + if (i > width * this.props.progress) waveCanvasCtx.fillStyle = this.props.color; + + const h = Math.round((posPeaks[Math.floor(i * scale)] / max) * halfH) || 1; + + waveCanvasCtx.fillRect(i + $, halfH - h, bar + $, h * 2); + } + }; + + addNegPeaks = (peaks: number[]) => + peaks.reduce((reflectedPeaks, peak) => reflectedPeaks.push(peak, -peak) ? reflectedPeaks:[], + [] as number[]); // prettier-ignore + + drawWaves = (waveCanvasCtx: CanvasRenderingContext2D, width: number, halfH: number, peaks: number[]) => { + const allPeaks = peaks.some(val => val < 0) ? peaks : this.addNegPeaks(peaks); // add negative peaks to arrays without negative peaks + + // A half-pixel offset makes lines crisp + const $ = 0.5 / this.props.pixelRatio; + const length = ~~(allPeaks.length / 2); // ~~ is Math.floor for positive numbers. + + const scale = width / length; + const absmax = Math.max(...allPeaks.map(peak => Math.abs(peak))); + + waveCanvasCtx.beginPath(); + waveCanvasCtx.moveTo($, halfH); + + for (var i = 0; i < length; i++) { + var h = Math.round((allPeaks[2 * i] / absmax) * halfH); + waveCanvasCtx.lineTo(i * scale + $, halfH - h); + } + + // Draw the bottom edge going backwards, to make a single closed hull to fill. + for (var i = length - 1; i >= 0; i--) { + var h = Math.round((allPeaks[2 * i + 1] / absmax) * halfH); + waveCanvasCtx.lineTo(i * scale + $, halfH - h); + } + + waveCanvasCtx.fill(); + + // Always draw a median line + waveCanvasCtx.fillRect(0, halfH - $, width, $); + }; + + updateSize = (width: number, height: number, peaks: number[], waveCanvasCtx: CanvasRenderingContext2D) => { + const displayWidth = Math.round(width / this.props.pixelRatio); + const displayHeight = Math.round(height / this.props.pixelRatio); + waveCanvasCtx.canvas.width = width; + waveCanvasCtx.canvas.height = height; + waveCanvasCtx.canvas.style.width = `${displayWidth}px`; + waveCanvasCtx.canvas.style.height = `${displayHeight}px`; + + waveCanvasCtx.clearRect(0, 0, width, height); + + const gradient = this.props.gradientColors && waveCanvasCtx.createLinearGradient(0, 0, width, 0); + gradient && this.props.gradientColors?.forEach(color => gradient.addColorStop(color.stopPosition, color.color)); + waveCanvasCtx.fillStyle = gradient ?? this.props.progressColor; + + const waveDrawer = this.props.barWidth ? this.drawBars : this.drawWaves; + waveDrawer(waveCanvasCtx, width, height / 2, peaks); + }; + + render() { + return this.props.peaks ? ( + <div style={{ position: 'relative', width: '100%', height: '100%', cursor: 'pointer' }}> + <canvas ref={instance => (ctx => ctx && this.updateSize(this.props.width, this.props.height, this.props.peaks, ctx))(instance?.getContext('2d'))} /> + </div> + ) : null; + } +} diff --git a/src/client/views/nodes/calendarBox/CalendarBox.tsx b/src/client/views/nodes/calendarBox/CalendarBox.tsx new file mode 100644 index 000000000..748c3322e --- /dev/null +++ b/src/client/views/nodes/calendarBox/CalendarBox.tsx @@ -0,0 +1,120 @@ +import { Calendar, EventClickArg, 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 { Doc } from '../../../../fields/Doc'; +import { StrCast } from '../../../../fields/Types'; +import { ViewBoxBaseComponent } from '../../DocComponent'; +import { FieldView, FieldViewProps } from '../FieldView'; + +type CalendarView = 'month' | 'multi-month' | 'week'; + +@observer +export class CalendarBox extends ViewBoxBaseComponent<FieldViewProps>() { + public static LayoutString(fieldKey: string = 'calendar') { + return FieldView.LayoutString(CalendarBox, fieldKey); + } + + constructor(props: FieldViewProps) { + super(props); + makeObservable(this); + } + + componentDidMount(): void {} + + componentWillUnmount(): void {} + + _calendarRef = React.createRef<HTMLElement>(); + + get dateRangeStr() { + return StrCast(this.Document.date_range); + } + + // Choose a calendar view based on the date range + get calendarViewType(): CalendarView { + const [fromDate, toDate] = dateRangeStrToDates(this.dateRangeStr); + + if (fromDate.getFullYear() !== toDate.getFullYear() || fromDate.getMonth() !== toDate.getMonth()) return 'multi-month'; + + if (Math.abs(fromDate.getDay() - toDate.getDay()) > 7) return 'month'; + return 'week'; + } + + get calendarStartDate() { + return this.dateRangeStr.split('|')[0]; + } + + get calendarToDate() { + return this.dateRangeStr.split('|')[1]; + } + + get childDocs(): Doc[] { + return this.childDocs; // get all sub docs for a calendar + } + + docBackgroundColor(type: string): string { + // TODO: Return a different color based on the event type + return 'blue'; + } + + get calendarEvents(): EventSourceInput | undefined { + if (this.childDocs.length === 0) return undefined; + return this.childDocs.map((doc, idx) => { + const docTitle = StrCast(doc.title); + const docDateRange = StrCast(doc.date_range); + const [startDate, endDate] = dateRangeStrToDates(docDateRange); + const docType = doc.type; + const docDescription = doc.description ? StrCast(doc.description) : ''; + + return { + title: docTitle, + start: startDate, + end: endDate, + allDay: false, + classNames: [StrCast(docType)], // will determine the style + editable: false, // subject to change in the future + backgroundColor: this.docBackgroundColor(StrCast(doc.type)), + color: 'white', + extendedProps: { + description: docDescription, + }, + }; + }); + } + + handleEventClick = (arg: EventClickArg) => { + // TODO: open popover with event description, option to open CalendarManager and change event date, delete event, etc. + }; + + calendarEl: HTMLElement = document.getElementById('calendar-box-v1')!; + + // https://fullcalendar.io + get calendar() { + return new Calendar(this.calendarEl, { + plugins: [this.calendarViewType === 'multi-month' ? multiMonthPlugin : dayGridPlugin], + headerToolbar: { + left: 'prev,next today', + center: 'title', + right: 'dayGridMonth,timeGridWeek,timeGridDay,listWeek', + }, + initialDate: this.calendarStartDate, + navLinks: true, + editable: false, + displayEventTime: false, + displayEventEnd: false, + events: this.calendarEvents, + eventClick: this.handleEventClick, + }); + } + + render() { + return ( + <div className="calendar-box-conatiner"> + <div id="calendar-box-v1"></div> + </div> + ); + } +} diff --git a/src/client/views/nodes/formattedText/DashDocCommentView.tsx b/src/client/views/nodes/formattedText/DashDocCommentView.tsx index aa269d8d6..b7d2a24c2 100644 --- a/src/client/views/nodes/formattedText/DashDocCommentView.tsx +++ b/src/client/views/nodes/formattedText/DashDocCommentView.tsx @@ -2,7 +2,7 @@ import { TextSelection } from 'prosemirror-state'; import * as ReactDOM from 'react-dom/client'; import { Doc } from '../../../../fields/Doc'; import { DocServer } from '../../../DocServer'; -import React = require('react'); +import * as React from 'react'; // 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. @@ -54,7 +54,7 @@ interface IDashDocCommentViewInternal { } export class DashDocCommentViewInternal extends React.Component<IDashDocCommentViewInternal> { - constructor(props: IDashDocCommentViewInternal) { + constructor(props: any) { super(props); this.onPointerLeaveCollapsed = this.onPointerLeaveCollapsed.bind(this); this.onPointerEnterCollapsed = this.onPointerEnterCollapsed.bind(this); diff --git a/src/client/views/nodes/formattedText/DashDocView.tsx b/src/client/views/nodes/formattedText/DashDocView.tsx index c5167461b..7335c9286 100644 --- a/src/client/views/nodes/formattedText/DashDocView.tsx +++ b/src/client/views/nodes/formattedText/DashDocView.tsx @@ -1,18 +1,21 @@ -import { action, IReactionDisposer, observable, reaction } from 'mobx'; +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 { Doc } from '../../../../fields/Doc'; import { Height, Width } from '../../../../fields/DocSymbols'; -import { NumCast, StrCast } from '../../../../fields/Types'; +import { NumCast } from '../../../../fields/Types'; import { emptyFunction, returnFalse, Utils } from '../../../../Utils'; import { DocServer } from '../../../DocServer'; import { Docs, DocUtils } from '../../../documents/Documents'; import { Transform } from '../../../util/Transform'; -import { DocFocusOptions, DocumentView } from '../DocumentView'; +import { ObservableReactComponent } from '../../ObservableReactComponent'; +import { DocumentView } from '../DocumentView'; +import { FocusViewOptions } from '../FieldView'; import { FormattedTextBox } from './FormattedTextBox'; -import React = require('react'); +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; @@ -21,8 +24,7 @@ export class DashDocView { this.dom = document.createElement('span'); this.dom.style.position = 'relative'; this.dom.style.textIndent = '0'; - this.dom.style.border = '1px solid ' + StrCast(tbox.layoutDoc.color, 'lightGray'); - this.dom.style.width = node.attrs.width; + 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; @@ -62,7 +64,12 @@ export class DashDocView { } catch {} }); } - selectNode() {} + deselectNode() { + this.dom.style.backgroundColor = ''; + } + selectNode() { + this.dom.style.backgroundColor = 'rgb(141, 182, 247)'; + } } interface IDashDocViewInternal { @@ -78,27 +85,27 @@ interface IDashDocViewInternal { getPos: any; } @observer -export class DashDocViewInternal extends React.Component<IDashDocViewInternal> { +export class DashDocViewInternal extends ObservableReactComponent<IDashDocViewInternal> { _spanRef = React.createRef<HTMLDivElement>(); _disposers: { [name: string]: IReactionDisposer } = {}; _textBox: FormattedTextBox; - @observable _dashDoc: Doc | undefined; - @observable _finalLayout: any; - @observable _width: number = 0; - @observable _height: number = 0; + @observable _dashDoc: Doc | undefined = undefined; + @computed get _width() { + return NumCast(this._dashDoc?._width); + } + @computed get _height() { + return NumCast(this._dashDoc?._height); + } updateDoc = action((dashDoc: Doc) => { this._dashDoc = dashDoc; - this._finalLayout = dashDoc; - if (this.props.width !== (this._dashDoc?._width ?? '') + 'px' || this.props.height !== (this._dashDoc?._height ?? '') + 'px') { + if (this._props.width !== (this._dashDoc?._width ?? '') + 'px' || this._props.height !== (this._dashDoc?._height ?? '') + 'px') { try { - this._width = NumCast(this._dashDoc?._width); - this._height = NumCast(this._dashDoc?._height); // bcz: an exception will be thrown if two embeddings are open at the same time when a doc view comment is made - this.props.view.dispatch( - this.props.view.state.tr.setNodeMarkup(this.props.getPos(), null, { - ...this.props.node.attrs, + this._props.view.dispatch( + this._props.view.state.tr.setNodeMarkup(this._props.getPos(), null, { + ...this._props.node.attrs, width: this._width + 'px', height: this._height + 'px', }) @@ -111,16 +118,17 @@ export class DashDocViewInternal extends React.Component<IDashDocViewInternal> { constructor(props: IDashDocViewInternal) { super(props); - this._textBox = this.props.tbox; + makeObservable(this); + this._textBox = this._props.tbox; - DocServer.GetRefField(this.props.docId + this.props.embedding).then(async dashDoc => { + DocServer.GetRefField(this._props.docId + this._props.embedding).then(async dashDoc => { if (!(dashDoc instanceof Doc)) { - this.props.embedding && - DocServer.GetRefField(this.props.docId).then(async dashDocBase => { + this._props.embedding && + DocServer.GetRefField(this._props.docId).then(async dashDocBase => { if (dashDocBase instanceof Doc) { - const embedding = Doc.MakeEmbedding(dashDocBase, this.props.docId + this.props.embedding); + const embedding = Doc.MakeEmbedding(dashDocBase, this._props.docId + this._props.embedding); embedding.layout_fieldKey = 'layout'; - this.props.fieldKey && DocUtils.makeCustomViewClicked(embedding, Docs.Create.StackingDocument, this.props.fieldKey, undefined); + this._props.fieldKey && DocUtils.makeCustomViewClicked(embedding, Docs.Create.StackingDocument, this._props.fieldKey, undefined); this.updateDoc(embedding); } }); @@ -132,23 +140,18 @@ export class DashDocViewInternal extends React.Component<IDashDocViewInternal> { componentDidMount() { this._disposers.upater = reaction( - () => this._dashDoc && NumCast(this._dashDoc._height) + NumCast(this._dashDoc._width), - action(() => { - if (this._dashDoc) { - this._width = NumCast(this._dashDoc._width); - this._height = NumCast(this._dashDoc._height); - const parent = this._spanRef.current?.parentNode as HTMLElement; - if (parent) { - parent.style.width = this._width + 'px'; - parent.style.height = this._height + 'px'; - } + () => ({ width: this._width, height: this._height, parent: this._spanRef.current?.parentNode as HTMLElement }), + action(({ width, height, parent }) => { + if (parent) { + parent.style.width = width + 'px'; + parent.style.height = height + 'px'; } }) ); } removeDoc = () => { - this.props.view.dispatch(this.props.view.state.tr.setSelection(new NodeSelection(this.props.view.state.doc.resolve(this.props.getPos()))).deleteSelection()); + this._props.view.dispatch(this._props.view.state.tr.setSelection(new NodeSelection(this._props.view.state.doc.resolve(this._props.getPos()))).deleteSelection()); return true; }; @@ -157,7 +160,7 @@ export class DashDocViewInternal extends React.Component<IDashDocViewInternal> { const { scale, translateX, translateY } = Utils.GetScreenTransform(this._spanRef.current); return new Transform(-translateX, -translateY, 1).scale(1 / scale); }; - outerFocus = (target: Doc, options: DocFocusOptions) => this._textBox.focus(target, options); // ideally, this would scroll to show the focus target + outerFocus = (target: Doc, options: FocusViewOptions) => this._textBox.focus(target, options); // ideally, this would scroll to show the focus target onKeyDown = (e: any) => { e.stopPropagation(); @@ -167,29 +170,30 @@ export class DashDocViewInternal extends React.Component<IDashDocViewInternal> { }; onPointerLeave = () => { - const ele = document.getElementById('DashDocCommentView-' + this.props.docId) as HTMLDivElement; + const ele = document.getElementById('DashDocCommentView-' + this._props.docId) as HTMLDivElement; ele && (ele.style.backgroundColor = ''); }; onPointerEnter = () => { - const ele = document.getElementById('DashDocCommentView-' + this.props.docId) as HTMLDivElement; + const ele = document.getElementById('DashDocCommentView-' + this._props.docId) as HTMLDivElement; ele && (ele.style.backgroundColor = 'orange'); }; componentWillUnmount = () => Object.values(this._disposers).forEach(disposer => disposer?.()); + isContentActive = () => this._props.tbox._props.isContentActive() || this._props.tbox.isAnyChildContentActive?.(); render() { - return !this._dashDoc || !this._finalLayout || this.props.hidden ? null : ( + return !this._dashDoc || this._props.hidden ? null : ( <div ref={this._spanRef} className="dash-span" style={{ - width: this._width, + width: `calc(100% - ${horizPadding}px)`, height: this._height, - position: 'absolute', - display: 'inline-block', - left: 0, - top: 0, + position: 'relative', + display: 'flex', + margin: 'auto', + pointerEvents: this.isContentActive() ? undefined : 'none', }} onPointerLeave={this.onPointerLeave} onPointerEnter={this.onPointerEnter} @@ -198,27 +202,25 @@ export class DashDocViewInternal extends React.Component<IDashDocViewInternal> { onKeyUp={e => e.stopPropagation()} onWheel={e => e.preventDefault()}> <DocumentView - Document={this._finalLayout} + Document={this._dashDoc} addDocument={returnFalse} - rootSelected={returnFalse} //{this._textBox.props.isSelected} removeDocument={this.removeDoc} isDocumentActive={returnFalse} - isContentActive={emptyFunction} - styleProvider={this._textBox.props.styleProvider} - docViewPath={this._textBox.props.docViewPath} + isContentActive={this.isContentActive} + styleProvider={this._textBox._props.styleProvider} + containerViewPath={this._textBox.DocumentView?.().docViewPath} ScreenToLocalTransform={this.getDocTransform} - addDocTab={this._textBox.props.addDocTab} + addDocTab={this._textBox._props.addDocTab} pinToPres={returnFalse} - renderDepth={this._textBox.props.renderDepth + 1} - PanelWidth={this._finalLayout[Width]} - PanelHeight={this._finalLayout[Height]} + renderDepth={this._textBox._props.renderDepth + 1} + PanelWidth={this._dashDoc[Width]} + PanelHeight={this._dashDoc[Height]} focus={this.outerFocus} - whenChildContentsActiveChanged={returnFalse} - bringToFront={emptyFunction} + whenChildContentsActiveChanged={this._props.tbox.whenChildContentsActiveChanged} dontRegisterView={false} - childFilters={this.props.tbox?.props.childFilters} - childFiltersByRanges={this.props.tbox?.props.childFiltersByRanges} - searchFilterDocs={this.props.tbox?.props.searchFilterDocs} + childFilters={this._props.tbox?._props.childFilters} + childFiltersByRanges={this._props.tbox?._props.childFiltersByRanges} + searchFilterDocs={this._props.tbox?._props.searchFilterDocs} /> </div> ); diff --git a/src/client/views/nodes/formattedText/DashFieldView.scss b/src/client/views/nodes/formattedText/DashFieldView.scss index ad315acc8..3426ba1a7 100644 --- a/src/client/views/nodes/formattedText/DashFieldView.scss +++ b/src/client/views/nodes/formattedText/DashFieldView.scss @@ -1,4 +1,4 @@ -@import '../../global/globalCssVariables'; +@import '../../global/globalCssVariables.module.scss'; .dashFieldView { position: relative; diff --git a/src/client/views/nodes/formattedText/DashFieldView.tsx b/src/client/views/nodes/formattedText/DashFieldView.tsx index d5ad128fe..dc9914637 100644 --- a/src/client/views/nodes/formattedText/DashFieldView.tsx +++ b/src/client/views/nodes/formattedText/DashFieldView.tsx @@ -1,23 +1,24 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { Tooltip } from '@material-ui/core'; -import { action, computed, IReactionDisposer, observable } from 'mobx'; +import { Tooltip } from '@mui/material'; +import { action, computed, IReactionDisposer, makeObservable, observable } from 'mobx'; import { observer } from 'mobx-react'; +import * as React from 'react'; import * as ReactDOM from 'react-dom/client'; import { Doc } from '../../../../fields/Doc'; import { List } from '../../../../fields/List'; import { listSpec } from '../../../../fields/Schema'; import { SchemaHeaderField } from '../../../../fields/SchemaHeaderField'; -import { Cast, StrCast } from '../../../../fields/Types'; +import { Cast } from '../../../../fields/Types'; import { emptyFunction, returnFalse, returnZero, setupMoveUpEvents } from '../../../../Utils'; import { DocServer } from '../../../DocServer'; import { CollectionViewType } from '../../../documents/DocumentTypes'; +import { Transform } from '../../../util/Transform'; import { AntimodeMenu, AntimodeMenuProps } from '../../AntimodeMenu'; import { SchemaTableCell } from '../../collections/collectionSchema/SchemaTableCell'; +import { ObservableReactComponent } from '../../ObservableReactComponent'; import { OpenWhere } from '../DocumentView'; import './DashFieldView.scss'; import { FormattedTextBox } from './FormattedTextBox'; -import React = require('react'); -import { Transform } from '../../../util/Transform'; export class DashFieldView { dom: HTMLDivElement; // container for label and value @@ -25,7 +26,7 @@ export class DashFieldView { node: any; tbox: FormattedTextBox; - unclickable = () => !this.tbox.props.isSelected() && this.node.marks.some((m: any) => m.type === this.tbox.EditorView?.state.schema.marks.linkAnchor && m.attrs.noPreview); + unclickable = () => !this.tbox._props.isSelected() && this.node.marks.some((m: any) => m.type === this.tbox.EditorView?.state.schema.marks.linkAnchor && m.attrs.noPreview); constructor(node: any, view: any, getPos: any, tbox: FormattedTextBox) { this.node = node; this.tbox = tbox; @@ -70,10 +71,10 @@ export class DashFieldView { } catch {} }); } - @action deselectNode() { + deselectNode() { this.dom.classList.remove('ProseMirror-selectednode'); } - @action selectNode() { + selectNode() { this.dom.classList.add('ProseMirror-selectednode'); } } @@ -92,25 +93,27 @@ interface IDashFieldViewInternal { } @observer -export class DashFieldViewInternal extends React.Component<IDashFieldViewInternal> { +export class DashFieldViewInternal extends ObservableReactComponent<IDashFieldViewInternal> { _reactionDisposer: IReactionDisposer | undefined; _textBoxDoc: Doc; _fieldKey: string; _fieldStringRef = React.createRef<HTMLSpanElement>(); - @observable _dashDoc: Doc | undefined; + @observable _dashDoc: Doc | undefined = undefined; @observable _expanded = false; constructor(props: IDashFieldViewInternal) { super(props); - this._fieldKey = this.props.fieldKey; - this._textBoxDoc = this.props.tbox.props.Document; + makeObservable(this); + this._fieldKey = this._props.fieldKey; + this._textBoxDoc = this._props.tbox.Document; - if (this.props.docId) { - DocServer.GetRefField(this.props.docId).then(action(dashDoc => dashDoc instanceof Doc && (this._dashDoc = dashDoc))); + if (this._props.docId) { + DocServer.GetRefField(this._props.docId).then(action(dashDoc => dashDoc instanceof Doc && (this._dashDoc = dashDoc))); } else { - this._dashDoc = this.props.tbox.rootDoc; + this._dashDoc = this._props.tbox.Document; } } + componentWillUnmount() { this._reactionDisposer?.(); } @@ -119,18 +122,18 @@ export class DashFieldViewInternal extends React.Component<IDashFieldViewInterna // 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.props.hideKey ? this.props.tbox.props.PanelWidth() - 20 : undefined }}> + <div onClick={action(e => (this._expanded = !this._props.editable ? !this._expanded : true))} style={{ fontSize: 'smaller', width: this._props.hideKey ? this._props.tbox._props.PanelWidth() - 20 : undefined }}> <SchemaTableCell Document={this._dashDoc} col={0} deselectCell={emptyFunction} selectCell={emptyFunction} - maxWidth={this.props.hideKey ? undefined : this.return100} - columnWidth={this.props.hideKey ? () => this.props.tbox.props.PanelWidth() - 20 : returnZero} + maxWidth={this._props.hideKey ? undefined : this.return100} + columnWidth={this._props.hideKey ? () => this._props.tbox._props.PanelWidth() - 20 : returnZero} selectedCell={() => [this._dashDoc!, 0]} fieldKey={this._fieldKey} rowHeight={returnZero} - isRowActive={() => this._expanded && this.props.editable} + isRowActive={() => this._expanded && this._props.editable} padding={0} getFinfo={emptyFunction} setColumnValues={returnFalse} @@ -145,9 +148,9 @@ export class DashFieldViewInternal extends React.Component<IDashFieldViewInterna } createPivotForField = (e: React.MouseEvent) => { - let container = this.props.tbox.props.DocumentView?.().props.docViewPath().lastElement(); + let container = this._props.tbox.DocumentView?.().containerViewPath?.().lastElement(); if (container) { - const embedding = Doc.MakeEmbedding(container.rootDoc); + const embedding = Doc.MakeEmbedding(container.Document); embedding._type_collection = CollectionViewType.Time; const colHdrKey = '_' + container.LayoutFieldKey + '_columnHeaders'; let list = Cast(embedding[colHdrKey], listSpec(SchemaHeaderField)); @@ -157,7 +160,7 @@ export class DashFieldViewInternal extends React.Component<IDashFieldViewInterna list.map(c => c.heading).indexOf(this._fieldKey) === -1 && list.push(new SchemaHeaderField(this._fieldKey, '#f1efeb')); list.map(c => c.heading).indexOf('text') === -1 && list.push(new SchemaHeaderField('text', '#f1efeb')); embedding._pivotField = this._fieldKey.startsWith('#') ? 'tags' : this._fieldKey; - this.props.tbox.props.addDocTab(embedding, OpenWhere.addRight); + this._props.tbox._props.addDocTab(embedding, OpenWhere.addRight); } }; @@ -175,16 +178,17 @@ export class DashFieldViewInternal extends React.Component<IDashFieldViewInterna <div className="dashFieldView" style={{ - width: this.props.width, - height: this.props.height, + width: this._props.width, + height: this._props.height, + pointerEvents: this._props.tbox._props.isSelected() || this._props.tbox.isAnyChildContentActive?.() ? undefined : 'none', }}> - {this.props.hideKey ? null : ( + {this._props.hideKey ? null : ( <span className="dashFieldView-labelSpan" title="click to see related tags" onPointerDown={this.onPointerDownLabelSpan}> {this._fieldKey} </span> )} - {this.props.fieldKey.startsWith('#') ? null : this.fieldValueContent} + {this._props.fieldKey.startsWith('#') ? null : this.fieldValueContent} </div> ); } @@ -197,7 +201,7 @@ export class DashFieldViewMenu extends AntimodeMenu<AntimodeMenuProps> { super(props); DashFieldViewMenu.Instance = this; } - @action + showFields = (e: React.MouseEvent) => { DashFieldViewMenu.createFieldView(e); DashFieldViewMenu.Instance.fadeOut(true); diff --git a/src/client/views/nodes/formattedText/EquationEditor.scss b/src/client/views/nodes/formattedText/EquationEditor.scss new file mode 100644 index 000000000..b0c17e56e --- /dev/null +++ b/src/client/views/nodes/formattedText/EquationEditor.scss @@ -0,0 +1,468 @@ +// using this import, we get runtime errors when trying to load the specified font-faces +// so we copy the .css and remove the @font-face imports + +// @import 'mathquill/build/mathquill.css' +/* + * MathQuill v0.10.1 http://mathquill.com + * by Han, Jeanine, and Mary maintainers@mathquill.com + * + * This Source Code Form is subject to the terms of the + * Mozilla Public License, v. 2.0. If a copy of the MPL + * was not distributed with this file, You can obtain + * one at http://mozilla.org/MPL/2.0/. + */ +// @font-face { +// font-family: Symbola; +// src: url(font/Symbola.eot); +// src: +// local('Symbola Regular'), +// local('Symbola'), +// url(font/Symbola.woff2) format('woff2'), +// url(font/Symbola.woff) format('woff'), +// url(font/Symbola.ttf) format('truetype'), +// url(font/Symbola.otf) format('opentype'), +// url(font/Symbola.svg#Symbola) format('svg'); +// } +.mq-editable-field { + display: -moz-inline-box; + display: inline-block; +} +.mq-editable-field .mq-cursor { + border-left: 1px solid black; + margin-left: -1px; + position: relative; + z-index: 1; + padding: 0; + display: -moz-inline-box; + display: inline-block; +} +.mq-editable-field .mq-cursor.mq-blink { + visibility: hidden; +} +.mq-editable-field, +.mq-math-mode .mq-editable-field { + border: 1px solid gray; +} +.mq-editable-field.mq-focused, +.mq-math-mode .mq-editable-field.mq-focused { + -webkit-box-shadow: + #8bd 0 0 1px 2px, + inset #6ae 0 0 2px 0; + -moz-box-shadow: + #8bd 0 0 1px 2px, + inset #6ae 0 0 2px 0; + box-shadow: + #8bd 0 0 1px 2px, + inset #6ae 0 0 2px 0; + border-color: #709ac0; + border-radius: 1px; +} +.mq-math-mode .mq-editable-field { + margin: 1px; +} +.mq-editable-field .mq-latex-command-input { + color: inherit; + font-family: 'Courier New', monospace; + border: 1px solid gray; + padding-right: 1px; + margin-right: 1px; + margin-left: 2px; +} +.mq-editable-field .mq-latex-command-input.mq-empty { + background: transparent; +} +.mq-editable-field .mq-latex-command-input.mq-hasCursor { + border-color: ActiveBorder; +} +.mq-editable-field.mq-empty:after, +.mq-editable-field.mq-text-mode:after, +.mq-math-mode .mq-empty:after { + visibility: hidden; + content: 'c'; +} +.mq-editable-field .mq-cursor:only-child:after, +.mq-editable-field .mq-textarea + .mq-cursor:last-child:after { + visibility: hidden; + content: 'c'; +} +.mq-editable-field .mq-text-mode .mq-cursor:only-child:after { + content: ''; +} +.mq-editable-field.mq-text-mode { + overflow-x: auto; + overflow-y: hidden; +} +.mq-root-block, +.mq-math-mode .mq-root-block { + display: -moz-inline-box; + display: inline-block; + width: 100%; + padding: 2px; + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; + white-space: nowrap; + overflow: hidden; + vertical-align: middle; +} +.mq-math-mode { + font-variant: normal; + font-weight: normal; + font-style: normal; + font-size: 115%; + line-height: 1; + display: -moz-inline-box; + display: inline-block; +} +.mq-math-mode .mq-non-leaf, +.mq-math-mode .mq-scaled { + display: -moz-inline-box; + display: inline-block; +} +.mq-math-mode var, +.mq-math-mode .mq-text-mode, +.mq-math-mode .mq-nonSymbola { + font-family: 'Times New Roman', Symbola, serif; + line-height: 0.9; +} +.mq-math-mode * { + font-size: inherit; + line-height: inherit; + margin: 0; + padding: 0; + border-color: black; + -webkit-user-select: none; + -moz-user-select: none; + user-select: none; + box-sizing: border-box; +} +.mq-math-mode .mq-empty { + background: #ccc; +} +.mq-math-mode .mq-empty.mq-root-block { + background: transparent; +} +.mq-math-mode.mq-empty { + background: transparent; +} +.mq-math-mode .mq-text-mode { + display: inline-block; +} +.mq-math-mode .mq-text-mode.mq-hasCursor { + box-shadow: inset darkgray 0 0.1em 0.2em; + padding: 0 0.1em; + margin: 0 -0.1em; + min-width: 1ex; +} +.mq-math-mode .mq-font { + font: + 1em 'Times New Roman', + Symbola, + serif; +} +.mq-math-mode .mq-font * { + font-family: inherit; + font-style: inherit; +} +.mq-math-mode b, +.mq-math-mode b.mq-font { + font-weight: bolder; +} +.mq-math-mode var, +.mq-math-mode i, +.mq-math-mode i.mq-font { + font-style: italic; +} +.mq-math-mode var.mq-f { + margin-right: 0.2em; + margin-left: 0.1em; +} +.mq-math-mode .mq-roman var.mq-f { + margin: 0; +} +.mq-math-mode big { + font-size: 200%; +} +.mq-math-mode .mq-int > big { + display: inline-block; + -webkit-transform: scaleX(0.7); + -moz-transform: scaleX(0.7); + -ms-transform: scaleX(0.7); + -o-transform: scaleX(0.7); + transform: scaleX(0.7); + vertical-align: -0.16em; +} +.mq-math-mode .mq-int > .mq-supsub { + font-size: 80%; + vertical-align: -1.1em; + padding-right: 0.2em; +} +.mq-math-mode .mq-int > .mq-supsub > .mq-sup > .mq-sup-inner { + vertical-align: 1.3em; +} +.mq-math-mode .mq-int > .mq-supsub > .mq-sub { + margin-left: -0.35em; +} +.mq-math-mode .mq-roman { + font-style: normal; +} +.mq-math-mode .mq-sans-serif { + font-family: sans-serif, Symbola, serif; +} +.mq-math-mode .mq-monospace { + font-family: monospace, Symbola, serif; +} +.mq-math-mode .mq-overline { + border-top: 1px solid black; + margin-top: 1px; +} +.mq-math-mode .mq-underline { + border-bottom: 1px solid black; + margin-bottom: 1px; +} +.mq-math-mode .mq-binary-operator { + padding: 0 0.2em; + display: -moz-inline-box; + display: inline-block; +} +.mq-math-mode .mq-supsub { + text-align: left; + font-size: 90%; + vertical-align: -0.5em; +} +.mq-math-mode .mq-supsub.mq-sup-only { + vertical-align: 0.5em; +} +.mq-math-mode .mq-supsub.mq-sup-only .mq-sup { + display: inline-block; + vertical-align: text-bottom; +} +.mq-math-mode .mq-supsub .mq-sup { + display: block; +} +.mq-math-mode .mq-supsub .mq-sub { + display: block; + float: left; +} +.mq-math-mode .mq-supsub .mq-binary-operator { + padding: 0 0.1em; +} +.mq-math-mode .mq-supsub .mq-fraction { + font-size: 70%; +} +.mq-math-mode sup.mq-nthroot { + font-size: 80%; + vertical-align: 0.8em; + margin-right: -0.6em; + margin-left: 0.2em; + min-width: 0.5em; +} +.mq-math-mode .mq-paren { + padding: 0 0.1em; + vertical-align: top; + -webkit-transform-origin: center 0.06em; + -moz-transform-origin: center 0.06em; + -ms-transform-origin: center 0.06em; + -o-transform-origin: center 0.06em; + transform-origin: center 0.06em; +} +.mq-math-mode .mq-paren.mq-ghost { + color: silver; +} +.mq-math-mode .mq-paren + span { + margin-top: 0.1em; + margin-bottom: 0.1em; +} +.mq-math-mode .mq-array { + vertical-align: middle; + text-align: center; +} +.mq-math-mode .mq-array > span { + display: block; +} +.mq-math-mode .mq-operator-name { + font-family: Symbola, 'Times New Roman', serif; + line-height: 0.9; + font-style: normal; +} +.mq-math-mode var.mq-operator-name.mq-first { + padding-left: 0.2em; +} +.mq-math-mode var.mq-operator-name.mq-last, +.mq-math-mode .mq-supsub.mq-after-operator-name { + padding-right: 0.2em; +} +.mq-math-mode .mq-fraction { + font-size: 90%; + text-align: center; + vertical-align: -0.4em; + padding: 0 0.2em; +} +.mq-math-mode .mq-fraction, +.mq-math-mode .mq-large-operator, +.mq-math-mode x:-moz-any-link { + display: -moz-groupbox; +} +.mq-math-mode .mq-fraction, +.mq-math-mode .mq-large-operator, +.mq-math-mode x:-moz-any-link, +.mq-math-mode x:default { + display: inline-block; +} +.mq-math-mode .mq-numerator, +.mq-math-mode .mq-denominator { + display: block; +} +.mq-math-mode .mq-numerator { + padding: 0 0.1em; +} +.mq-math-mode .mq-denominator { + border-top: 1px solid; + float: right; + width: 100%; + padding: 0.1em; +} +.mq-math-mode .mq-sqrt-prefix { + padding-top: 0; + position: relative; + top: 0.1em; + vertical-align: top; + -webkit-transform-origin: top; + -moz-transform-origin: top; + -ms-transform-origin: top; + -o-transform-origin: top; + transform-origin: top; +} +.mq-math-mode .mq-sqrt-stem { + border-top: 1px solid; + margin-top: 1px; + padding-left: 0.15em; + padding-right: 0.2em; + margin-right: 0.1em; + padding-top: 1px; +} +.mq-math-mode .mq-vector-prefix { + display: block; + text-align: center; + line-height: 0.25em; + margin-bottom: -0.1em; + font-size: 0.75em; +} +.mq-math-mode .mq-vector-stem { + display: block; +} +.mq-math-mode .mq-large-operator { + vertical-align: -0.2em; + padding: 0.2em; + text-align: center; +} +.mq-math-mode .mq-large-operator .mq-from, +.mq-math-mode .mq-large-operator big, +.mq-math-mode .mq-large-operator .mq-to { + display: block; +} +.mq-math-mode .mq-large-operator .mq-from, +.mq-math-mode .mq-large-operator .mq-to { + font-size: 80%; +} +.mq-math-mode .mq-large-operator .mq-from { + float: right; + /* take out of normal flow to manipulate baseline */ + width: 100%; +} +.mq-math-mode, +.mq-math-mode .mq-editable-field { + cursor: text; + font-family: Symbola, 'Times New Roman', serif; +} +.mq-math-mode .mq-overarrow { + border-top: 1px solid black; + margin-top: 1px; + padding-top: 0.2em; +} +.mq-math-mode .mq-overarrow:before { + display: block; + position: relative; + top: -0.34em; + font-size: 0.5em; + line-height: 0em; + content: '\27A4'; + text-align: right; +} +.mq-math-mode .mq-overarrow.mq-arrow-left:before { + -moz-transform: scaleX(-1); + -o-transform: scaleX(-1); + -webkit-transform: scaleX(-1); + transform: scaleX(-1); + filter: FlipH; + -ms-filter: 'FlipH'; +} +.mq-math-mode .mq-selection, +.mq-editable-field .mq-selection, +.mq-math-mode .mq-selection .mq-non-leaf, +.mq-editable-field .mq-selection .mq-non-leaf, +.mq-math-mode .mq-selection .mq-scaled, +.mq-editable-field .mq-selection .mq-scaled { + background: #b4d5fe !important; + background: Highlight !important; + color: HighlightText; + border-color: HighlightText; +} +.mq-math-mode .mq-selection .mq-matrixed, +.mq-editable-field .mq-selection .mq-matrixed { + background: #39f !important; +} +.mq-math-mode .mq-selection .mq-matrixed-container, +.mq-editable-field .mq-selection .mq-matrixed-container { + filter: progid:DXImageTransform.Microsoft.Chroma(color='#3399FF') !important; +} +.mq-math-mode .mq-selection.mq-blur, +.mq-editable-field .mq-selection.mq-blur, +.mq-math-mode .mq-selection.mq-blur .mq-non-leaf, +.mq-editable-field .mq-selection.mq-blur .mq-non-leaf, +.mq-math-mode .mq-selection.mq-blur .mq-scaled, +.mq-editable-field .mq-selection.mq-blur .mq-scaled, +.mq-math-mode .mq-selection.mq-blur .mq-matrixed, +.mq-editable-field .mq-selection.mq-blur .mq-matrixed { + background: #d4d4d4 !important; + color: black; + border-color: black; +} +.mq-math-mode .mq-selection.mq-blur .mq-matrixed-container, +.mq-editable-field .mq-selection.mq-blur .mq-matrixed-container { + filter: progid:DXImageTransform.Microsoft.Chroma(color='#D4D4D4') !important; +} +.mq-editable-field .mq-textarea, +.mq-math-mode .mq-textarea { + position: relative; + -webkit-user-select: text; + -moz-user-select: text; + user-select: text; +} +.mq-editable-field .mq-textarea *, +.mq-math-mode .mq-textarea *, +.mq-editable-field .mq-selectable, +.mq-math-mode .mq-selectable { + -webkit-user-select: text; + -moz-user-select: text; + user-select: text; + position: absolute; + clip: rect(1em 1em 1em 1em); + -webkit-transform: scale(0); + -moz-transform: scale(0); + -ms-transform: scale(0); + -o-transform: scale(0); + transform: scale(0); + resize: none; + width: 1px; + height: 1px; +} +.mq-math-mode .mq-matrixed { + background: white; + display: -moz-inline-box; + display: inline-block; +} +.mq-math-mode .mq-matrixed-container { + filter: progid:DXImageTransform.Microsoft.Chroma(color='white'); + margin-top: -0.1em; +} diff --git a/src/client/views/nodes/formattedText/EquationEditor.tsx b/src/client/views/nodes/formattedText/EquationEditor.tsx new file mode 100644 index 000000000..b4102e08e --- /dev/null +++ b/src/client/views/nodes/formattedText/EquationEditor.tsx @@ -0,0 +1,87 @@ +import React, { Component, createRef } from 'react'; + +// Import JQuery, required for the functioning of the equation editor +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'); + +(window as any).MathQuill = (window as any).MathQuill.getInterface(1); + +type EquationEditorProps = { + onChange(latex: string): void; + value: string; + spaceBehavesLikeTab?: boolean; + autoCommands: string; + autoOperatorNames: string; + onEnter?(): void; +}; + +/** + * @typedef {EquationEditorProps} props + * @prop {Function} onChange Triggered when content of the equation editor changes + * @prop {string} value Content of the equation handler + * @prop {boolean}[false] spaceBehavesLikeTab Whether spacebar should simulate tab behavior + * @prop {string} autoCommands List of commands for which you only have to type the name of the + * command with a \ in front of it. Examples: pi theta rho sum + * @prop {string} autoOperatorNames List of operators for which you only have to type the name of the + * operator with a \ in front of it. Examples: sin cos tan + * @prop {Function} onEnter Triggered when enter is pressed in the equation editor + * @extends {Component<EquationEditorProps>} + */ +class EquationEditor extends Component<EquationEditorProps> { + element: any; + mathField: any; + ignoreEditEvents: number; + + // Element needs to be in the class format and thus requires a constructor. The steps that are run + // in the constructor is to make sure that React can succesfully communicate with the equation + // editor. + constructor(props: any) { + super(props); + + this.element = createRef(); + this.mathField = null; + + // MathJax apparently fire 2 edit events on startup. + this.ignoreEditEvents = 2; + } + + componentDidMount() { + const { onChange, value, spaceBehavesLikeTab, autoCommands, autoOperatorNames, onEnter } = this.props; + + const config = { + handlers: { + edit: () => { + if (this.ignoreEditEvents > 0) { + this.ignoreEditEvents -= 1; + return; + } + if (this.mathField.latex() !== value) { + onChange(this.mathField.latex()); + } + }, + enter: onEnter, + }, + spaceBehavesLikeTab, + autoCommands, + autoOperatorNames, + }; + + this.mathField = (window as any).MathQuill.MathField(this.element.current, config); + this.mathField.latex(value || ''); + } + + render() { + return <span ref={this.element} style={{ border: '0px', boxShadow: 'None' }} />; + } +} + +export default EquationEditor; diff --git a/src/client/views/nodes/formattedText/EquationView.tsx b/src/client/views/nodes/formattedText/EquationView.tsx index 5e62d94c2..b786c5ffb 100644 --- a/src/client/views/nodes/formattedText/EquationView.tsx +++ b/src/client/views/nodes/formattedText/EquationView.tsx @@ -1,13 +1,13 @@ -import EquationEditor from 'equation-editor-react'; -import { IReactionDisposer, trace } from 'mobx'; +import { IReactionDisposer } from 'mobx'; import { observer } from 'mobx-react'; import { TextSelection } from 'prosemirror-state'; +import * as React from 'react'; import * as ReactDOM from 'react-dom/client'; import { Doc } from '../../../../fields/Doc'; import { StrCast } from '../../../../fields/Types'; import './DashFieldView.scss'; +import EquationEditor from './EquationEditor'; import { FormattedTextBox } from './FormattedTextBox'; -import React = require('react'); export class EquationView { dom: HTMLDivElement; // container for label and value @@ -63,10 +63,10 @@ export class EquationViewInternal extends React.Component<IEquationViewInternal> _fieldKey: string; _ref: React.RefObject<EquationEditor> = React.createRef(); - constructor(props: IEquationViewInternal) { + constructor(props: any) { super(props); - this._fieldKey = this.props.fieldKey; - this._textBoxDoc = this.props.tbox.props.Document; + this._fieldKey = props.fieldKey; + this._textBoxDoc = props.tbox.Document; } componentWillUnmount() { @@ -101,7 +101,7 @@ export class EquationViewInternal extends React.Component<IEquationViewInternal> <EquationEditor ref={this._ref} value={StrCast(this._textBoxDoc[this._fieldKey], 'y=')} - onChange={str => (this._textBoxDoc[this._fieldKey] = str)} + onChange={(str: any) => (this._textBoxDoc[this._fieldKey] = str)} autoCommands="pi theta sqrt sum prod alpha beta gamma rho" autoOperatorNames="sin cos tan" spaceBehavesLikeTab={true} diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.scss b/src/client/views/nodes/formattedText/FormattedTextBox.scss index 818c0cbe7..03ff0436b 100644 --- a/src/client/views/nodes/formattedText/FormattedTextBox.scss +++ b/src/client/views/nodes/formattedText/FormattedTextBox.scss @@ -1,4 +1,4 @@ -@import '../../global/globalCssVariables'; +@import '../../global/globalCssVariables.module.scss'; .ProseMirror { width: 100%; @@ -49,6 +49,7 @@ audiotag:hover { transition: opacity 1s; width: 100%; position: relative; + transform-origin: left top; top: 0; left: 0; } @@ -89,6 +90,7 @@ audiotag:hover { bottom: 0; width: 11; height: 11; + cursor: default; } .formattedTextBox-outer { @@ -590,7 +592,7 @@ footnote::before { } @media only screen and (max-width: 1000px) { - @import '../../global/globalCssVariables'; + @import '../../global/globalCssVariables.module.scss'; .ProseMirror { width: 100%; diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx index 563b6a25d..f9cef1a60 100644 --- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx +++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx @@ -1,8 +1,7 @@ import { IconProp } from '@fortawesome/fontawesome-svg-core'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { Tooltip } from '@material-ui/core'; -import { isEqual } from 'lodash'; -import { action, computed, IReactionDisposer, observable, ObservableSet, reaction, runInAction } from 'mobx'; +import { Tooltip } from '@mui/material'; +import { action, computed, IReactionDisposer, makeObservable, observable, ObservableSet, reaction, runInAction } from 'mobx'; import { observer } from 'mobx-react'; import { baseKeymap, selectAll } from 'prosemirror-commands'; import { history } from 'prosemirror-history'; @@ -11,21 +10,20 @@ import { keymap } from 'prosemirror-keymap'; import { Fragment, Mark, Node, Slice } from 'prosemirror-model'; import { EditorState, NodeSelection, Plugin, TextSelection, Transaction } from 'prosemirror-state'; import { EditorView } from 'prosemirror-view'; +import * as React from 'react'; import { BsMarkdownFill } from 'react-icons/bs'; import { DateField } from '../../../../fields/DateField'; import { Doc, DocListCast, Field, Opt } from '../../../../fields/Doc'; -import { AclAdmin, AclAugment, AclEdit, AclSelfEdit, DocCss, ForceServerWrite, Height, UpdatingFromServer, Width } from '../../../../fields/DocSymbols'; +import { AclAdmin, AclAugment, AclEdit, AclSelfEdit, DocCss, DocData, ForceServerWrite, UpdatingFromServer } from '../../../../fields/DocSymbols'; import { Id } from '../../../../fields/FieldSymbols'; import { InkTool } from '../../../../fields/InkField'; import { List } from '../../../../fields/List'; import { PrefetchProxy } from '../../../../fields/Proxy'; import { RichTextField } from '../../../../fields/RichTextField'; -import { RichTextUtils } from '../../../../fields/RichTextUtils'; import { ComputedField } from '../../../../fields/ScriptField'; import { BoolCast, Cast, DocCast, FieldValue, NumCast, ScriptCast, StrCast } from '../../../../fields/Types'; import { GetEffectiveAcl, TraceMobx } from '../../../../fields/util'; import { addStyleSheet, addStyleSheetRule, clearStyleSheetRules, emptyFunction, numberRange, returnFalse, returnZero, setupMoveUpEvents, smoothScroll, unimplementedFunction, Utils } from '../../../../Utils'; -import { GoogleApiClientUtils, Pulls, Pushes } from '../../../apis/google_docs/GoogleApiClientUtils'; import { gptAPICall, GPTCallType } from '../../../apis/gpt/GPT'; import { DocServer } from '../../../DocServer'; import { Docs, DocUtils } from '../../../documents/Documents'; @@ -44,17 +42,17 @@ import { CollectionStackingView } from '../../collections/CollectionStackingView import { CollectionTreeView } from '../../collections/CollectionTreeView'; import { ContextMenu } from '../../ContextMenu'; import { ContextMenuProps } from '../../ContextMenuItem'; -import { ViewBoxAnnotatableComponent } from '../../DocComponent'; -import { DocumentButtonBar } from '../../DocumentButtonBar'; +import { ViewBoxAnnotatableComponent, ViewBoxInterface } from '../../DocComponent'; import { Colors } from '../../global/globalEnums'; import { LightboxView } from '../../LightboxView'; import { AnchorMenu } from '../../pdf/AnchorMenu'; import { GPTPopup } from '../../pdf/GPTPopup/GPTPopup'; import { SidebarAnnos } from '../../SidebarAnnos'; import { StyleProp } from '../../StyleProvider'; -import { DocFocusOptions, DocumentView, DocumentViewInternal, OpenWhere } from '../DocumentView'; -import { FieldView, FieldViewProps } from '../FieldView'; -import { LinkDocPreview } from '../LinkDocPreview'; +import { media_state } from '../AudioBox'; +import { DocumentView, DocumentViewInternal, OpenWhere } from '../DocumentView'; +import { FocusViewOptions, FieldView, FieldViewProps } from '../FieldView'; +import { LinkInfo } from '../LinkDocPreview'; import { PinProps, PresBox } from '../trails'; import { DashDocCommentView } from './DashDocCommentView'; import { DashDocView } from './DashDocView'; @@ -69,19 +67,9 @@ import { RichTextMenu, RichTextMenuPlugin } from './RichTextMenu'; import { RichTextRules } from './RichTextRules'; import { schema } from './schema_rts'; import { SummaryView } from './SummaryView'; -import applyDevTools = require('prosemirror-dev-tools'); -import React = require('react'); -import { media_state } from '../AudioBox'; -import { setCORS } from 'google-translate-api-browser'; -import { isDarkMode } from '../../../util/reportManager/reportManagerUtils'; -// setting up cors-anywhere server address -const translate = setCORS('http://cors-anywhere.herokuapp.com/'); -export const GoogleRef = 'googleDocId'; -type PullHandler = (exportState: Opt<GoogleApiClientUtils.Docs.ImportResult>, dataDoc: Doc) => void; - -export interface FormattedTextBoxProps {} +// import * as applyDevTools from 'prosemirror-dev-tools'; @observer -export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps & FormattedTextBoxProps>() { +export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implements ViewBoxInterface { public static LayoutString(fieldStr: string) { return FieldView.LayoutString(FormattedTextBox, fieldStr); } @@ -94,12 +82,14 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps static _bulletStyleSheet: any = addStyleSheet(); static _userStyleSheet: any = addStyleSheet(); static _hadSelection: boolean = false; + private _selectionHTML: string | undefined; private _sidebarRef = React.createRef<SidebarAnnos>(); private _sidebarTagRef = React.createRef<React.Component>(); private _ref: React.RefObject<HTMLDivElement> = React.createRef(); private _scrollRef: React.RefObject<HTMLDivElement> = React.createRef(); private _editorView: Opt<EditorView>; public _applyingChange: string = ''; + private _inDrop = false; private _finishingLink = false; private _searchIndex = 0; private _lastTimedMark: Mark | undefined = undefined; @@ -110,6 +100,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps private _recordingStart: number = 0; private _ignoreScroll = false; private _lastText = ''; + private _hadDownFocus = false; private _focusSpeed: Opt<number>; private _keymap: any = undefined; private _rules: RichTextRules | undefined; @@ -117,6 +108,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps private _forceDownNode: Node | undefined; private _downX = 0; private _downY = 0; + private _downTime = 0; private _break = true; public ProseRef?: HTMLDivElement; public get EditorView() { @@ -130,7 +122,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps } @computed get noSidebar() { - return this.props.docViewPath().lastElement()?.props.hideDecorationTitle || this.props.noSidebar || this.Document._layout_noSidebar; + return this.DocumentView?.()._props.hideDecorationTitle || this._props.noSidebar || this.Document._layout_noSidebar; } @computed get layout_sidebarWidthPercent() { return this._showSidebar ? '20%' : StrCast(this.layoutDoc._layout_sidebarWidthPercent, '0%'); @@ -139,19 +131,19 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps return StrCast(this.layoutDoc.sidebar_color, StrCast(this.layoutDoc[this.fieldKey + '_backgroundColor'], '#e4e4e4')); } @computed get layout_autoHeight() { - return (this.props.forceAutoHeight || this.layoutDoc._layout_autoHeight) && !this.props.ignoreAutoHeight; + return (this._props.forceAutoHeight || this.layoutDoc._layout_autoHeight) && !this._props.ignoreAutoHeight; } @computed get textHeight() { - return NumCast(this.rootDoc[this.fieldKey + '_height']); + return NumCast(this.dataDoc[this.fieldKey + '_height']); } @computed get scrollHeight() { - return NumCast(this.rootDoc[this.fieldKey + '_scrollHeight']); + return NumCast(this.dataDoc[this.fieldKey + '_scrollHeight']); } @computed get sidebarHeight() { - return !this.sidebarWidth() ? 0 : NumCast(this.rootDoc[this.SidebarKey + '_height']); + return !this.sidebarWidth() ? 0 : NumCast(this.dataDoc[this.SidebarKey + '_height']); } @computed get titleHeight() { - return this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.HeaderMargin) || 0; + return this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.HeaderMargin) || 0; } @computed get layout_autoHeightMargins() { return this.titleHeight + NumCast(this.layoutDoc._layout_autoHeightMargins); @@ -163,8 +155,8 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps !this.dataDoc[`${this.fieldKey}_recordingSource`] && (this.dataDoc.mediaState = value ? media_state.Recording : undefined); } @computed get config() { - this._keymap = buildKeymap(schema, this.props); - this._rules = new RichTextRules(this.rootDoc, this); + this._keymap = buildKeymap(schema, this._props); + this._rules = new RichTextRules(this.Document, this); return { schema, plugins: [ @@ -188,7 +180,10 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps private gptRes: string = ''; public static PasteOnLoad: ClipboardEvent | undefined; - public static SelectOnLoad = ''; + 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) { @@ -206,8 +201,9 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps return url.startsWith(document.location.origin) ? new URL(url).pathname.split('doc/').lastElement() : ''; // docId } - constructor(props: any) { + constructor(props: FieldViewProps) { super(props); + makeObservable(this); FormattedTextBox.Instance = this; this._recordingStart = Date.now(); } @@ -247,13 +243,13 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps } getAnchor = (addAsAnnotation: boolean, pinProps?: PinProps) => { - if (!pinProps && this._editorView?.state.selection.empty) return this.rootDoc; - const anchor = Docs.Create.ConfigDocument({ title: StrCast(this.rootDoc.title), annotationOn: this.rootDoc }); + if (!pinProps && this._editorView?.state.selection.empty) return this.Document; + const anchor = Docs.Create.ConfigDocument({ title: StrCast(this.Document.title), annotationOn: this.Document }); 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.rootDoc); + PresBox.pinDocView(anchor, { pinDocLayout: pinProps?.pinDocLayout, pinData: { ...(pinProps?.pinData ?? {}), scrollable: true } }, this.Document); return anchor; }; @@ -274,14 +270,15 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps if (target) { anchor.followLinkAudio = true; let stopFunc: any; - Doc.GetProto(target).mediaState = media_state.Recording; - Doc.GetProto(target).audioAnnoState = 'recording'; - DocumentViewInternal.recordAudioAnnotation(Doc.GetProto(target), Doc.LayoutFieldKey(target), stop => (stopFunc = stop)); + const targetData = target[DocData]; + targetData.mediaState = media_state.Recording; + targetData.audioAnnoState = 'recording'; + DocumentViewInternal.recordAudioAnnotation(targetData, Doc.LayoutFieldKey(target), stop => (stopFunc = stop)); let reactionDisposer = reaction( () => target.mediaState, action(dictation => { if (!dictation) { - Doc.GetProto(target).audioAnnoState = 'stopped'; + targetData.audioAnnoState = 'stopped'; stopFunc(); reactionDisposer(); } @@ -308,15 +305,24 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps e.preventDefault(); e.stopPropagation(); const targetCreator = (annotationOn?: Doc) => { - const target = DocUtils.GetNewTextDoc('Note linked to ' + this.rootDoc.title, 0, 0, 100, 100, undefined, annotationOn); - FormattedTextBox.SelectOnLoad = target[Id]; + const target = DocUtils.GetNewTextDoc('Note linked to ' + this.Document.title, 0, 0, 100, 100, undefined, annotationOn); + FormattedTextBox.SetSelectOnLoad(target); return target; }; - DragManager.StartAnchorAnnoDrag([ele], new DragManager.AnchorAnnoDragData(this.props.docViewPath().lastElement(), () => this.getAnchor(true), targetCreator), e.pageX, e.pageY); + DragManager.StartAnchorAnnoDrag([ele], new DragManager.AnchorAnnoDragData(this.DocumentView?.()!, () => this.getAnchor(true), targetCreator), e.pageX, e.pageY); }); const coordsB = this._editorView!.coordsAtPos(this._editorView!.state.selection.to); - this.props.isSelected(true) && AnchorMenu.Instance.jumpTo(coordsB.left, coordsB.bottom); + this._props.rootSelected?.() && AnchorMenu.Instance.jumpTo(coordsB.left, coordsB.bottom); + let ele: Opt<HTMLDivElement> = undefined; + try { + const contents = window.getSelection()?.getRangeAt(0).cloneContents(); + if (contents) { + ele = document.createElement('div'); + ele.append(contents); + } + this._selectionHTML = ele?.innerHTML; + } catch (e) {} }; dispatchTransaction = (tx: Transaction) => { @@ -328,7 +334,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps const newText = state.doc.textBetween(0, state.doc.content.size, ' \n'); 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.rootDoc !== this.layoutDoc ? prevData : undefined; // the default text stored in a layout template + 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 effectiveAcl = GetEffectiveAcl(dataDoc); @@ -350,11 +356,11 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps textChange && (dataDoc[this.fieldKey + '_modificationDate'] = new DateField(new Date(Date.now()))); if ((!prevData && !protoData) || newText || (!newText && !templateData)) { // if no template, or there's text that didn't come from the layout template, write it to the document. (if this is driven by a template, then this overwrites the template text which is intended) - if ((this._finishingLink || this.props.isContentActive()) && removeSelection(newJson) !== removeSelection(prevData?.Data)) { + if ((this._finishingLink || this._props.isContentActive() || this._inDrop) && removeSelection(newJson) !== removeSelection(prevData?.Data)) { const numstring = NumCast(dataDoc[this.fieldKey], null); dataDoc[this.fieldKey] = numstring !== undefined ? Number(newText) : new RichTextField(newJson, newText); dataDoc[this.fieldKey + '_noTemplate'] = true; // mark the data field as being split from the template if it has been edited - textChange && ScriptCast(this.layoutDoc.onTextChanged, null)?.script.run({ this: this.layoutDoc, self: this.rootDoc, text: newText }); + textChange && ScriptCast(this.layoutDoc.onTextChanged, null)?.script.run({ this: this.Document, text: newText }); unchanged = false; } } else { @@ -362,7 +368,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps dataDoc[this.fieldKey] = undefined; this._editorView.updateState(EditorState.fromJSON(this.config, JSON.parse((protoData || prevData).Data))); dataDoc[this.fieldKey + '_noTemplate'] = undefined; // mark the data field as not being split from any template it might have - ScriptCast(this.layoutDoc.onTextChanged, null)?.script.run({ this: this.layoutDoc, self: this.rootDoc, text: newText }); + ScriptCast(this.layoutDoc.onTextChanged, null)?.script.run({ this: this.layoutDoc, text: newText }); unchanged = false; } this._applyingChange = ''; @@ -379,7 +385,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps this._editorView.updateState(EditorState.fromJSON(this.config, json)); } } - if (window.getSelection()?.isCollapsed && this.props.isSelected()) { + if (window.getSelection()?.isCollapsed && this._props.rootSelected?.()) { AnchorMenu.Instance.fadeOut(true); } } @@ -427,7 +433,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps autoLink = () => { const newAutoLinks = new Set<Doc>(); - const oldAutoLinks = LinkManager.Links(this.props.Document).filter(link => link.link_relationship === LinkManager.AutoKeywords); + const oldAutoLinks = LinkManager.Links(this.Document).filter(link => link.link_relationship === LinkManager.AutoKeywords); if (this._editorView?.state.doc.textContent) { const isNodeSel = this._editorView.state.selection instanceof NodeSelection; const f = this._editorView.state.selection.from; @@ -435,22 +441,21 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps 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); - DocListCast(Doc.MyPublishedDocs.data).forEach(term => (tr = this.hyperlinkTerm(tr, term, newAutoLinks))); + Doc.MyPublishedDocs.forEach(term => (tr = this.hyperlinkTerm(tr, term, newAutoLinks))); tr = tr.setSelection(isNodeSel && false ? new NodeSelection(tr.doc.resolve(f)) : new TextSelection(tr.doc.resolve(f), tr.doc.resolve(t))); this._editorView?.dispatch(tr); - // this.prepareForTyping(); } - oldAutoLinks.filter(oldLink => !newAutoLinks.has(oldLink) && oldLink.link_anchor_2 !== this.rootDoc).forEach(LinkManager.Instance.deleteLink); + oldAutoLinks.filter(oldLink => !newAutoLinks.has(oldLink) && oldLink.link_anchor_2 !== this.Document).forEach(LinkManager.Instance.deleteLink); }; updateTitle = () => { const title = StrCast(this.dataDoc.title, Cast(this.dataDoc.title, RichTextField, null)?.Text); if ( - !this.props.dontRegisterView && // (this.props.Document.isTemplateForField === "text" || !this.props.Document.isTemplateForField) && // only update the title if the data document's data field is changing + !this._props.dontRegisterView && // (this.Document.isTemplateForField === "text" || !this.Document.isTemplateForField) && // only update the title if the data document's data field is changing (title.startsWith('-') || title.startsWith('@')) && this._editorView && !this.dataDoc.title_custom && - (Doc.LayoutFieldKey(this.rootDoc) === this.fieldKey || this.fieldKey === 'text') + (Doc.LayoutFieldKey(this.Document) === this.fieldKey || this.fieldKey === 'text') ) { let node = this._editorView.state.doc; while (node.firstChild && node.firstChild.type.name !== 'text') node = node.firstChild; @@ -461,7 +466,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps if (!(cfield instanceof ComputedField)) { this.dataDoc.title = (prefix + str.substring(0, Math.min(40, str.length)) + (str.length > 40 ? '...' : '')).trim(); if (str.startsWith('@') && str.length > 1) { - Doc.AddDocToList(Doc.MyPublishedDocs, undefined, this.rootDoc); + Doc.AddToMyPublished(this.Document); } } } @@ -470,7 +475,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps // creates links between terms in a document and published documents (myPublishedDocs) that have titles starting with an '@' hyperlinkTerm = (tr: any, target: Doc, newAutoLinks: Set<Doc>) => { const editorView = this._editorView; - if (editorView && (editorView as any).docView && !Doc.AreProtosEqual(target, this.rootDoc)) { + if (editorView && (editorView as any).docView && !Doc.AreProtosEqual(target, this.Document)) { const autoLinkTerm = StrCast(target.title).replace(/^@/, ''); var alink: Doc | undefined; this.findInNode(editorView, editorView.state.doc, autoLinkTerm).forEach(sel => { @@ -481,16 +486,17 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps 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.rootDoc).find( + (LinkManager.Links(this.Document).find( link => - Doc.AreProtosEqual(Cast(link.link_anchor_1, Doc, null), this.rootDoc) && // + Doc.AreProtosEqual(Cast(link.link_anchor_1, Doc, null), this.Document) && // Doc.AreProtosEqual(Cast(link.link_anchor_2, Doc, null), target) ) || - DocUtils.MakeLink(this.rootDoc, target, { link_relationship: LinkManager.AutoKeywords })!); + DocUtils.MakeLink(this.Document, target, { link_relationship: LinkManager.AutoKeywords })!); newAutoLinks.add(alink); - const allAnchors = [{ href: Doc.localServerPath(target), title: 'a link', anchorId: this.props.Document[Id] }]; + // DocCast(alink.link_anchor_1).followLinkLocation = 'add:right'; + const allAnchors = [{ href: Doc.localServerPath(target), title: 'a link', anchorId: this.Document[Id] }]; allAnchors.push(...(node.marks.find((m: Mark) => m.type.name === schema.marks.autoLinkAnchor.name)?.attrs.allAnchors ?? [])); - const link = editorView.state.schema.marks.autoLinkAnchor.create({ allAnchors, title: 'auto term', location: 'add:right' }); + const link = editorView.state.schema.marks.autoLinkAnchor.create({ allAnchors, title: 'auto term' }); tr = tr.addMark(pos, pos + node.nodeSize, link); } }); @@ -557,7 +563,6 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps }; @undoBatch - @action drop = (e: Event, de: DragManager.DropEvent) => { if (de.complete.annoDragData) { de.complete.annoDragData.dropDocCreator = () => this.getAnchor(true); @@ -567,45 +572,51 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps if (dragData) { const dataDoc = Doc.IsDelegateField(DocCast(this.layoutDoc.proto), this.fieldKey) ? DocCast(this.layoutDoc.proto) : this.dataDoc; const effectiveAcl = GetEffectiveAcl(dataDoc); - let added = [AclEdit, AclAdmin, AclSelfEdit].includes(effectiveAcl); - const draggedDoc = dragData.draggedDocuments.lastElement(); - if (added) { + const draggedDoc = dragData.droppedDocuments.lastElement(); + let added: Opt<boolean>; + const dropAction = dragData.dropAction || dragData.userDropAction; + if ([AclEdit, AclAdmin, AclSelfEdit].includes(effectiveAcl)) { // replace text contents when dragging with Alt if (de.altKey) { const fieldKey = Doc.LayoutFieldKey(draggedDoc); - if (draggedDoc[fieldKey] instanceof RichTextField && !Doc.AreProtosEqual(draggedDoc, this.props.Document)) { + if (draggedDoc[fieldKey] instanceof RichTextField && !Doc.AreProtosEqual(draggedDoc, this.Document)) { Doc.GetProto(this.dataDoc)[this.fieldKey] = Field.Copy(draggedDoc[fieldKey]); } // embed document when drag marked as embed - } else if (de.embedKey) { + } else if (de.embedKey || dropAction) { const node = schema.nodes.dashDoc.create({ - width: draggedDoc[Width](), - height: draggedDoc[Height](), + width: NumCast(draggedDoc._width), + height: NumCast(draggedDoc._height), title: 'dashDoc', docId: draggedDoc[Id], float: 'unset', }); - if (!['embed', 'copy'].includes((dragData.dropAction ?? '') as any)) { + if (!['embed', 'copy'].includes((dropAction ?? '') as any)) { added = dragData.removeDocument?.(draggedDoc) ? true : false; + } else { + added = true; } if (added) { draggedDoc._freeform_fitContentsToBox = true; - Doc.SetContainer(draggedDoc, this.rootDoc); + Doc.SetContainer(draggedDoc, this.Document); const view = this._editorView!; try { + 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 = false; + } finally { + this._inDrop = false; } } } } // otherwise, fall through to outer collection to handle drop - !added && e.preventDefault(); - e.stopPropagation(); + added === false && e.preventDefault(); + added === true && e.stopPropagation(); return added; } return false; @@ -705,14 +716,16 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps @action toggleSidebar = (preview: boolean = false) => { + const defaultSidebar = 250; const prevWidth = 1 - this.sidebarWidth() / Number(getComputedStyle(this._ref.current!).width.replace('px', '')); if (preview) this._showSidebar = true; else { this.layoutDoc[this.SidebarKey + '_freeform_scale_max'] = 1; - this.layoutDoc._layout_showSidebar = (this.layoutDoc._layout_sidebarWidthPercent = StrCast(this.layoutDoc._layout_sidebarWidthPercent, '0%') === '0%' ? '50%' : '0%') !== '0%'; + this.layoutDoc._layout_showSidebar = + (this.layoutDoc._layout_sidebarWidthPercent = StrCast(this.layoutDoc._layout_sidebarWidthPercent, '0%') === '0%' ? `${(defaultSidebar / (NumCast(this.layoutDoc._width) + defaultSidebar)) * 100}%` : '0%') !== '0%'; } - this.layoutDoc._width = !preview && this.SidebarShown ? NumCast(this.layoutDoc._width) * 2 : Math.max(20, NumCast(this.layoutDoc._width) * prevWidth); + this.layoutDoc._width = !preview && this.SidebarShown ? NumCast(this.layoutDoc._width) + defaultSidebar : Math.max(20, NumCast(this.layoutDoc._width) * prevWidth); }; sidebarDown = (e: React.PointerEvent) => { const batch = UndoManager.StartBatch('toggle sidebar'); @@ -729,12 +742,12 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps ); }; sidebarMove = (e: PointerEvent, down: number[], delta: number[]) => { - const localDelta = this.props + const localDelta = this._props .ScreenToLocalTransform() - .scale(this.props.NativeDimScaling?.() || 1) + .scale(this._props.NativeDimScaling?.() || 1) .transformDirection(delta[0], delta[1]); const sidebarWidth = (NumCast(this.layoutDoc._width) * Number(this.layout_sidebarWidthPercent.replace('%', ''))) / 100; - const width = this.layoutDoc[Width]() + localDelta[0]; + const width = NumCast(this.layoutDoc._width) + localDelta[0]; this.layoutDoc._layout_sidebarWidthPercent = Math.max(0, (sidebarWidth + localDelta[0]) / width) * 100 + '%'; this.layoutDoc.width = width; this.layoutDoc._layout_showSidebar = this.layoutDoc._layout_sidebarWidthPercent !== '0%'; @@ -745,15 +758,15 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps deleteAnnotation = (anchor: Doc) => { const batch = UndoManager.StartBatch('delete link'); LinkManager.Instance.deleteLink(LinkManager.Links(anchor)[0]); - // const docAnnotations = DocListCast(this.props.dataDoc[this.fieldKey]); - // this.props.dataDoc[this.fieldKey] = new List<Doc>(docAnnotations.filter(a => a !== this.annoTextRegion)); + // const docAnnotations = DocListCast(this._props.dataDoc[this.fieldKey]); + // this._props.dataDoc[this.fieldKey] = new List<Doc>(docAnnotations.filter(a => a !== this.annoTextRegion)); // AnchorMenu.Instance.fadeOut(true); - this.props.select(false); + this._props.select(false); setTimeout(batch.end); // wait for reaction to remove link from document }; @undoBatch - pinToPres = (anchor: Doc) => this.props.pinToPres(anchor, {}); + pinToPres = (anchor: Doc) => this._props.pinToPres(anchor, {}); @undoBatch makeTargetToggle = (anchor: Doc) => (anchor.followLinkToggle = !anchor.followLinkToggle); @@ -763,7 +776,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps const trail = DocCast(anchor.presentationTrail); if (trail) { Doc.ActivePresentation = trail; - this.props.addDocTab(trail, OpenWhere.replaceRight); + this._props.addDocTab(trail, OpenWhere.replaceRight); } }; @@ -781,7 +794,10 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps ?.trim() .split(' ') .filter(h => h); - const anchorDoc = Array.from(hrefs).lastElement().replace(Doc.localServerPath(), '').split('?')[0]; + const anchorDoc = Array.from(hrefs ?? []) + .lastElement() + .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)); @@ -810,7 +826,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps changeItems.push({ description: 'plain', event: undoBatch(() => { - Doc.setNativeView(this.rootDoc); + Doc.setNativeView(this.Document); this.layoutDoc.layout_autoHeightMargins = undefined; }), icon: 'eye', @@ -819,8 +835,8 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps description: 'metadata', event: undoBatch(() => { this.dataDoc.layout_meta = Cast(Doc.UserDoc().emptyHeader, Doc, null)?.layout; - this.rootDoc.layout_fieldKey = 'layout_meta'; - setTimeout(() => (this.rootDoc._headerHeight = this.rootDoc._layout_autoHeightMargins = 50), 50); + this.Document.layout_fieldKey = 'layout_meta'; + setTimeout(() => (this.layoutDoc._headerHeight = this.layoutDoc._layout_autoHeightMargins = 50), 50); }), icon: 'eye', }); @@ -831,8 +847,8 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps description: StrCast(note.title), event: undoBatch(() => { this.layoutDoc.layout_autoHeightMargins = undefined; - Doc.setNativeView(this.rootDoc); - DocUtils.makeCustomViewClicked(this.rootDoc, Docs.Create.TreeDocument, StrCast(note.title), note); + Doc.setNativeView(this.Document); + DocUtils.makeCustomViewClicked(this.Document, Docs.Create.TreeDocument, StrCast(note.title), note); }), icon: icon, }); @@ -854,69 +870,50 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps icon: !FormattedTextBox._globalHighlights.has(option) ? 'highlighter' : 'remove-format', }) ); + const appearance = cm.findByDescription('Appearance...'); + const appearanceItems: ContextMenuProps[] = appearance && 'subitems' in appearance ? appearance.subitems : []; - const uicontrols: ContextMenuProps[] = []; - uicontrols.push({ + appearanceItems.push({ description: !this.Document._layout_noSidebar ? 'Hide Sidebar Handle' : 'Show Sidebar Handle', event: () => (this.layoutDoc._layout_noSidebar = !this.layoutDoc._layout_noSidebar), icon: !this.Document._layout_noSidebar ? 'eye-slash' : 'eye', }); - uicontrols.push({ + appearanceItems.push({ description: (this.Document._layout_enableAltContentUI ? 'Hide' : 'Show') + ' Alt Content UI', event: () => (this.layoutDoc._layout_enableAltContentUI = !this.layoutDoc._layout_enableAltContentUI), icon: !this.Document._layout_enableAltContentUI ? 'eye-slash' : 'eye', }); - !Doc.noviceMode && uicontrols.push({ description: 'Show Highlights...', noexpand: true, subitems: highlighting, icon: 'hand-point-right' }); + !Doc.noviceMode && appearanceItems.push({ description: 'Show Highlights...', noexpand: true, subitems: highlighting, icon: 'hand-point-right' }); !Doc.noviceMode && - uicontrols.push({ + appearanceItems.push({ description: 'Broadcast Message', - event: () => DocServer.GetRefField('rtfProto').then(proto => proto instanceof Doc && (proto.BROADCAST_MESSAGE = Cast(this.rootDoc[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', }); - cm.addItem({ description: 'UI Controls...', subitems: uicontrols, icon: 'asterisk' }); - const appearance = cm.findByDescription('Appearance...'); - const appearanceItems = appearance && 'subitems' in appearance ? appearance.subitems : []; appearanceItems.push({ description: 'Change Style...', noexpand: true, subitems: changeItems, icon: 'external-link-alt' }); - // this.rootDoc.isTemplateDoc && appearanceItems.push({ description: "Make Default Layout", event: async () => Doc.UserDoc().defaultTextLayout = new PrefetchProxy(this.rootDoc), icon: "eye" }); !Doc.noviceMode && appearanceItems.push({ description: 'Make Default Layout', event: () => { if (!this.layoutDoc.isTemplateDoc) { - const title = StrCast(this.rootDoc.title); - this.rootDoc.title = 'text'; - MakeTemplate(this.rootDoc, true, title); - } else if (!this.rootDoc.isTemplateDoc) { - const title = StrCast(this.rootDoc.title); - this.rootDoc.title = 'text'; - this.rootDoc.layout = this.layoutDoc.layout as string; - this.rootDoc.title = this.layoutDoc.isTemplateForField as string; - this.rootDoc.isTemplateDoc = false; - this.rootDoc.isTemplateForField = ''; - this.rootDoc.layout_fieldKey = 'layout'; - MakeTemplate(this.rootDoc, true, title); - setTimeout(() => { - this.rootDoc._layout_autoHeight = this.layoutDoc._layout_autoHeight; // layout_autoHeight, width and height - this.rootDoc._width = this.layoutDoc._width || 300; // are stored on the template, since we're getting rid of the old template - this.rootDoc._height = this.layoutDoc._height || 200; // we need to copy them over to the root. This should probably apply to all '_' fields - this.rootDoc._backgroundColor = Cast(this.layoutDoc._backgroundColor, 'string', null); - this.rootDoc.backgroundColor = Cast(this.layoutDoc.backgroundColor, 'string', null); - }, 10); + const title = StrCast(this.Document.title); + this.Document.title = 'text'; + MakeTemplate(this.Document, true, title); } - Doc.UserDoc().defaultTextLayout = new PrefetchProxy(this.rootDoc); - Doc.AddDocToList(Cast(Doc.UserDoc().template_notes, Doc, null), 'data', this.rootDoc); + Doc.UserDoc().defaultTextLayout = new PrefetchProxy(this.Document); + Doc.AddDocToList(Cast(Doc.UserDoc().template_notes, Doc, null), 'data', this.Document); }, icon: 'eye', }); - cm.addItem({ description: 'Appearance...', subitems: appearanceItems, icon: 'eye' }); + !appearance && appearanceItems.length && cm.addItem({ description: 'Appearance...', subitems: appearanceItems, icon: 'eye' }); const options = cm.findByDescription('Options...'); const optionItems = options && 'subitems' in options ? options.subitems : []; optionItems.push({ description: `Generate Dall-E Image`, event: () => this.generateImage(), icon: 'star' }); optionItems.push({ description: `Ask GPT-3`, event: () => this.askGPT(), icon: 'lightbulb' }); - this.props.renderDepth && + this._props.renderDepth && optionItems.push({ description: !this.Document._createDocOnCR ? 'Create New Doc on Carriage Return' : 'Allow Carriage Returns', event: () => (this.layoutDoc._createDocOnCR = !this.layoutDoc._createDocOnCR), @@ -959,8 +956,8 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps generateImage = async () => { GPTPopup.Instance?.setTextAnchor(this.getAnchor(false)); - GPTPopup.Instance?.setImgTargetDoc(this.rootDoc); - GPTPopup.Instance.addToCollection = this.props.addDocument; + GPTPopup.Instance?.setImgTargetDoc(this.Document); + GPTPopup.Instance.addToCollection = this._props.addDocument; GPTPopup.Instance.setImgDesc((this.dataDoc.text as RichTextField)?.Text); GPTPopup.Instance.generateImage(); }; @@ -1000,7 +997,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps }; const link = DocUtils.MakeLinkToActiveAudio(textanchorFunc, false).lastElement(); if (link) { - Doc.GetProto(link).isDictation = true; + link[DocData].isDictation = true; const audioanchor = Cast(link.link_anchor_2, Doc, null); const textanchor = Cast(link.link_anchor_1, Doc, null); if (audioanchor) { @@ -1010,7 +1007,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps audioId: audioanchor[Id], textId: textanchor[Id], }); - Doc.GetProto(textanchor).title = 'dictation:' + audiotag.attrs.timeCode; + textanchor[DocData].title = 'dictation:' + audiotag.attrs.timeCode; const tr = this._editorView.state.tr.insert(this._editorView.state.doc.content.size, audiotag); const tr2 = tr.setSelection(TextSelection.create(tr.doc, tr.doc.content.size)); this._editorView.dispatch(tr.setSelection(TextSelection.create(tr2.doc, tr2.doc.content.size))); @@ -1055,22 +1052,27 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps this._editorView!.dispatch(tr.removeMark(sel.from, sel.to, splitter)); this.dataDoc[UpdatingFromServer] = this.dataDoc[ForceServerWrite] = false; anchor.text = selectedText; + anchor.text_html = this._selectionHTML ?? selectedText; anchor.title = selectedText.substring(0, 30); + anchor.presentation_zoomText = true; return anchor; } - return anchorDoc ?? this.rootDoc; + return anchorDoc ?? this.Document; } - return anchorDoc ?? this.rootDoc; + return anchorDoc ?? this.Document; } - getView = async (doc: Doc) => { - if (DocListCast(this.rootDoc[this.SidebarKey]).find(anno => Doc.AreProtosEqual(doc.layout_unrendered ? DocCast(doc.annotationOn) : doc, anno))) { - !this.SidebarShown && this.toggleSidebar(false); + getView = async (doc: Doc, options: FocusViewOptions) => { + if (DocListCast(this.dataDoc[this.SidebarKey]).find(anno => Doc.AreProtosEqual(doc.layout_unrendered ? DocCast(doc.annotationOn) : doc, anno))) { + if (!this.SidebarShown) { + this.toggleSidebar(false); + options.didMove = true; + } setTimeout(() => this._sidebarRef?.current?.makeDocUnfiltered(doc)); } return new Promise<Opt<DocumentView>>(res => DocumentManager.Instance.AddViewRenderedCb(doc, dv => res(dv))); }; - focus = (textAnchor: Doc, options: DocFocusOptions) => { + focus = (textAnchor: Doc, options: FocusViewOptions) => { const focusSpeed = options.zoomTime ?? 500; const textAnchorId = textAnchor[Id]; const findAnchorFrag = (frag: Fragment, editor: EditorView) => { @@ -1128,7 +1130,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps setTimeout(() => clearStyleSheetRules(FormattedTextBox._highlightStyleSheet), Math.max(this._focusSpeed || 0, 3000)); return focusSpeed; } else { - return this.props.focus(this.rootDoc, options); + return this._props.focus(this.Document, options); } } }; @@ -1137,12 +1139,12 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps // Since we also monitor all component height changes, this will update the document's height. resetNativeHeight = (scrollHeight: number) => { const nh = this.layoutDoc.isTemplateForField ? 0 : NumCast(this.layoutDoc._nativeHeight); - this.rootDoc[this.fieldKey + '_height'] = scrollHeight; + this.dataDoc[this.fieldKey + '_height'] = scrollHeight; if (nh) this.layoutDoc._nativeHeight = scrollHeight; }; @computed get contentScaling() { - return Doc.NativeAspect(this.rootDoc, this.dataDoc, false) ? this.props.NativeDimScaling?.() || 1 : 1; + return Doc.NativeAspect(this.Document, this.dataDoc, false) ? this._props.NativeDimScaling?.() || 1 : 1; } @action @@ -1164,7 +1166,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps } componentDidMount() { - !this.props.dontSelectOnLoad && this.props.setContentView?.(this); // this tells the DocumentView that this AudioBox is the "content" of the document. this allows the DocumentView to indirectly call getAnchor() on the AudioBox when making a link. + !this._props.dontSelectOnLoad && this._props.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._disposers.breakupDictation = reaction(() => Doc.RecordingEvent, this.breakupDictation); this._disposers.layout_autoHeight = reaction( @@ -1177,7 +1179,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps { fireImmediately: true } ); this._disposers.width = reaction( - () => this.props.PanelWidth(), + () => this._props.PanelWidth(), width => this.tryUpdateScrollHeight() ); this._disposers.scrollHeight = reaction( @@ -1195,13 +1197,13 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps ({ sidebarHeight, textHeight, layout_autoHeight, marginsHeight }) => { const newHeight = this.contentScaling * (marginsHeight + Math.max(sidebarHeight, textHeight)); if ( - (!Array.from(FormattedTextBox._globalHighlights).includes('Bold Text') || this.props.isSelected()) && // + (!Array.from(FormattedTextBox._globalHighlights).includes('Bold Text') || this._props.isSelected()) && // layout_autoHeight && newHeight && - newHeight !== this.rootDoc.height && - !this.props.dontRegisterView + newHeight !== this.layoutDoc.height && + !this._props.dontRegisterView ) { - this.props.setHeight?.(newHeight); + this._props.setHeight?.(newHeight); } }, { fireImmediately: !Array.from(FormattedTextBox._globalHighlights).includes('Bold Text') } @@ -1213,20 +1215,11 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps this._cachedLinks = newLinks; } ); - this._disposers.buttonBar = reaction( - () => DocumentButtonBar.Instance, - instance => { - if (instance) { - this.pullFromGoogleDoc(this.checkState); - this.dataDoc[GoogleRef] && this.dataDoc.googleDocUnchanged && runInAction(() => (instance.isAnimatingFetch = true)); - } - } - ); this._disposers.editorState = reaction( () => { const dataDoc = Doc.IsDelegateField(DocCast(this.layoutDoc?.proto), this.fieldKey) ? DocCast(this.layoutDoc?.proto) : this?.dataDoc; const whichDoc = !this.dataDoc || !this.layoutDoc ? undefined : dataDoc?.[this.fieldKey + '_noTemplate'] || !this.layoutDoc[this.fieldKey] ? dataDoc : this.layoutDoc; - return !whichDoc ? undefined : { data: Cast(whichDoc[this.fieldKey], RichTextField, null), str: Field.toString(DocCast(whichDoc[this.fieldKey])) }; + return !whichDoc ? undefined : { data: Cast(whichDoc[this.fieldKey], RichTextField, null), str: Field.toString(DocCast(whichDoc[this.fieldKey]) ?? StrCast(whichDoc[this.fieldKey])) }; }, incomingValue => { if (this._editorView && this._applyingChange !== this.fieldKey) { @@ -1242,50 +1235,32 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps } } ); - this._disposers.pullDoc = reaction( - () => this.props.Document[Pulls], - () => { - if (!DocumentButtonBar.hasPulledHack) { - DocumentButtonBar.hasPulledHack = true; - this.pullFromGoogleDoc(this.dataDoc.googleDocUnchanged ? this.checkState : this.updateState); - } - } - ); - this._disposers.pushDoc = reaction( - () => this.props.Document[Pushes], - () => { - if (!DocumentButtonBar.hasPushedHack) { - DocumentButtonBar.hasPushedHack = true; - this.pushToGoogleDoc(); - } - } - ); this._disposers.search = reaction( - () => Doc.IsSearchMatch(this.rootDoc), + () => Doc.IsSearchMatch(this.Document), search => (search ? this.highlightSearchTerms([Doc.SearchQuery()], search.searchMatch < 0) : this.unhighlightSearchTerms()), - { fireImmediately: Doc.IsSearchMatchUnmemoized(this.rootDoc) ? true : false } + { fireImmediately: Doc.IsSearchMatchUnmemoized(this.Document) ? true : false } ); this._disposers.selected = reaction( - () => this.props.isSelected(), + () => this._props.rootSelected?.(), action(selected => { //selected && setTimeout(() => this.prepareForTyping()); if (FormattedTextBox._globalHighlights.has('Bold Text')) { this.layoutDoc[DocCss] = this.layoutDoc[DocCss] + 1; // css change happens outside of mobx/react, so this will notify anyone interested in the layout that it has changed } if (RichTextMenu.Instance?.view === this._editorView && !selected) { - RichTextMenu.Instance?.updateMenu(undefined, undefined, undefined); + RichTextMenu.Instance?.updateMenu(undefined, undefined, undefined, undefined); } if (this._editorView && selected) { - RichTextMenu.Instance?.updateMenu(this._editorView, undefined, this.props); + RichTextMenu.Instance?.updateMenu(this._editorView, undefined, this._props, this.layoutDoc); setTimeout(this.autoLink, 20); } }), { fireImmediately: true } ); - if (!this.props.dontRegisterView) { + if (!this._props.dontRegisterView) { this._disposers.record = reaction( () => this._recordingDictation, () => { @@ -1300,7 +1275,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps this._disposers.scroll = reaction( () => NumCast(this.layoutDoc._layout_scrollTop), pos => { - if (!this._ignoreScroll && this._scrollRef.current && !this.props.dontSelectOnLoad) { + if (!this._ignoreScroll && this._scrollRef.current && !this._props.dontSelectOnLoad) { const viewTrans = quickScroll ?? StrCast(this.Document._viewTransition); const durationMiliStr = viewTrans.match(/([0-9]*)ms/); const durationSecStr = viewTrans.match(/([0-9.]*)s/); @@ -1319,80 +1294,6 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps setTimeout(this.tryUpdateScrollHeight, 250); } - pushToGoogleDoc = async () => { - this.pullFromGoogleDoc(async (exportState: Opt<GoogleApiClientUtils.Docs.ImportResult>, dataDoc: Doc) => { - const modes = GoogleApiClientUtils.Docs.WriteMode; - let mode = modes.Replace; - let reference: Opt<GoogleApiClientUtils.Docs.Reference> = Cast(this.dataDoc[GoogleRef], 'string'); - if (!reference) { - mode = modes.Insert; - reference = { title: StrCast(this.dataDoc.title) }; - } - const redo = async () => { - if (this._editorView && reference) { - const content = await RichTextUtils.GoogleDocs.Export(this._editorView.state); - const response = await GoogleApiClientUtils.Docs.write({ reference, content, mode }); - response && (this.dataDoc[GoogleRef] = response.documentId); - const pushSuccess = response !== undefined && !('errors' in response); - dataDoc.googleDocUnchanged = pushSuccess; - DocumentButtonBar.Instance.startPushOutcome(pushSuccess); - } - }; - const undo = () => { - if (exportState && reference) { - const content: GoogleApiClientUtils.Docs.Content = { - text: exportState.text, - requests: [], - }; - GoogleApiClientUtils.Docs.write({ reference, content, mode }); - } - }; - UndoManager.AddEvent({ undo, redo, prop: '' }); - redo(); - }); - }; - - pullFromGoogleDoc = async (handler: PullHandler) => { - const dataDoc = this.dataDoc; - const documentId = StrCast(dataDoc[GoogleRef]); - let exportState: Opt<GoogleApiClientUtils.Docs.ImportResult>; - if (documentId) { - exportState = await RichTextUtils.GoogleDocs.Import(documentId, dataDoc); - } - exportState && UndoManager.RunInBatch(() => handler(exportState, dataDoc), Pulls); - }; - - updateState = (exportState: Opt<GoogleApiClientUtils.Docs.ImportResult>, dataDoc: Doc) => { - let pullSuccess = false; - if (exportState !== undefined) { - pullSuccess = true; - dataDoc[this.fieldKey] = new RichTextField(JSON.stringify(exportState.state.toJSON())); - setTimeout(() => { - if (this._editorView) { - const state = this._editorView.state; - const end = state.doc.content.size - 1; - this._editorView.dispatch(state.tr.setSelection(TextSelection.create(state.doc, end, end))); - } - }, 0); - dataDoc.title = exportState.title; - this.dataDoc.title_custom = true; - dataDoc.googleDocUnchanged = true; - } else { - delete dataDoc[GoogleRef]; - } - DocumentButtonBar.Instance.startPullOutcome(pullSuccess); - }; - - checkState = (exportState: Opt<GoogleApiClientUtils.Docs.ImportResult>, dataDoc: Doc) => { - if (exportState && this._editorView) { - const equalContent = isEqual(this._editorView.state.doc, exportState.state.doc); - const equalTitles = dataDoc.title === exportState.title; - const unchanged = equalContent && equalTitles; - dataDoc.googleDocUnchanged = unchanged; - DocumentButtonBar.Instance.setPullState(unchanged); - } - }; - clipboardTextSerializer = (slice: Slice): string => { let text = '', separated = true; @@ -1430,8 +1331,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps const dashField = view.state.schema.nodes.paragraph.create({}, [ view.state.schema.nodes.dashField.create({ fieldKey: 'text', docId: pdfAnchor[Id], hideKey: true, editable: false }, undefined, [ view.state.schema.marks.linkAnchor.create({ - allAnchors: [{ href: `/doc/${this.rootDoc[Id]}`, title: this.rootDoc.title, anchorId: `${this.rootDoc[Id]}` }], - location: 'add:right', + allAnchors: [{ href: `/doc/${this.Document[Id]}`, title: this.Document.title, anchorId: `${this.Document[Id]}` }], title: `from: ${DocCast(pdfAnchor.embedContainer).title}`, noPreview: true, docref: false, @@ -1441,7 +1341,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps ]), ]); - const link = DocUtils.MakeLink(pdfAnchor, this.rootDoc, { link_relationship: 'PDF pasted' }); + const link = DocUtils.MakeLink(pdfAnchor, this.Document, { link_relationship: 'PDF pasted' }); if (link) { view.dispatch(view.state.tr.replaceSelectionWith(dashField, false).scrollIntoView().setMeta('paste', true).setMeta('uiEvent', 'paste')); } @@ -1464,8 +1364,8 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps const self = this; return new Plugin({ view(newView) { - runInAction(() => self.props.isSelected(true) && RichTextMenu.Instance && (RichTextMenu.Instance.view = newView)); - return new RichTextMenuPlugin({ editorProps: this.props }); + runInAction(() => self._props.rootSelected?.() && RichTextMenu.Instance && (RichTextMenu.Instance.view = newView)); + return new RichTextMenuPlugin({ editorProps: this._props }); }, }); } @@ -1487,7 +1387,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps const botOff = docPos.bottom > viewRect.bottom ? docPos.bottom - viewRect.bottom : undefined; if (((topOff && Math.abs(Math.trunc(topOff)) > 0) || (botOff && Math.abs(Math.trunc(botOff)) > 0)) && scrollRef) { const shift = Math.min(topOff ?? Number.MAX_VALUE, botOff ?? Number.MAX_VALUE); - const scrollPos = scrollRef.scrollTop + shift * self.props.ScreenToLocalTransform().Scale; + const scrollPos = scrollRef.scrollTop + shift * self.ScreenToLocalBoxXf().Scale; if (this._focusSpeed !== undefined) { scrollPos && (this._scrollStopper = smoothScroll(this._focusSpeed, scrollRef, scrollPos, 'ease', this._scrollStopper)); } else { @@ -1539,11 +1439,11 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps (this._editorView as any).TextView = this; } - const selectOnLoad = this.rootDoc[Id] === FormattedTextBox.SelectOnLoad && (!LightboxView.LightboxDoc || LightboxView.IsLightboxDocView(this.props.docViewPath())); - if (this._editorView && selectOnLoad && !this.props.dontRegisterView && !this.props.dontSelectOnLoad && this.isActiveTab(this.ProseRef)) { + const selectOnLoad = Doc.AreProtosEqual(this._props.TemplateDataDocument ?? this.Document, FormattedTextBox.SelectOnLoad) && (!LightboxView.LightboxDoc || LightboxView.Contains(this.DocumentView?.())); + if (this._editorView && selectOnLoad && !this._props.dontRegisterView && !this._props.dontSelectOnLoad && this.isActiveTab(this.ProseRef)) { const selLoadChar = FormattedTextBox.SelectOnLoadChar; - FormattedTextBox.SelectOnLoad = ''; - this.props.select(false); + FormattedTextBox.SelectOnLoad = undefined; + 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) }); @@ -1559,7 +1459,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps } } selectOnLoad && this._editorView!.focus(); - if (this.props.isContentActive()) this.prepareForTyping(); + if (this._props.isContentActive()) this.prepareForTyping(); if (this._editorView) { const tr = this._editorView.state.tr; const { from, to } = tr.selection; @@ -1582,15 +1482,14 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps ...(Doc.UserDoc().fontColor !== 'transparent' && Doc.UserDoc().fontColor ? [schema.mark(schema.marks.pFontColor, { color: StrCast(Doc.UserDoc().fontColor) })] : []), ...(Doc.UserDoc().fontStyle === 'italics' ? [schema.mark(schema.marks.em)] : []), ...(Doc.UserDoc().textDecoration === 'underline' ? [schema.mark(schema.marks.underline)] : []), - ...(Doc.UserDoc().fontFamily ? [schema.mark(schema.marks.pFontFamily, { family: this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.FontFamily) })] : []), - ...(Doc.UserDoc().fontSize ? [schema.mark(schema.marks.pFontSize, { fontSize: this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.FontSize) })] : []), + ...(Doc.UserDoc().fontFamily ? [schema.mark(schema.marks.pFontFamily, { family: this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.FontFamily) })] : []), + ...(Doc.UserDoc().fontSize ? [schema.mark(schema.marks.pFontSize, { fontSize: this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.FontSize) })] : []), ...(Doc.UserDoc().fontWeight === 'bold' ? [schema.mark(schema.marks.strong)] : []), ...[schema.marks.user_mark.create({ userid: Doc.CurrentUserEmail, modified: Math.floor(Date.now() / 1000) })], ]; this._editorView?.dispatch(this._editorView?.state.tr.setStoredMarks(docDefaultMarks)); }; - @action componentWillUnmount() { if (this._recordingDictation) { this._recordingDictation = !this._recordingDictation; @@ -1601,7 +1500,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps FormattedTextBox.LiveTextUndo = undefined; this.unhighlightSearchTerms(); this._editorView?.destroy(); - RichTextMenu.Instance?.TextView === this && RichTextMenu.Instance.updateMenu(undefined, undefined, undefined); + RichTextMenu.Instance?.TextView === this && RichTextMenu.Instance.updateMenu(undefined, undefined, undefined, undefined); FormattedTextBoxComment.tooltip && (FormattedTextBoxComment.tooltip.style.display = 'none'); } @@ -1623,7 +1522,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps const func = () => { const docView = DocumentManager.Instance.getDocumentView(audiodoc); if (!docView) { - this.props.addDocTab(audiodoc, OpenWhere.addBottom); + this._props.addDocTab(audiodoc, OpenWhere.addBottom); setTimeout(func); } else docView.ComponentView?.playFrom?.(timecode, Cast(anchor.timecodeToHide, 'number', null)); // bcz: would be nice to find the next audio tag in the doc and play until that }; @@ -1636,8 +1535,10 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps } this._downX = e.clientX; this._downY = e.clientY; + this._downTime = Date.now(); + this._hadDownFocus = this.ProseRef?.children[0].className.includes('focused') ?? false; FormattedTextBoxComment.textBox = this; - if (e.button === 0 && (this.props.rootSelected(true) || this.props.isSelected(true)) && !e.altKey && !e.ctrlKey && !e.metaKey) { + if (e.button === 0 && this._props.rootSelected?.() && !e.altKey && !e.ctrlKey && !e.metaKey) { if (e.clientX < this.ProseRef!.getBoundingClientRect().right) { // stop propagation if not in sidebar, otherwise nested boxes will lose focus to outer boxes. e.stopPropagation(); // if the text box's content is active, then it consumes all down events @@ -1654,9 +1555,12 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps onPointerUp = (e: React.PointerEvent): void => { const editor = this._editorView!; const state = editor?.state; + if (!Utils.isClick(e.clientX, e.clientY, this._downX, this._downY, this._downTime) && !this._hadDownFocus) { + (this.ProseRef?.children[0] as HTMLElement)?.blur?.(); + } if (!state || !editor || !this.ProseRef?.children[0].className.includes('-focused')) return; if (!state.selection.empty && !(state.selection instanceof NodeSelection)) this.setupAnchorMenu(); - else if (this.props.isContentActive(true)) { + else if (this._props.isContentActive()) { const pcords = editor.posAtCoords({ left: e.clientX, top: e.clientY }); let xpos = pcords?.pos || 0; while (xpos > 0 && !state.doc.resolve(xpos).node()?.isTextblock) { @@ -1666,15 +1570,12 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps let target = e.target as any; // hrefs are stored on the dataset of the <a> node that wraps the hyerlink <span> while (target && !target.dataset?.targethrefs) target = target.parentElement; FormattedTextBoxComment.update(this, editor, undefined, target?.dataset?.targethrefs, target?.dataset.linkdoc, target?.dataset.nopreview === 'true'); - if (pcords && pcords.inside > 0 && state.doc.nodeAt(pcords.inside)?.type === state.schema.nodes.dashDoc) { - return; - } } }; @action onDoubleClick = (e: React.MouseEvent): void => { FormattedTextBoxComment.textBox = this; - if (e.button === 0 && this.props.isSelected(true) && !e.altKey && !e.ctrlKey && !e.metaKey) { + if (e.button === 0 && this._props.rootSelected?.() && !e.altKey && !e.ctrlKey && !e.metaKey) { if (e.clientX < this.ProseRef!.getBoundingClientRect().right) { // stop propagation if not in sidebar e.stopPropagation(); // if the text box is selected, then it consumes all click events @@ -1685,7 +1586,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps } FormattedTextBoxComment.Hide(); - if (e.buttons === 1 && this.props.isSelected(true) && !e.altKey) { + if (e.buttons === 1 && this._props.rootSelected?.() && !e.altKey) { e.stopPropagation(); } }; @@ -1697,12 +1598,12 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps @action onFocused = (e: React.FocusEvent): void => { //applyDevTools.applyDevTools(this._editorView); - this.ProseRef?.children[0] === e.nativeEvent.target && this._editorView && RichTextMenu.Instance?.updateMenu(this._editorView, undefined, this.props); + this.ProseRef?.children[0] === e.nativeEvent.target && this._editorView && RichTextMenu.Instance?.updateMenu(this._editorView, undefined, this._props, this.layoutDoc); e.stopPropagation(); }; onClick = (e: React.MouseEvent): void => { - if (!this.props.isContentActive()) return; + if (!this._props.isContentActive()) return; if ((e.nativeEvent as any).handledByInnerReactInstance) { e.stopPropagation(); return; @@ -1731,7 +1632,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps this._editorView!.dispatch(this._editorView!.state.tr.setSelection(NodeSelection.create(this._editorView!.state.doc, pcords.pos))); } } - if (this.props.isSelected(true)) { + 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); @@ -1745,7 +1646,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps clearStyleSheetRules(FormattedTextBox._bulletStyleSheet); const clickPos = this._editorView!.posAtCoords({ left: x, top: y }); let olistPos = clickPos?.pos; - if (clickPos && olistPos && this.props.isSelected(true)) { + if (clickPos && olistPos && this._props.rootSelected?.()) { const clickNode = this._editorView?.state.doc.nodeAt(olistPos); const nodeBef = this._editorView?.state.doc.nodeAt(Math.max(0, olistPos - 1)); olistPos = nodeBef?.type === this._editorView?.state.schema.nodes.ordered_list ? olistPos - 1 : olistPos; @@ -1773,7 +1674,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps } } startUndoTypingBatch() { - !this._undoTyping && (this._undoTyping = UndoManager.StartBatch('text edits on ' + this.rootDoc.title)); + !this._undoTyping && (this._undoTyping = UndoManager.StartBatch('text edits on ' + this.Document.title)); } public endUndoTypingBatch() { this._undoTyping?.end(); @@ -1794,8 +1695,8 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps tr && this._editorView.dispatch(tr); } } - if (RichTextMenu.Instance?.view === this._editorView && !this.props.isSelected(true)) { - RichTextMenu.Instance?.updateMenu(undefined, undefined, undefined); + if (RichTextMenu.Instance?.view === this._editorView && !this._props.rootSelected?.()) { + RichTextMenu.Instance?.updateMenu(undefined, undefined, undefined, undefined); } FormattedTextBox._hadSelection = window.getSelection()?.toString() !== ''; this.endUndoTypingBatch(); @@ -1805,29 +1706,12 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps const state = this._editorView!.state; const curText = state.doc.textBetween(0, state.doc.content.size, ' \n'); - if (this.layoutDoc[this.SidebarKey + '_type_collection'] === 'translation' && !this.fieldKey.includes('translation') && curText.endsWith(' ') && curText !== this._lastText) { - try { - translate(curText, { from: 'en', to: 'es' }).then((result1: any) => { - setTimeout( - () => - translate(result1.text, { from: 'es', to: 'en' }).then((result: any) => { - const tb = this._sidebarTagRef.current as FormattedTextBox; - tb._editorView?.dispatch(tb._editorView!.state.tr.insertText(result1.text + '\r\n\r\n' + result.text)); - }), - 1000 - ); - }); - } catch (e: any) { - console.log(e.message); - } - this._lastText = curText; - } - if (StrCast(this.rootDoc.title).startsWith('@') && !this.dataDoc.title_custom) { + if (StrCast(this.Document.title).startsWith('@') && !this.dataDoc.title_custom) { UndoManager.RunInBatch(() => { this.dataDoc.title_custom = true; this.dataDoc.layout_showTitle = 'title'; const tr = this._editorView!.state.tr; - this._editorView?.dispatch(tr.setSelection(new TextSelection(tr.doc.resolve(0), tr.doc.resolve(StrCast(this.rootDoc.title).length + 2))).deleteSelection()); + this._editorView?.dispatch(tr.setSelection(new TextSelection(tr.doc.resolve(0), tr.doc.resolve(StrCast(this.Document.title).length + 2))).deleteSelection()); }, 'titler'); } }; @@ -1836,7 +1720,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps if ((e.altKey || e.ctrlKey) && e.key === 't') { e.preventDefault(); e.stopPropagation(); - this.props.setTitleFocus?.(); + this._props.setTitleFocus?.(); return; } const state = this._editorView!.state; @@ -1853,7 +1737,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps let stopPropagation = true; for (var 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.rootDoc))) { + 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))) { e.preventDefault(); } } @@ -1862,7 +1746,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps this._editorView!.dispatch(state.tr.setSelection(TextSelection.create(state.doc, state.selection.from, state.selection.from))); (document.activeElement as any).blur?.(); SelectionManager.DeselectAll(); - RichTextMenu.Instance.updateMenu(undefined, undefined, undefined); + RichTextMenu.Instance.updateMenu(undefined, undefined, undefined, undefined); return; case 'Enter': this.insertTime(); @@ -1876,7 +1760,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps if (this._lastTimedMark?.attrs.userid === Doc.CurrentUserEmail) break; case ' ': if (e.code !== 'Space') { - [AclEdit, AclAugment, AclAdmin].includes(GetEffectiveAcl(this.rootDoc)) && + [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) }))); } break; @@ -1889,8 +1773,8 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps e.stopPropagation(); // drag n drop of text within text note will generate a new note if not caughst, as will dragging in from outside of Dash. }; onScroll = (e: React.UIEvent) => { - if (!LinkDocPreview.LinkInfo && this._scrollRef.current) { - if (!this.props.dontSelectOnLoad) { + if (!LinkInfo.Instance?.LinkInfo && this._scrollRef.current) { + if (!this._props.dontSelectOnLoad) { this._ignoreScroll = true; this.layoutDoc._layout_scrollTop = this._scrollRef.current.scrollTop; this._ignoreScroll = false; @@ -1900,21 +1784,21 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps } }; tryUpdateScrollHeight = () => { - const margins = 2 * NumCast(this.layoutDoc._yMargin, this.props.yPadding || 0); + const margins = 2 * NumCast(this.layoutDoc._yMargin, this._props.yPadding || 0); const children = this.ProseRef?.children.length ? Array.from(this.ProseRef.children[0].children) : undefined; - if (children && !SnappingManager.GetIsDragging()) { + if (children && !SnappingManager.IsDragging) { const toNum = (val: string) => Number(val.replace('px', '').replace('auto', '0')); const toHgt = (node: Element) => { const { height, marginTop, marginBottom } = getComputedStyle(node); return toNum(height) + Math.max(0, toNum(marginTop)) + Math.max(0, toNum(marginBottom)); }; const proseHeight = !this.ProseRef ? 0 : children.reduce((p, child) => p + toHgt(child), margins); - const scrollHeight = this.ProseRef && Math.min(NumCast(this.layoutDoc.layout_maxAutoHeight, proseHeight), proseHeight); - if (this.props.setHeight && scrollHeight && !this.props.dontRegisterView) { + const scrollHeight = this.ProseRef && proseHeight; + if (this._props.setHeight && scrollHeight && !this._props.dontRegisterView) { // if top === 0, then the text box is growing upward (as the overlay caption) which doesn't contribute to the height computation - const setScrollHeight = () => (this.rootDoc[this.fieldKey + '_scrollHeight'] = scrollHeight); + const setScrollHeight = () => (this.dataDoc[this.fieldKey + '_scrollHeight'] = scrollHeight); - if (this.rootDoc === this.layoutDoc || this.layoutDoc.resolvedDataDoc) { + if (this.Document === this.layoutDoc || this.layoutDoc.resolvedDataDoc) { setScrollHeight(); } else { setTimeout(setScrollHeight, 10); // if we have a template that hasn't been resolved yet, we can't set the height or we'd be setting it on the unresolved template. So set a timeout and hope its arrived... @@ -1922,21 +1806,21 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps } } }; - fitContentsToBox = () => BoolCast(this.props.Document._freeform_fitContentsToBox); - sidebarContentScaling = () => (this.props.NativeDimScaling?.() || 1) * NumCast(this.layoutDoc._freeform_scale, 1); + fitContentsToBox = () => BoolCast(this.Document._freeform_fitContentsToBox); + sidebarContentScaling = () => (this._props.NativeDimScaling?.() || 1) * NumCast(this.layoutDoc._freeform_scale, 1); sidebarAddDocument = (doc: Doc | Doc[], sidebarKey: string = this.SidebarKey) => { if (!this.layoutDoc._layout_showSidebar) this.toggleSidebar(); return this.addDocument(doc, sidebarKey); }; 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.rootDoc[this.SidebarKey + '_height'] = height); - sidebarWidth = () => (Number(this.layout_sidebarWidthPercent.substring(0, this.layout_sidebarWidthPercent.length - 1)) / 100) * this.props.PanelWidth(); + 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 + this._props .ScreenToLocalTransform() - .translate(-(this.props.PanelWidth() - this.sidebarWidth()) / (this.props.NativeDimScaling?.() || 1), 0) - .scale(1 / NumCast(this.layoutDoc._freeform_scale, 1) / (this.props.NativeDimScaling?.() || 1)); + .translate(-(this._props.PanelWidth() - this.sidebarWidth()) / (this._props.NativeDimScaling?.() || 1), 0) + .scale(1 / NumCast(this.layoutDoc._freeform_scale, 1) / (this._props.NativeDimScaling?.() || 1)); @computed get audioHandle() { return !this._recordingDictation ? null : ( @@ -1959,9 +1843,9 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps TraceMobx(); const annotated = DocListCast(this.dataDoc[this.SidebarKey]).filter(d => d?.author).length; const color = !annotated ? Colors.WHITE : Colors.BLACK; - const backgroundColor = !annotated ? (this.sidebarWidth() ? Colors.MEDIUM_BLUE : Colors.BLACK) : this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.WidgetColor + (annotated ? ':annotated' : '')); + const backgroundColor = !annotated ? (this.sidebarWidth() ? Colors.MEDIUM_BLUE : Colors.BLACK) : this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.WidgetColor + (annotated ? ':annotated' : '')); - return !annotated && (!this.props.isContentActive() || SnappingManager.GetIsDragging()) ? null : ( + return !annotated && (!this._props.isContentActive() || SnappingManager.IsDragging || Doc.ActiveTool !== InkTool.None) ? null : ( <div className="formattedTextBox-sidebar-handle" onPointerDown={this.sidebarDown} @@ -1976,12 +1860,12 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps } @computed get sidebarCollection() { const renderComponent = (tag: string) => { - const ComponentTag = tag === CollectionViewType.Freeform ? CollectionFreeFormView : tag === CollectionViewType.Tree ? CollectionTreeView : tag === 'translation' ? FormattedTextBox : CollectionStackingView; + const ComponentTag: any = tag === CollectionViewType.Freeform ? CollectionFreeFormView : tag === CollectionViewType.Tree ? CollectionTreeView : tag === 'translation' ? FormattedTextBox : CollectionStackingView; return ComponentTag === CollectionStackingView ? ( <SidebarAnnos ref={this._sidebarRef} - {...this.props} - rootDoc={this.rootDoc} + {...this._props} + Document={this.Document} layoutDoc={this.layoutDoc} dataDoc={this.dataDoc} usePanelWidth={true} @@ -1997,14 +1881,14 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps setHeight={this.setSidebarHeight} /> ) : ( - <div onPointerDown={e => setupMoveUpEvents(this, e, returnFalse, emptyFunction, () => SelectionManager.SelectView(this.props.DocumentView?.()!, false), true)}> + <div onPointerDown={e => setupMoveUpEvents(this, e, returnFalse, emptyFunction, () => SelectionManager.SelectView(this.DocumentView?.()!, false), true)}> <ComponentTag - {...this.props} + {...this._props} ref={this._sidebarTagRef as any} setContentView={emptyFunction} NativeWidth={returnZero} NativeHeight={returnZero} - PanelHeight={this.props.PanelHeight} + PanelHeight={this._props.PanelHeight} PanelWidth={this.sidebarWidth} xPadding={0} yPadding={0} @@ -2018,7 +1902,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps moveDocument={this.sidebarMoveDocument} addDocument={this.sidebarAddDocument} ScreenToLocalTransform={this.sidebarScreenToLocal} - renderDepth={this.props.renderDepth + 1} + renderDepth={this._props.renderDepth + 1} setHeight={this.setSidebarHeight} fitContentsToBox={this.fitContentsToBox} noSidebar={true} @@ -2036,12 +1920,12 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps } cycleAlternateText = () => { if (this.layoutDoc._layout_enableAltContentUI) { - const usePath = this.rootDoc[`_${this.props.fieldKey}_usePath`]; - this.rootDoc[`_${this.props.fieldKey}_usePath`] = usePath === undefined ? 'alternate' : usePath === 'alternate' ? 'alternate:hover' : undefined; + const usePath = this.layoutDoc[`_${this._props.fieldKey}_usePath`]; + this.layoutDoc[`_${this._props.fieldKey}_usePath`] = usePath === undefined ? 'alternate' : usePath === 'alternate' ? 'alternate:hover' : undefined; } }; @computed get overlayAlternateIcon() { - const usePath = this.rootDoc[`_${this.props.fieldKey}_usePath`]; + const usePath = this.layoutDoc[`_${this._props.fieldKey}_usePath`]; return ( <Tooltip title={ @@ -2063,7 +1947,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps className="formattedTextBox-alternateButton" onPointerDown={e => setupMoveUpEvents(e.target, e, returnFalse, emptyFunction, e => this.cycleAlternateText())} style={{ - display: this.props.isContentActive() && !SnappingManager.GetIsDragging() ? 'flex' : 'none', + display: this._props.isContentActive() && !SnappingManager.IsDragging ? 'flex' : 'none', background: usePath === undefined ? 'white' : usePath === 'alternate' ? 'black' : 'gray', color: usePath === undefined ? 'black' : 'white', }}> @@ -2072,14 +1956,14 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps </Tooltip> ); } - @computed get fieldKey() { - const usePath = StrCast(this.rootDoc[`${this.props.fieldKey}_usePath`]); - return this.props.fieldKey + (usePath && (!usePath.includes(':hover') || this._isHovering) ? `_${usePath.replace(':hover', '')}` : ''); + get fieldKey() { + const usePath = StrCast(this.layoutDoc[`${this._props.fieldKey}_usePath`]); + return this._props.fieldKey + (usePath && (!usePath.includes(':hover') || this._isHovering || this._props.isContentActive()) ? `_${usePath.replace(':hover', '')}` : ''); } @observable _isHovering = false; onPassiveWheel = (e: WheelEvent) => { if (e.clientX > this.ProseRef!.getBoundingClientRect().right) { - if (this.rootDoc[this.SidebarKey + '_type_collection'] === CollectionViewType.Freeform) { + 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(); @@ -2088,11 +1972,14 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps } // if scrollTop is 0, then don't let wheel trigger scroll on any container (which it would since onScroll won't be triggered on this) - if (this.props.isContentActive()) { + if (this._props.isContentActive()) { + const scale = this._props.NativeDimScaling?.() || 1; + const styleFromLayoutString = Doc.styleFromLayoutString(this.Document, this._props, scale); // this converts any expressions in the format string to style props. e.g., <FormattedTextBox height='{this._headerHeight}px' > + const height = Number(styleFromLayoutString.height?.replace('px', '')); // prevent default if selected || child is active but this doc isn't scrollable if ( - (this._scrollRef.current?.scrollHeight ?? 0) <= Math.ceil(this.props.PanelHeight()) && // - (this.props.isSelected() || this.isAnyChildContentActive()) + (this._scrollRef.current?.scrollHeight ?? 0) <= Math.ceil((height ? height : this._props.PanelHeight()) / scale) && // + (this._props.rootSelected?.() || this.isAnyChildContentActive()) ) { e.preventDefault(); } @@ -2101,25 +1988,25 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps }; _oldWheel: any; @computed get fontColor() { - return this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.Color); + return this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.Color); } @computed get fontSize() { - return this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.FontSize); + return this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.FontSize); } @computed get fontFamily() { - return this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.FontFamily); + return this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.FontFamily); } @computed get fontWeight() { - return this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.FontWeight); + return this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.FontWeight); } render() { TraceMobx(); - const scale = (this.props.NativeDimScaling?.() || 1) * NumCast(this.layoutDoc._freeform_scale, 1); + const scale = (this._props.NativeDimScaling?.() || 1) * NumCast(this.layoutDoc._freeform_scale, 1); const rounded = StrCast(this.layoutDoc.layout_borderRounding) === '100%' ? '-rounded' : ''; - setTimeout(() => !this.props.isContentActive() && FormattedTextBoxComment.textBox === this && FormattedTextBoxComment.Hide); - const paddingX = NumCast(this.layoutDoc._xMargin, this.props.xPadding || 0); - const paddingY = NumCast(this.layoutDoc._yMargin, this.props.yPadding || 0); - const styleFromLayoutString = Doc.styleFromLayoutString(this.rootDoc, this.layoutDoc, this.props, scale); // this converts any expressions in the format string to style props. e.g., <FormattedTextBox height='{this._headerHeight}px' > + setTimeout(() => !this._props.isContentActive() && FormattedTextBoxComment.textBox === this && FormattedTextBoxComment.Hide); + const paddingX = NumCast(this.layoutDoc._xMargin, this._props.xPadding || 0); + const paddingY = NumCast(this.layoutDoc._yMargin, this._props.yPadding || 0); + const styleFromLayoutString = Doc.styleFromLayoutString(this.Document, this._props, scale); // this converts any expressions in the format string to style props. e.g., <FormattedTextBox height='{this._headerHeight}px' > return styleFromLayoutString?.height === '0px' ? null : ( <div className="formattedTextBox" @@ -2131,7 +2018,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps r?.addEventListener('wheel', this.onPassiveWheel, { passive: false }); }} style={{ - ...(this.props.dontScale + ...(this._props.dontScale ? {} : { transform: `scale(${scale})`, @@ -2150,9 +2037,10 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps className="formattedTextBox-cont" ref={this._ref} style={{ - cursor: this.props.isContentActive() ? 'text' : undefined, - height: this.props.height || (this.layout_autoHeight && this.props.renderDepth && !this.props.suppressSetHeight ? 'max-content' : undefined), - pointerEvents: Doc.ActiveTool === InkTool.None && !this.props.onBrowseClick?.() ? undefined : 'none', + cursor: this._props.isContentActive() ? 'text' : undefined, + height: this._props.height ? 'max-content' : undefined, + overflow: this.layout_autoHeight ? 'hidden' : undefined, + pointerEvents: Doc.ActiveTool === InkTool.None && !this._props.onBrowseClickScript?.() ? undefined : 'none', }} onContextMenu={this.specificContextMenu} onKeyDown={this.onKeyDown} @@ -2167,7 +2055,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps className="formattedTextBox-outer" ref={this._scrollRef} style={{ - width: this.props.dontSelectOnLoad || this.noSidebar ? '100%' : `calc(100% - ${this.layout_sidebarWidthPercent})`, + width: this._props.dontSelectOnLoad || this.noSidebar ? '100%' : `calc(100% - ${this.layout_sidebarWidthPercent})`, overflow: this.layoutDoc._createDocOnCR ? 'hidden' : this.layoutDoc._layout_autoHeight ? 'visible' : undefined, }} onScroll={this.onScroll} @@ -2184,8 +2072,8 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps }} /> </div> - {this.noSidebar || this.props.dontSelectOnLoad || !this.SidebarShown || this.layout_sidebarWidthPercent === '0%' ? null : this.sidebarCollection} - {this.noSidebar || this.Document._layout_noSidebar || this.props.dontSelectOnLoad || this.Document._createDocOnCR || this.layoutDoc._chromeHidden ? null : this.sidebarHandle} + {this.noSidebar || this._props.dontSelectOnLoad || !this.SidebarShown || this.layout_sidebarWidthPercent === '0%' ? null : this.sidebarCollection} + {this.noSidebar || this.Document._layout_noSidebar || this._props.dontSelectOnLoad || this.Document._createDocOnCR || this.layoutDoc._chromeHidden ? null : this.sidebarHandle} {this.audioHandle} {this.layoutDoc._layout_enableAltContentUI && !this.layoutDoc._chromeHidden ? this.overlayAlternateIcon : null} </div> diff --git a/src/client/views/nodes/formattedText/FormattedTextBoxComment.tsx b/src/client/views/nodes/formattedText/FormattedTextBoxComment.tsx index e7ca26d5c..ce17af6ca 100644 --- a/src/client/views/nodes/formattedText/FormattedTextBoxComment.tsx +++ b/src/client/views/nodes/formattedText/FormattedTextBoxComment.tsx @@ -3,7 +3,7 @@ import { EditorState, NodeSelection } from 'prosemirror-state'; import { EditorView } from 'prosemirror-view'; import { Doc } from '../../../../fields/Doc'; import { DocServer } from '../../../DocServer'; -import { LinkDocPreview } from '../LinkDocPreview'; +import { LinkDocPreview, LinkInfo } from '../LinkDocPreview'; import { FormattedTextBox } from './FormattedTextBox'; import './FormattedTextBoxComment.scss'; import { schema } from './schema_rts'; @@ -133,9 +133,10 @@ export class FormattedTextBoxComment { const naft = findEndOfMark(state.selection.$from, view, findLinkMark) || nbef; //nbef && naft && - LinkDocPreview.SetLinkInfo({ - docProps: textBox.props, - linkSrc: textBox.rootDoc, + LinkInfo.SetLinkInfo({ + DocumentView: textBox.DocumentView, + styleProvider: textBox._props.styleProvider, + linkSrc: textBox.Document, linkDoc: linkDoc ? (DocServer.GetCachedRefField(linkDoc) as Doc) : undefined, location: (pos => [pos.left, pos.top + 25])(view.coordsAtPos(state.selection.from - Math.max(0, nbef - 1))), hrefs, diff --git a/src/client/views/nodes/formattedText/ProsemirrorExampleTransfer.ts b/src/client/views/nodes/formattedText/ProsemirrorExampleTransfer.ts index ec11079b4..08bad2d57 100644 --- a/src/client/views/nodes/formattedText/ProsemirrorExampleTransfer.ts +++ b/src/client/views/nodes/formattedText/ProsemirrorExampleTransfer.ts @@ -47,7 +47,7 @@ export function buildKeymap<S extends Schema<any>>(schema: S, props: any, mapKey } const canEdit = (state: any) => { - switch (GetEffectiveAcl(props.DataDoc)) { + switch (GetEffectiveAcl(props.TemplateDataDocument)) { case AclAugment: const prevNode = state.selection.$cursor.nodeBefore; const prevUser = !prevNode ? Doc.CurrentUserEmail : prevNode.marks[prevNode.marks.length - 1].attrs.userid; @@ -334,7 +334,7 @@ export function buildKeymap<S extends Schema<any>>(schema: S, props: any, mapKey //Command to create a blank space bind('Space', (state: EditorState, dispatch: (tx: Transaction) => void) => { - if (props.DataDoc && GetEffectiveAcl(props.DataDoc) != AclEdit && GetEffectiveAcl(props.DataDoc) != AclAugment && GetEffectiveAcl(props.DataDoc) != AclAdmin) return true; + if (props.TemplateDataDocument && GetEffectiveAcl(props.TemplateDataDocument) != AclEdit && GetEffectiveAcl(props.TemplateDataDocument) != AclAugment && GetEffectiveAcl(props.TemplateDataDocument) != AclAdmin) return true; const marks = state.storedMarks || (state.selection.$to.parentOffset && state.selection.$from.marks()); dispatch(splitMetadata(marks, state.tr)); return false; diff --git a/src/client/views/nodes/formattedText/RichTextMenu.scss b/src/client/views/nodes/formattedText/RichTextMenu.scss index 8afa0f6b5..d6ed5ebee 100644 --- a/src/client/views/nodes/formattedText/RichTextMenu.scss +++ b/src/client/views/nodes/formattedText/RichTextMenu.scss @@ -1,4 +1,4 @@ -@import "../../global/globalCssVariables"; +@import '../../global/globalCssVariables.module.scss'; .button-dropdown-wrapper { position: relative; @@ -55,7 +55,6 @@ input { color: black; } - } .richTextMenu { @@ -68,12 +67,12 @@ font-size: 12px; height: 100%; margin-right: 3px; - + &:focus, &:hover { background-color: black; } - + &::-ms-expand { color: white; } @@ -126,7 +125,7 @@ display: flex; justify-content: space-between; - >div { + > div { display: flex; } -}
\ No newline at end of file +} diff --git a/src/client/views/nodes/formattedText/RichTextMenu.tsx b/src/client/views/nodes/formattedText/RichTextMenu.tsx index e3ac4fb9d..5858c3b11 100644 --- a/src/client/views/nodes/formattedText/RichTextMenu.tsx +++ b/src/client/views/nodes/formattedText/RichTextMenu.tsx @@ -1,27 +1,28 @@ -import React = require('react'); import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { Tooltip } from '@material-ui/core'; -import { action, computed, IReactionDisposer, observable, reaction, runInAction } from 'mobx'; +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 { 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 { Cast, StrCast } from '../../../../fields/Types'; +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 { EquationBox } from '../EquationBox'; import { FieldViewProps } from '../FieldView'; import { FormattedTextBox } from './FormattedTextBox'; import { updateBullets } from './ProsemirrorExampleTransfer'; import './RichTextMenu.scss'; import { schema } from './schema_rts'; -import { EquationBox } from '../EquationBox'; -import { numberRange } from '../../../../Utils'; const { toggleMark } = require('prosemirror-commands'); @observer @@ -30,7 +31,8 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> { public overMenu: boolean = false; // kind of hacky way to prevent selects not being selectable private _linkToRef = React.createRef<HTMLInputElement>(); - @observable public view?: EditorView; + layoutDoc: Doc | undefined; + @observable public view?: EditorView = undefined; public editorProps: FieldViewProps | undefined; public _brushMap: Map<string, Set<Mark>> = new Map(); @@ -62,14 +64,13 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> { @observable private showLinkDropdown: boolean = false; _reaction: IReactionDisposer | undefined; - constructor(props: Readonly<{}>) { + constructor(props: AntimodeMenuProps) { super(props); - runInAction(() => { - RichTextMenu.Instance = this; - this.updateMenu(undefined, undefined, props); - this._canFade = false; - this.Pinned = true; - }); + makeObservable(this); + RichTextMenu.Instance = this; + this.updateMenu(undefined, undefined, props, this.layoutDoc); + this._canFade = false; + this.Pinned = true; } @computed get noAutoLink() { @@ -102,11 +103,14 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> { @computed get textAlign() { return this._activeAlignment; } + @computed get textVcenter() { + return BoolCast(this.layoutDoc?.layout_centered); + } _disposer: IReactionDisposer | undefined; componentDidMount() { this._disposer = reaction( - () => SelectionManager.Views().slice(), - views => this.updateMenu(undefined, undefined, undefined) + () => SelectionManager.Views.slice(), + views => this.updateMenu(undefined, undefined, undefined, undefined) ); } componentWillUnmount() { @@ -114,11 +118,12 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> { } @action - public updateMenu(view: EditorView | undefined, lastState: EditorState | undefined, props: any) { + public updateMenu(view: EditorView | undefined, lastState: EditorState | undefined, props: any, layoutDoc: Doc | undefined) { if (this._linkToRef.current?.getBoundingClientRect().width) { return; } this.view = view; + this.layoutDoc = layoutDoc; props && (this.editorProps = props); // Don't do anything if the document/selection didn't change @@ -181,7 +186,7 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> { // finds font sizes and families in selection getActiveAlignment() { - if (this.view && this.TextView?.props.isSelected(true)) { + if (this.view && this.TextView?._props.rootSelected?.()) { const path = (this.view.state.selection.$from as any).path; 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) { @@ -194,7 +199,7 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> { // finds font sizes and families in selection getActiveListStyle() { - if (this.view && this.TextView?.props.isSelected(true)) { + if (this.view && this.TextView?._props.rootSelected?.()) { const path = (this.view.state.selection.$from as any).path; for (let i = 0; i < path.length; i += 3) { if (path[i].type === this.view.state.schema.nodes.ordered_list) { @@ -214,7 +219,7 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> { const activeSizes = new Set<string>(); const activeColors = new Set<string>(); const activeHighlights = new Set<string>(); - if (this.view && this.TextView?.props.isSelected(true)) { + if (this.view && this.TextView?._props.rootSelected?.()) { const state = this.view.state; const pos = this.view.state.selection.$from; const marks: Mark[] = [...(state.storedMarks ?? [])]; @@ -233,8 +238,8 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> { m.type === state.schema.marks.pFontSize && activeSizes.add(m.attrs.fontSize); m.type === state.schema.marks.marker && activeHighlights.add(String(m.attrs.highlight)); }); - } else if (SelectionManager.Views().some(dv => dv.ComponentView instanceof EquationBox)) { - SelectionManager.Views().forEach(dv => StrCast(dv.rootDoc._text_fontSize) && activeSizes.add(StrCast(dv.rootDoc._text_fontSize))); + } 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))); } return { activeFamilies: Array.from(activeFamilies), activeSizes: Array.from(activeSizes), activeColors: Array.from(activeColors), activeHighlights: Array.from(activeHighlights) }; } @@ -249,7 +254,7 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> { //finds all active marks on selection in given group getActiveMarksOnSelection() { let activeMarks: MarkType[] = []; - if (!this.view || !this.TextView?.props.isSelected(true)) return activeMarks; + if (!this.view || !this.TextView?._props.rootSelected?.()) return activeMarks; const markGroup = [schema.marks.noAutoLinkAnchor, schema.marks.strong, schema.marks.em, schema.marks.underline, schema.marks.strikethrough, schema.marks.superscript, schema.marks.subscript]; if (this.view.state.storedMarks) return this.view.state.storedMarks.map(mark => mark.type); @@ -285,10 +290,6 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> { return activeMarks; } - destroy() { - !this.TextView?.props.isSelected(true) && this.fadeOut(true); - } - @action setActiveMarkButtons(activeMarks: MarkType[] | undefined) { if (!activeMarks) return; @@ -357,10 +358,10 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> { this.setMark(fmark, this.view.state, (tx: any) => this.view!.dispatch(tx.addStoredMark(fmark)), true); this.view.focus(); } - } else if (SelectionManager.Views().some(dv => dv.ComponentView instanceof EquationBox)) { - SelectionManager.Views().forEach(dv => (dv.rootDoc._text_fontSize = fontSize)); + } else if (SelectionManager.Views.some(dv => dv.ComponentView instanceof EquationBox)) { + SelectionManager.Views.forEach(dv => (dv.Document._text_fontSize = fontSize)); } else Doc.UserDoc().fontSize = fontSize; - this.updateMenu(this.view, undefined, this.props); + this.updateMenu(this.view, undefined, this.props, this.layoutDoc); }; setFontFamily = (family: string) => { @@ -369,7 +370,7 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> { this.setMark(fmark, this.view.state, (tx: any) => this.view!.dispatch(tx.addStoredMark(fmark)), true); this.view.focus(); } else Doc.UserDoc().fontFamily = family; - this.updateMenu(this.view, undefined, this.props); + this.updateMenu(this.view, undefined, this.props, this.layoutDoc); }; setHighlight(color: string) { @@ -378,7 +379,7 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> { this.setMark(highlightMark, this.view.state, (tx: any) => this.view!.dispatch(tx.addStoredMark(highlightMark)), true); this.view.focus(); } else Doc.UserDoc()._fontHighlight = color; - this.updateMenu(this.view, undefined, this.props); + this.updateMenu(this.view, undefined, this.props, this.layoutDoc); } setColor(color: string) { @@ -387,7 +388,7 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> { this.setMark(colorMark, this.view.state, (tx: any) => this.view!.dispatch(tx.addStoredMark(colorMark)), true); this.view.focus(); } else Doc.UserDoc().fontColor = color; - this.updateMenu(this.view, undefined, this.props); + this.updateMenu(this.view, undefined, this.props, this.layoutDoc); } // TODO: remove doesn't work @@ -428,7 +429,7 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> { } } this.view.focus(); - this.updateMenu(this.view, undefined, this.props); + this.updateMenu(this.view, undefined, this.props, this.layoutDoc); }; insertSummarizer(state: EditorState, dispatch: any) { @@ -442,8 +443,11 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> { return true; } + vcenterToggle = (view: EditorView, dispatch: any) => { + this.layoutDoc && (this.layoutDoc.layout_centered = !this.layoutDoc.layout_centered); + }; align = (view: EditorView, dispatch: any, alignment: 'left' | 'right' | 'center') => { - if (this.TextView?.props.isSelected(true)) { + 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) => { if ([schema.nodes.paragraph, schema.nodes.heading].includes(node.type)) { @@ -577,7 +581,7 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> { return (this.view as any)?.TextView as FormattedTextBox; } get TextViewFieldKey() { - return this.TextView?.props.fieldKey; + return this.TextView?._props.fieldKey; } @action setActiveHighlight(color: string) { @@ -638,7 +642,7 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> { 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(); + 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); @@ -665,7 +669,6 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> { }; @undoBatch - @action deleteLink = () => { if (this.view) { const linkAnchor = this.view.state.selection.$from.nodeAfter?.marks.find(m => m.type === this.view!.state.schema.marks.linkAnchor); @@ -771,11 +774,11 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> { // <div className="collectionMenu-divider" key="divider 3" /> // {[this.createMarksDropdown(this.activeFontSize, this.fontSizeOptions, "font size", action((val: string) => { // this.activeFontSize = val; - // SelectionManager.Views().map(dv => dv.props.Document._text_fontSize = val); + // SelectionManager.Views.map(dv => dv.Document._text_fontSize = val); // })), // this.createMarksDropdown(this.activeFontFamily, this.fontFamilyOptions, "font family", action((val: string) => { // this.activeFontFamily = val; - // SelectionManager.Views().map(dv => dv.props.Document._text_fontFamily = val); + // SelectionManager.Views.map(dv => dv.Document._text_fontFamily = val); // })), // <div className="collectionMenu-divider" key="divider 4" />, // this.createNodesDropdown(this.activeListType, this.listTypeOptions, "list type", () => ({})), @@ -808,10 +811,15 @@ interface ButtonDropdownProps { } @observer -export class ButtonDropdown extends React.Component<ButtonDropdownProps> { +export class ButtonDropdown extends ObservableReactComponent<ButtonDropdownProps> { @observable private showDropdown: boolean = false; private ref: HTMLDivElement | null = null; + constructor(props: any) { + super(props); + makeObservable(this); + } + componentDidMount() { document.addEventListener('pointerdown', this.onBlur); } @@ -846,22 +854,22 @@ export class ButtonDropdown extends React.Component<ButtonDropdownProps> { render() { return ( <div className="button-dropdown-wrapper" ref={node => (this.ref = node)}> - {!this.props.pdf ? ( - <div className="antimodeMenu-button dropdown-button-combined" onPointerDown={this.props.openDropdownOnButton ? this.onDropdownClick : undefined}> - {this.props.button} - <div style={{ marginTop: '-8.5', position: 'relative' }} onPointerDown={!this.props.openDropdownOnButton ? this.onDropdownClick : undefined}> + {!this._props.pdf ? ( + <div className="antimodeMenu-button dropdown-button-combined" onPointerDown={this._props.openDropdownOnButton ? this.onDropdownClick : undefined}> + {this._props.button} + <div style={{ marginTop: '-8.5', position: 'relative' }} onPointerDown={!this._props.openDropdownOnButton ? this.onDropdownClick : undefined}> <FontAwesomeIcon icon="caret-down" size="sm" /> </div> </div> ) : ( <> - {this.props.button} + {this._props.button} <button className="dropdown-button antimodeMenu-button" key="antimodebutton" onPointerDown={this.onDropdownClick}> <FontAwesomeIcon icon="caret-down" size="sm" /> </button> </> )} - {this.showDropdown ? this.props.dropdownContent : null} + {this.showDropdown ? this._props.dropdownContent : null} </div> ); } @@ -875,6 +883,6 @@ export class RichTextMenuPlugin extends React.Component<RichTextMenuPluginProps> return null; } update(view: EditorView, lastState: EditorState | undefined) { - RichTextMenu.Instance?.updateMenu(view, lastState, this.props.editorProps); + RichTextMenu.Instance?.updateMenu(view, lastState, this.props.editorProps, (view as any).TextView?.layoutDoc); } } diff --git a/src/client/views/nodes/formattedText/RichTextRules.ts b/src/client/views/nodes/formattedText/RichTextRules.ts index 3e2afd2ce..456ed4732 100644 --- a/src/client/views/nodes/formattedText/RichTextRules.ts +++ b/src/client/views/nodes/formattedText/RichTextRules.ts @@ -95,7 +95,6 @@ export class RichTextRules { textDocInline.title_custom = true; // And make sure that it's 'custom' so that editing text doesn't change the title of the containing doc textDocInline.isTemplateForField = inlineFieldKey; // this is needed in case the containing text doc is converted to a template at some point textDocInline.proto = textDoc; // make the annotation inherit from the outer text doc so that it can resolve any nested field references, e.g., [[field]] - textDocInline._textContext = ComputedField.MakeFunction(`copyField(self.${inlineFieldKey})`); textDoc[inlineLayoutKey] = FormattedTextBox.LayoutString(inlineFieldKey); // create a layout string for the layout key that will render the annotation text textDoc[inlineFieldKey] = ''; // set a default value for the annotation const node = (state.doc.resolve(start) as any).nodeAfter; @@ -265,9 +264,16 @@ export class RichTextRules { this.TextBox.EditorView?.dispatch(fstate.tr.setSelection(new TextSelection(fstate.doc.resolve(selection)))); } }; + const getTitledDoc = (docTitle: string) => { + if (!DocServer.FindDocByTitle(docTitle)) { + Doc.AddToMyPublished(Docs.Create.TextDocument('', { title: docTitle, _width: 400, _layout_autoHeight: true })); + } + const titledDoc = DocServer.FindDocByTitle(docTitle); + return titledDoc ? Doc.BestEmbedding(titledDoc) : titledDoc; + }; if (!fieldKey) { if (docTitle) { - const target = DocServer.FindDocByTitle(docTitle); + const target = getTitledDoc(docTitle); if (target) { setTimeout(() => linkToDoc(target)); return state.tr.deleteRange(end - 1, end).deleteRange(start, start + 3); @@ -279,7 +285,7 @@ export class RichTextRules { const num = value.match(/^[0-9.]$/); this.Document[DocData][fieldKey] = value === 'true' ? true : value === 'false' ? false : num ? Number(value) : value; } - const target = DocServer.FindDocByTitle(docTitle); + const target = getTitledDoc(docTitle); const fieldView = state.schema.nodes.dashField.create({ fieldKey, docId: target?.[Id], hideKey: false }); return state.tr.setSelection(new TextSelection(state.doc.resolve(start), state.doc.resolve(end))).replaceSelectionWith(fieldView, true); }), diff --git a/src/client/views/nodes/formattedText/SummaryView.tsx b/src/client/views/nodes/formattedText/SummaryView.tsx index 3355e4529..7ec296ed2 100644 --- a/src/client/views/nodes/formattedText/SummaryView.tsx +++ b/src/client/views/nodes/formattedText/SummaryView.tsx @@ -1,7 +1,7 @@ import { TextSelection } from 'prosemirror-state'; import { Fragment, Node, Slice } from 'prosemirror-model'; import * as ReactDOM from 'react-dom/client'; -import React = require('react'); +import * as React from 'react'; // 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 diff --git a/src/client/views/nodes/formattedText/TooltipTextMenu.scss b/src/client/views/nodes/formattedText/TooltipTextMenu.scss index 8c4d77da9..87320943d 100644 --- a/src/client/views/nodes/formattedText/TooltipTextMenu.scss +++ b/src/client/views/nodes/formattedText/TooltipTextMenu.scss @@ -1,9 +1,9 @@ -@import "../views/global/globalCssVariables"; +@import '../views/global/globalCssVariables.module.scss'; .ProseMirror-menu-dropdown-wrap { display: inline-block; position: relative; } - + .ProseMirror-menu-dropdown { vertical-align: 1px; cursor: pointer; @@ -17,11 +17,11 @@ margin-right: 4px; &:after { - content: ""; + content: ''; border-left: 4px solid transparent; border-right: 4px solid transparent; border-top: 4px solid currentColor; - opacity: .6; + opacity: 0.6; position: absolute; right: 4px; top: calc(50% - 2px); @@ -33,7 +33,7 @@ margin-right: -4px; } -.ProseMirror-menu-dropdown-menu, +.ProseMirror-menu-dropdown-menu, .ProseMirror-menu-submenu { font-size: 12px; background: white; @@ -55,19 +55,18 @@ } } - .ProseMirror-menu-submenu-label:after { - content: ""; + content: ''; border-top: 4px solid transparent; border-bottom: 4px solid transparent; border-left: 4px solid currentColor; - opacity: .6; + opacity: 0.6; position: absolute; right: 4px; top: calc(50% - 4px); } - - .ProseMirror-icon { + +.ProseMirror-icon { display: inline-block; // line-height: .8; // vertical-align: -2px; /* Compensate for padding */ @@ -79,14 +78,14 @@ } svg { - fill:white; + fill: white; height: 1em; } span { vertical-align: text-top; - } - } + } +} .wrapper { position: absolute; @@ -99,10 +98,10 @@ background: #323232; border-radius: 6px; box-shadow: 3px 3px 3px rgba(0, 0, 0, 0.25); - } -.tooltipMenu, .basic-tools { +.tooltipMenu, +.basic-tools { z-index: 3000; pointer-events: all; padding: 3px; @@ -111,17 +110,17 @@ align-items: center; .ProseMirror-example-setup-style hr { - padding: 2px 10px; - border: none; - margin: 1em 0; + padding: 2px 10px; + border: none; + margin: 1em 0; } - + .ProseMirror-example-setup-style hr:after { - content: ""; - display: block; - height: 1px; - background-color: silver; - line-height: 2px; + content: ''; + display: block; + height: 1px; + background-color: silver; + line-height: 2px; } } @@ -142,7 +141,7 @@ } } - &> * { + & > * { margin-top: 50%; margin-left: 50%; transform: translate(-50%, -50%); @@ -168,7 +167,7 @@ background-color: black; } - &> * { + & > * { margin-top: 50%; margin-left: 50%; transform: translate(-50%, -50%); @@ -208,18 +207,17 @@ margin-top: 13px; } - .font-size-indicator { - font-size: 12px; - padding-right: 0px; - } - .summarize{ - color: white; - height: 20px; - text-align: center; - } - - -.brush{ +.font-size-indicator { + font-size: 12px; + padding-right: 0px; +} +.summarize { + color: white; + height: 20px; + text-align: center; +} + +.brush { display: inline-block; width: 1em; height: 1em; @@ -229,7 +227,7 @@ margin-right: 15px; } -.brush-active{ +.brush-active { display: inline-block; width: 1em; height: 1em; @@ -269,7 +267,6 @@ } .buttonSettings-dropdown { - &.ProseMirror-menu-dropdown { width: 10px; height: 25px; @@ -301,7 +298,7 @@ padding: 3px; box-shadow: 3px 3px 3px rgba(0, 0, 0, 0.25); - .ProseMirror-menu-dropdown-item{ + .ProseMirror-menu-dropdown-item { cursor: default; &:last-child { @@ -312,7 +309,8 @@ background-color: #323232; } - .button-setting, .button-setting-disabled { + .button-setting, + .button-setting-disabled { padding: 2px; border-radius: 2px; } @@ -328,25 +326,23 @@ } input { - color: black; - border: none; - border-radius: 1px; - padding: 3px; + color: black; + border: none; + border-radius: 1px; + padding: 3px; } button { - padding: 6px; - background-color: #323232; - border: 1px solid black; - border-radius: 1px; + padding: 6px; + background-color: #323232; + border: 1px solid black; + border-radius: 1px; &:hover { - background-color: black; + background-color: black; } } } - - } } diff --git a/src/client/views/nodes/formattedText/marks_rts.ts b/src/client/views/nodes/formattedText/marks_rts.ts index 6f07588b3..a342285b0 100644 --- a/src/client/views/nodes/formattedText/marks_rts.ts +++ b/src/client/views/nodes/formattedText/marks_rts.ts @@ -1,4 +1,4 @@ -import React = require('react'); +import * as React from 'react'; import { DOMOutputSpec, Fragment, MarkSpec, Node, NodeSpec, Schema, Slice } from 'prosemirror-model'; import { Doc } from '../../../../fields/Doc'; @@ -28,7 +28,6 @@ export const marks: { [index: string]: MarkSpec } = { autoLinkAnchor: { attrs: { allAnchors: { default: [] as { href: string; title: string; anchorId: string }[] }, - location: { default: null }, title: { default: null }, }, inclusive: false, @@ -37,7 +36,6 @@ export const marks: { [index: string]: MarkSpec } = { tag: 'a[href]', getAttrs(dom: any) { return { - location: dom.getAttribute('location'), title: dom.getAttribute('title'), }; }, @@ -46,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', { class: anchorids, 'data-targethrefs': targethrefs, 'data-noPreview': 'true', 'data-linkdoc': node.attrs.linkDoc, title: node.attrs.title, location: node.attrs.location, style: `background: lightBlue` }, 0]; + return ['a', { class: anchorids, 'data-targethrefs': targethrefs, /*'data-noPreview': 'true', */ 'data-linkdoc': node.attrs.linkDoc, title: node.attrs.title, style: `background: lightBlue` }, 0]; }, }, noAutoLinkAnchor: { @@ -73,7 +71,6 @@ export const marks: { [index: string]: MarkSpec } = { linkAnchor: { attrs: { allAnchors: { default: [] as { href: string; title: string; anchorId: string }[] }, - location: { default: null }, title: { default: null }, noPreview: { default: false }, docref: { default: false }, // flags whether the linked text comes from a document within Dash. If so, an attribution label is appended after the text @@ -84,7 +81,6 @@ export const marks: { [index: string]: MarkSpec } = { tag: 'a[href]', getAttrs(dom: any) { return { - location: dom.getAttribute('location'), title: dom.getAttribute('title'), noPreview: dom.getAttribute('noPreview'), }; @@ -108,7 +104,7 @@ export const marks: { [index: string]: MarkSpec } = { node.attrs.title, ], ] - : ['a', { class: anchorids, 'data-targethrefs': targethrefs, title: node.attrs.title, 'data-noPreview': node.attrs.noPreview, location: node.attrs.location, style: `text-decoration: underline; cursor: default` }, 0]; + : ['a', { class: anchorids, 'data-targethrefs': targethrefs, title: node.attrs.title, 'data-noPreview': node.attrs.noPreview, style: `text-decoration: underline; cursor: default` }, 0]; }, }, diff --git a/src/client/views/nodes/formattedText/nodes_rts.ts b/src/client/views/nodes/formattedText/nodes_rts.ts index f27fb18e2..d023020e1 100644 --- a/src/client/views/nodes/formattedText/nodes_rts.ts +++ b/src/client/views/nodes/formattedText/nodes_rts.ts @@ -1,4 +1,4 @@ -import React = require('react'); +import * as React from 'react'; import { DOMOutputSpec, Node, NodeSpec } from 'prosemirror-model'; import { listItem, orderedList } from 'prosemirror-schema-list'; import { ParagraphNodeSpec, toParagraphDOM, getParagraphNodeAttrs } from './ParagraphNodeSpec'; @@ -212,7 +212,6 @@ export const nodes: { [index: string]: NodeSpec } = { alt: { default: null }, title: { default: null }, float: { default: 'left' }, - location: { default: 'add:right' }, docId: { default: '' }, }, group: 'inline', diff --git a/src/client/views/nodes/generativeFill/GenerativeFill.tsx b/src/client/views/nodes/generativeFill/GenerativeFill.tsx index 3093287e9..87e1b69c3 100644 --- a/src/client/views/nodes/generativeFill/GenerativeFill.tsx +++ b/src/client/views/nodes/generativeFill/GenerativeFill.tsx @@ -9,7 +9,9 @@ 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 { CollectionDockingView } from '../../collections/CollectionDockingView'; +import { CollectionFreeFormView } from '../../collections/collectionFreeForm'; import { OpenWhereMod } from '../DocumentView'; import { ImageBox } from '../ImageBox'; import './GenerativeFill.scss'; @@ -19,9 +21,7 @@ import { activeColor, canvasSize, eraserColor, freeformRenderSize, newCollection import { CursorData, ImageDimensions, Point } from './generativeFillUtils/generativeFillInterfaces'; import { APISuccess, ImageUtility } from './generativeFillUtils/ImageHandler'; import { PointerHandler } from './generativeFillUtils/PointerHandler'; -import React = require('react'); -import { DocumentManager } from '../../../util/DocumentManager'; -import { CollectionFreeFormView } from '../../collections/collectionFreeForm'; +import * as React from 'react'; enum BrushStyle { ADD, @@ -438,7 +438,7 @@ const GenerativeFill = ({ imageEditorOpen, imageEditorSource, imageRootDoc, addD // disable once edited has been clicked (doesn't make sense to change after first edit) disabled={edited} checked={isNewCollection} - onChange={e => { + onChange={() => { setIsNewCollection(prev => !prev); }} /> @@ -513,7 +513,7 @@ const GenerativeFill = ({ imageEditorOpen, imageEditorSource, imageRootDoc, addD defaultValue={150} size="small" valueLabelDisplay="auto" - onChange={(e, val) => { + onChange={(e: any, val: any) => { setCursorData(prev => ({ ...prev, width: val as number })); }} /> @@ -571,7 +571,7 @@ const GenerativeFill = ({ imageEditorOpen, imageEditorSource, imageRootDoc, addD <div> <TextField value={input} - onChange={e => setInput(e.target.value)} + onChange={(e: any) => setInput(e.target.value)} disabled={isBrushing} type="text" label="Prompt" diff --git a/src/client/views/nodes/generativeFill/GenerativeFillButtons.tsx b/src/client/views/nodes/generativeFill/GenerativeFillButtons.tsx index 10eca358e..185ba2280 100644 --- a/src/client/views/nodes/generativeFill/GenerativeFillButtons.tsx +++ b/src/client/views/nodes/generativeFill/GenerativeFillButtons.tsx @@ -1,5 +1,5 @@ import './GenerativeFillButtons.scss'; -import React = require('react'); +import * as React from 'react'; import ReactLoading from 'react-loading'; import { activeColor } from './generativeFillUtils/generativeFillConstants'; import { Button, IconButton, Type } from 'browndash-components'; diff --git a/src/client/views/nodes/importBox/ImportElementBox.tsx b/src/client/views/nodes/importBox/ImportElementBox.tsx index 58f0b29e4..6e7c3e612 100644 --- a/src/client/views/nodes/importBox/ImportElementBox.tsx +++ b/src/client/views/nodes/importBox/ImportElementBox.tsx @@ -1,30 +1,31 @@ -import { computed } from 'mobx'; +import { computed, makeObservable } from 'mobx'; import { observer } from 'mobx-react'; +import * as React from 'react'; +import { returnFalse } from '../../../../Utils'; import { Doc } from '../../../../fields/Doc'; -import { emptyFunction, returnEmptyDoclist, returnEmptyFilter, returnFalse, returnOne, returnTrue } from '../../../../Utils'; -import { Transform } from '../../../util/Transform'; import { ViewBoxBaseComponent } from '../../DocComponent'; -import { DefaultStyleProvider } from '../../StyleProvider'; -import { DocumentView, DocumentViewInternal } from '../DocumentView'; +import { DocumentView } from '../DocumentView'; import { FieldView, FieldViewProps } from '../FieldView'; -import React = require('react'); @observer export class ImportElementBox extends ViewBoxBaseComponent<FieldViewProps>() { public static LayoutString(fieldKey: string) { return FieldView.LayoutString(ImportElementBox, fieldKey); } + constructor(props: FieldViewProps) { + super(props); + makeObservable(this); + } - screenToLocalXf = () => this.props.ScreenToLocalTransform().scale(1 * (this.props.NativeDimScaling?.() || 1)); + screenToLocalXf = () => this.ScreenToLocalBoxXf().scale(1 * (this._props.NativeDimScaling?.() || 1)); @computed get mainItem() { return ( <div style={{ backgroundColor: 'pink' }}> <DocumentView - {...this.props} // + {...this._props} // LayoutTemplateString={undefined} - Document={this.rootDoc} + Document={this.Document} isContentActive={returnFalse} - DataDoc={undefined} addDocument={returnFalse} ScreenToLocalTransform={this.screenToLocalXf} hideResizeHandles={true} @@ -33,6 +34,6 @@ export class ImportElementBox extends ViewBoxBaseComponent<FieldViewProps>() { ); } render() { - return !(this.rootDoc instanceof Doc) ? null : this.mainItem; + return !(this.Document instanceof Doc) ? null : this.mainItem; } } diff --git a/src/client/views/nodes/trails/PresBox.scss b/src/client/views/nodes/trails/PresBox.scss index 1b76a39ad..1537ad0b8 100644 --- a/src/client/views/nodes/trails/PresBox.scss +++ b/src/client/views/nodes/trails/PresBox.scss @@ -1,4 +1,4 @@ -@import '../../global/globalCssVariables'; +@import '../../global/globalCssVariables.module.scss'; .presBox-cont { cursor: auto; diff --git a/src/client/views/nodes/trails/PresBox.tsx b/src/client/views/nodes/trails/PresBox.tsx index f1b501506..b2059b185 100644 --- a/src/client/views/nodes/trails/PresBox.tsx +++ b/src/client/views/nodes/trails/PresBox.tsx @@ -1,10 +1,10 @@ -import React = require('react'); import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { Tooltip } from '@material-ui/core'; -import { action, computed, IReactionDisposer, observable, ObservableSet, reaction, runInAction } from 'mobx'; +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, FieldResult, NumListCast, Opt, StrListCast } from '../../../../fields/Doc'; -import { Animation } from '../../../../fields/DocSymbols'; +import { Animation, DocData } from '../../../../fields/DocSymbols'; import { Copy, Id } from '../../../../fields/FieldSymbols'; import { InkField } from '../../../../fields/InkField'; import { List } from '../../../../fields/List'; @@ -24,27 +24,19 @@ import { SerializationHelper } from '../../../util/SerializationHelper'; import { SettingsManager } from '../../../util/SettingsManager'; import { undoBatch, UndoManager } from '../../../util/UndoManager'; import { CollectionDockingView } from '../../collections/CollectionDockingView'; -import { CollectionFreeFormView, computeTimelineLayout, MarqueeViewBounds } from '../../collections/collectionFreeForm'; +import { CollectionFreeFormView, MarqueeViewBounds } from '../../collections/collectionFreeForm'; import { CollectionStackedTimeline } from '../../collections/CollectionStackedTimeline'; import { CollectionView } from '../../collections/CollectionView'; -import { TabDocView } from '../../collections/TabDocView'; import { TreeView } from '../../collections/TreeView'; import { ViewBoxBaseComponent } from '../../DocComponent'; import { Colors } from '../../global/globalEnums'; import { LightboxView } from '../../LightboxView'; -import { DocFocusOptions, DocumentView, OpenWhere, OpenWhereMod } from '../DocumentView'; -import { FieldView, FieldViewProps } from '../FieldView'; +import { DocumentView, OpenWhere, OpenWhereMod } from '../DocumentView'; +import { FocusViewOptions, FieldView, FieldViewProps } from '../FieldView'; import { ScriptingBox } from '../ScriptingBox'; import './PresBox.scss'; import ReactLoading from 'react-loading'; import { PresEffect, PresEffectDirection, PresMovement, PresStatus } from './PresEnums'; -import { IconButton, Type } from 'browndash-components'; -import { AiOutlineSend } from 'react-icons/ai'; -import { DictationManager } from '../../../util/DictationManager'; -import { BiMicrophone, BiX } from 'react-icons/bi'; -import TextareaAutosize from 'react-textarea-autosize'; -import { gptTrailSlideCustomization } from '../../../apis/gpt/customization'; - export interface pinDataTypes { scrollable?: boolean; dataviz?: number[]; @@ -79,10 +71,11 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { } static navigateToDocScript: ScriptField; - constructor(props: any) { + constructor(props: FieldViewProps) { super(props); + makeObservable(this); if (!PresBox.navigateToDocScript) { - PresBox.navigateToDocScript = ScriptField.MakeFunction('navigateToDoc(self.presentation_targetDoc, self)')!; + PresBox.navigateToDocScript = ScriptField.MakeFunction('navigateToDoc(this.presentation_targetDoc, self)')!; } } @@ -91,12 +84,12 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { _batch: UndoManager.Batch | undefined = undefined; // undo batch for dragging sliders which generate multiple scene edit events as the cursor moves _keyTimer: NodeJS.Timeout | undefined; // timer for turning off transition flag when key frame change has completed. Need to clear this if you do a second navigation before first finishes, or else first timer can go off during second naviation. _unmounting = false; // flag that view is unmounting used to block RemFromMap from deleting things + _presTimer: NodeJS.Timeout | undefined; @observable public static Instance: PresBox; @observable _isChildActive = false; @observable _moveOnFromAudio: boolean = true; - @observable _presTimer!: NodeJS.Timeout; @observable _eleArray: HTMLElement[] = []; @observable _dragArray: HTMLElement[] = []; @@ -151,16 +144,16 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { return StrCast(this.layoutDoc.presFieldKey, 'data'); } @computed get childDocs() { - return DocListCast(this.rootDoc[this.presFieldKey]); + return DocListCast(this.Document[this.presFieldKey]); } @computed get tagDocs() { return this.childDocs.map(doc => Cast(doc.presentation_targetDoc, Doc, null)); } @computed get itemIndex() { - return NumCast(this.rootDoc._itemIndex); + return NumCast(this.Document._itemIndex); } @computed get activeItem() { - return DocCast(this.childDocs[NumCast(this.rootDoc._itemIndex)]); + return DocCast(this.childDocs[NumCast(this.Document._itemIndex)]); } @computed get targetDoc() { return Cast(this.activeItem?.presentation_targetDoc, Doc, null); @@ -178,21 +171,20 @@ 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.rootDoc); + if (SelectionManager.Views.length) return SelectionManager.Views[0]; + if (this.selectedArray.size) return DocumentManager.Instance.getDocumentView(this.Document); } @computed get isPres() { - return this.selectedDoc === this.rootDoc; + return this.selectedDoc === this.Document; } @computed get selectedDoc() { - return this.selectedDocumentView?.rootDoc; + return this.selectedDocumentView?.Document; } isActiveItemTarget = (layoutDoc: Doc) => this.activeItem?.presentation_targetDoc === layoutDoc; clearSelectedArray = () => this.selectedArray.clear(); addToSelectedArray = action((doc: Doc) => this.selectedArray.add(doc)); removeFromSelectedArray = action((doc: Doc) => this.selectedArray.delete(doc)); - @action componentWillUnmount() { this._unmounting = true; if (this._presTimer) clearTimeout(this._presTimer); @@ -207,7 +199,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { () => this.selectedDoc, selected => { document.removeEventListener('keydown', PresBox.keyEventsWrapper, true); - (this._presKeyEvents = selected === this.rootDoc) && document.addEventListener('keydown', PresBox.keyEventsWrapper, true); + (this._presKeyEvents = selected === this.Document) && document.addEventListener('keydown', PresBox.keyEventsWrapper, true); }, { fireImmediately: true } ); @@ -223,39 +215,27 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { }, { fireImmediately: true } ); - this.props.setContentView?.(this); + this._props.setContentViewBox?.(this); this._unmounting = false; - if (this.props.renderDepth > 0) { - runInAction(() => { - this.rootDoc._forceRenderEngine = computeTimelineLayout.name; - this.layoutDoc._gridGap = 0; - this.layoutDoc._yMargin = 0; - }); - } this.turnOffEdit(true); this._disposers.selection = reaction( - () => SelectionManager.Views().slice(), - views => (!PresBox.Instance || views.some(view => view.props.Document === this.rootDoc)) && this.updateCurrentPresentation(), + () => SelectionManager.Views.slice(), + views => (!PresBox.Instance || views.some(view => view.Document === this.Document)) && this.updateCurrentPresentation(), { fireImmediately: true } ); this._disposers.editing = reaction( () => this.layoutDoc.presentation_status === PresStatus.Edit, - editing => { - if (editing) { - this.childDocs.forEach(doc => { - if (doc.presentation_indexed !== undefined) { - this.progressivizedItems(doc)?.forEach(indexedDoc => (indexedDoc.opacity = undefined)); - doc.presentation_indexed = Math.min(this.progressivizedItems(doc)?.length ?? 0, 1); - } - }); - } - } + editing => editing && + this.childDocs.filter(doc => doc.presentation_indexed !== undefined).forEach(doc => { + this.progressivizedItems(doc)?.forEach(indexedDoc => (indexedDoc.opacity = undefined)); + doc.presentation_indexed = Math.min(this.progressivizedItems(doc)?.length ?? 0, 1); + }) // prettier-ignore ); } @action updateCurrentPresentation = (pres?: Doc) => { - Doc.ActivePresentation = pres ?? this.rootDoc; + Doc.ActivePresentation = pres ?? this.Document; PresBox.Instance = this; }; @@ -384,19 +364,10 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { }; // go to documents chain - runSubroutines = (childrenToRun: Doc[], normallyNextSlide: Doc) => { - console.log(childrenToRun, normallyNextSlide, 'runSUBFUNC'); - if (childrenToRun === undefined) { - console.log('children undefined'); - return; - } - if (childrenToRun[0] === normallyNextSlide) { - return; + runSubroutines = (childrenToRun: Opt<Doc[]>, normallyNextSlide: Doc) => { + if (childrenToRun && childrenToRun[0] !== normallyNextSlide) { + childrenToRun.forEach(child => DocumentManager.Instance.showDocument(child, {})); } - - childrenToRun.forEach(child => { - DocumentManager.Instance.showDocument(child, {}); - }); }; // Called when the user activates 'next' - to move to the next part of the pres. trail @@ -419,13 +390,9 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { listItemDoc.presentation_effect = this.activeItem.presBulletEffect; listItemDoc.presentation_transition = 500; targetView?.setAnimEffect(listItemDoc, 500); - if (targetView?.docView && this.activeItem.presBulletExpand) { - targetView.docView._animateScalingTo = 1.2; - targetView.docView._animateScaleTime = 400; - Doc.AddUnHighlightWatcher(() => { - targetView.docView!._animateScaleTime = undefined; - targetView!.docView!._animateScalingTo = 0; - }); + if (targetView && this.activeItem.presBulletExpand) { + targetView.setAnimateScaling(1.2, 400); + Doc.AddUnHighlightWatcher(() => targetView?.setAnimateScaling(0, undefined)); } listItemDoc.opacity = undefined; this.activeItem.presentation_indexed = presIndexed + 1; @@ -437,7 +404,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { if (progressiveReveal(false)) return true; if (this.childDocs[this.itemIndex + 1] !== undefined) { // Case 1: No more frames in current doc and next slide is defined, therefore move to next slide - const slides = DocListCast(this.rootDoc[StrCast(this.presFieldKey, 'data')]); + const slides = DocListCast(this.Document[StrCast(this.presFieldKey, 'data')]); const curLast = this.selectedArray.size ? Math.max(...Array.from(this.selectedArray).map(d => slides.indexOf(DocCast(d)))) : this.itemIndex; // before moving onto next slide, run the subroutines :) @@ -472,7 +439,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { // Case 2: There are no other frames so it should go to the previous slide prevSelected = Math.max(0, prevSelected - 1); this.nextSlide(prevSelected); - this.rootDoc._itemIndex = prevSelected; + this.Document._itemIndex = prevSelected; } else if (this.childDocs[this.itemIndex - 1] === undefined && this.layoutDoc.presLoop) { // Case 3: Pres loop is on so it should go to the last slide this.nextSlide(this.childDocs.length - 1); @@ -486,7 +453,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { public gotoDocument = action((index: number, from?: Doc, group?: boolean, finished?: () => void) => { Doc.UnBrushAllDocs(); if (index >= 0 && index < this.childDocs.length) { - this.rootDoc._itemIndex = index; + this.Document._itemIndex = index; if (from?.mediaStopTriggerList && this.layoutDoc.presentation_status !== PresStatus.Edit) { DocListCast(from.mediaStopTriggerList).forEach(this.stopTempMedia); } @@ -526,7 +493,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { playAnnotation = (anno: AudioField) => {}; @action static restoreTargetDocView(bestTargetView: Opt<DocumentView>, activeItem: Doc, transTime: number, pinDocLayout: boolean = BoolCast(activeItem.config_pinLayout), pinDataTypes?: pinDataTypes, targetDoc?: Doc) { - const bestTarget = bestTargetView?.rootDoc ?? (targetDoc?.layout_unrendered ? DocCast(targetDoc?.annotationOn) : targetDoc); + const bestTarget = bestTargetView?.Document ?? (targetDoc?.layout_unrendered ? DocCast(targetDoc?.annotationOn) : targetDoc); if (!bestTarget) return; let changed = false; if (pinDocLayout) { @@ -554,10 +521,10 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { 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)?.props.CollectionFreeFormDocumentView?.().props.CollectionFreeFormView; + const ffview = DocumentManager.Instance.getFirstDocumentView(context)?.CollectionFreeFormView; if (ffview?.childDocs) { PresBox.Instance._keyTimer = CollectionFreeFormView.gotoKeyframe(PresBox.Instance._keyTimer, ffview.childDocs, transTime); - ffview.rootDoc._currentFrame = NumCast(activeFrame); + ffview.layoutDoc._currentFrame = NumCast(activeFrame); } } } @@ -567,9 +534,10 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { const setData = bestTargetView?.ComponentView?.setData; if (setData) setData(activeItem.config_data); else { - const current = Doc.GetProto(bestTarget)[fkey]; - Doc.GetProto(bestTarget)[fkey + '_' + Date.now()] = current instanceof ObjectField ? current[Copy]() : current; - Doc.GetProto(bestTarget)[fkey] = activeItem.config_data instanceof ObjectField ? activeItem.config_data[Copy]() : activeItem.config_data; + const bestTargetData = bestTarget[DocData]; + const current = bestTargetData[fkey]; + bestTargetData[fkey + '_' + Date.now()] = 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); @@ -627,11 +595,11 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { } if (pinDataTypes?.inkable || (!pinDataTypes && (activeItem.config_fillColor !== undefined || activeItem.color !== undefined))) { if (bestTarget.fillColor !== activeItem.config_fillColor) { - Doc.GetProto(bestTarget).fillColor = StrCast(activeItem.config_fillColor, StrCast(bestTarget.fillColor)); + bestTarget[DocData].fillColor = StrCast(activeItem.config_fillColor, StrCast(bestTarget.fillColor)); changed = true; } if (bestTarget.color !== activeItem.config_color) { - Doc.GetProto(bestTarget).color = StrCast(activeItem.config_color, StrCast(bestTarget.color)); + bestTarget[DocData].color = StrCast(activeItem.config_color, StrCast(bestTarget.color)); changed = true; } if (bestTarget.width !== activeItem.width) { @@ -672,11 +640,6 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { if (bestTarget._layout_scrollTop !== activeItem.config_scrollTop) { bestTarget._layout_scrollTop = activeItem.config_scrollTop; changed = true; - const contentBounds = Cast(activeItem.config_viewBounds, listSpec('number')); - if (contentBounds) { - const dv = DocumentManager.Instance.getDocumentView(bestTarget)?.ComponentView; - dv?.brushView?.({ panX: (contentBounds[0] + contentBounds[2]) / 2, panY: (contentBounds[1] + contentBounds[3]) / 2, width: contentBounds[2] - contentBounds[0], height: contentBounds[3] - contentBounds[1] }, transTime); - } } } if (pinDataTypes?.dataannos || (!pinDataTypes && activeItem.config_annotations !== undefined)) { @@ -693,7 +656,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { return doc; }); const newList = new List<Doc>([...oldItems, ...hiddenItems, ...newItems]); - Doc.GetProto(bestTarget)[fkey + '_annotations'] = newList; + bestTarget[DocData][fkey + '_annotations'] = newList; } if (pinDataTypes?.poslayoutview || (!pinDataTypes && activeItem.config_pinLayoutData !== undefined)) { changed = true; @@ -714,14 +677,14 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { data.fill && (doc._fillColor = data.fill); doc._width = data.w; doc._height = data.h; - data.data && (Doc.GetProto(doc).data = field); - data.text && (Doc.GetProto(doc).text = tfield); - Doc.AddDocToList(Doc.GetProto(bestTarget), layoutField, doc); + data.data && (doc[DocData].data = field); + data.text && (doc[DocData].text = tfield); + Doc.AddDocToList(bestTarget[DocData], layoutField, doc); } }); 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) { + 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')); if (contentBounds) { const viewport = { panX: (contentBounds[0] + contentBounds[2]) / 2, panY: (contentBounds[1] + contentBounds[3]) / 2, width: contentBounds[2] - contentBounds[0], height: contentBounds[3] - contentBounds[1] }; @@ -730,9 +693,9 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { const dv = DocumentManager.Instance.getDocumentView(bestTarget); if (dv) { changed = true; - const computedScale = NumCast(activeItem.config_zoom, 1) * Math.min(dv.props.PanelWidth() / viewport.width, dv.props.PanelHeight() / viewport.height); + const computedScale = NumCast(activeItem.config_zoom, 1) * Math.min(dv._props.PanelWidth() / viewport.width, dv._props.PanelHeight() / viewport.height); activeItem.presentation_movement === PresMovement.Zoom && (bestTarget._freeform_scale = computedScale); - dv.ComponentView?.brushView?.(viewport, transTime); + 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) { @@ -782,7 +745,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { } if (pinProps.pinData.dataannos) { const fkey = Doc.LayoutFieldKey(targetDoc); - pinDoc.config_annotations = new List<Doc>(DocListCast(Doc.GetProto(targetDoc)[fkey + '_annotations']).filter(doc => !doc.layout_unrendered)); + 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; @@ -877,8 +840,8 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { const dragViewCache = Array.from(this._dragArray); const eleViewCache = Array.from(this._eleArray); const resetSelection = action(() => { - if (!this.props.isSelected()) { - const presDocView = DocumentManager.Instance.getDocumentView(this.rootDoc); + if (!this._props.isSelected()) { + const presDocView = DocumentManager.Instance.getDocumentView(this.Document); if (presDocView) SelectionManager.SelectView(presDocView, false); this.clearSelectedArray(); selViewCache.forEach(doc => this.addToSelectedArray(doc)); @@ -890,6 +853,8 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { PresBox.NavigateToTarget(targetDoc, activeItem, resetSelection); }; + public static PanelName = 'PRESBOX'; + 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?.(); @@ -897,14 +862,14 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { } const effect = activeItem.presentation_effect && activeItem.presentation_effect !== PresEffect.None ? activeItem.presentation_effect : undefined; const presTime = NumCast(activeItem.presentation_transition, effect ? 750 : 500); - const options: DocFocusOptions = { + const options: FocusViewOptions = { willPan: activeItem.presentation_movement !== PresMovement.None, willZoomCentered: activeItem.presentation_movement === PresMovement.Zoom || activeItem.presentation_movement === PresMovement.Jump || activeItem.presentation_movement === PresMovement.Center, zoomScale: activeItem.presentation_movement === PresMovement.Center ? 0 : NumCast(activeItem.config_zoom, 1), zoomTime: activeItem.presentation_movement === PresMovement.Jump ? 0 : Math.min(Math.max(effect ? 750 : 500, (effect ? 0.2 : 1) * presTime), presTime), effect: activeItem, noSelect: true, - openLocation: OpenWhere.addLeft, + 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), @@ -914,7 +879,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { if (activeItem.presentation_openInLightbox) { const context = DocCast(targetDoc.annotationOn) ?? targetDoc; if (!DocumentManager.Instance.getLightboxDocumentView(context)) { - LightboxView.SetLightboxDoc(context); + LightboxView.Instance.SetLightboxDoc(context); } } if (targetDoc) { @@ -923,7 +888,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { DocumentManager.Instance.AddViewRenderedCb(LightboxView.LightboxDoc, dv => { // if target or the doc it annotates is not in the lightbox, then close the lightbox if (!DocumentManager.Instance.getLightboxDocumentView(DocCast(targetDoc.annotationOn) ?? targetDoc)) { - LightboxView.SetLightboxDoc(undefined); + LightboxView.Instance.SetLightboxDoc(undefined); } DocumentManager.Instance.showDocument(targetDoc, options, finished); }); @@ -1017,8 +982,8 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { break; } }); - LightboxView.SetLightboxDoc(undefined); - Doc.RemFromMyOverlay(this.rootDoc); + LightboxView.Instance.SetLightboxDoc(undefined); + Doc.RemFromMyOverlay(this.Document); return PresStatus.Edit; }; }; @@ -1057,7 +1022,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { // The function allows for expanding the view of pres on toggle @action toggleExpandMode = () => { runInAction(() => (this._expandBoolean = !this._expandBoolean)); - this.rootDoc.expandBoolean = this._expandBoolean; + this.Document.expandBoolean = this._expandBoolean; this.childDocs.forEach(doc => { doc.presentation_expandInlineButton = this._expandBoolean; }); @@ -1107,16 +1072,16 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { */ @action enterMinimize = () => { - this.updateCurrentPresentation(this.rootDoc); + this.updateCurrentPresentation(this.Document); clearTimeout(this._presTimer); - const pt = this.props.ScreenToLocalTransform().inverse().transformPoint(0, 0); - this.props.removeDocument?.(this.layoutDoc); - return PresBox.OpenPresMinimized(this.rootDoc, [pt[0] + (this.props.PanelWidth() - 250), pt[1] + 10]); + const pt = this.ScreenToLocalBoxXf().inverse().transformPoint(0, 0); + this._props.removeDocument?.(this.layoutDoc); + return PresBox.OpenPresMinimized(this.Document, [pt[0] + (this._props.PanelWidth() - 250), pt[1] + 10]); }; exitMinimize = () => { if (Doc.IsInMyOverlay(this.layoutDoc)) { - Doc.RemFromMyOverlay(this.rootDoc); - CollectionDockingView.AddSplit(this.rootDoc, OpenWhereMod.right); + Doc.RemFromMyOverlay(this.Document); + CollectionDockingView.AddSplit(this.Document, OpenWhereMod.right); } return PresStatus.Edit; }; @@ -1142,8 +1107,8 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { const type_collection = e.target.selectedOptions[0].value as CollectionViewType; this.layoutDoc.presFieldKey = this.fieldKey + (type_collection === 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.rootDoc._pivotField = undefined); - this.rootDoc._type_collection = type_collection; + [CollectionViewType.Tree || CollectionViewType.Stacking].includes(type_collection) && (this.Document._pivotField = undefined); + this.Document._type_collection = type_collection; if (this.isTreeOrStack) { this.layoutDoc._gridGap = 0; } @@ -1179,7 +1144,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { return StrCast(activeItem.presentation_movement); }); - whenChildContentsActiveChanged = action((isActive: boolean) => this.props.whenChildContentsActiveChanged((this._isChildActive = isActive))); + whenChildContentsActiveChanged = action((isActive: boolean) => this._props.whenChildContentsActiveChanged((this._isChildActive = isActive))); // For dragging documents into the presentation trail addDocumentFilter = (docs: Doc[]) => { docs.forEach((doc, i) => { @@ -1190,11 +1155,11 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { audio.config_clipStart = NumCast(doc._timecodeToShow /* audioStart */, NumCast(doc._timecodeToShow /* videoStart */)); audio.config_clipEnd = NumCast(doc._timecodeToHide /* audioEnd */, NumCast(doc._timecodeToHide /* videoEnd */)); audio.presentation_duration = audio.config_clipStart - audio.config_clipEnd; - TabDocView.PinDoc(audio, { audioRange: true }); + this._props.pinToPres(audio, { audioRange: true }); setTimeout(() => this.removeDocument(doc), 0); return false; } - } else { + } else if (doc.type !== DocumentType.PRES) { if (!doc.presentation_targetDoc) doc.title = doc.title + ' - Slide'; doc.presentation_targetDoc = doc.createdFrom; // dropped document will be a new embedding of an embedded document somewhere else. doc.presentation_movement = PresMovement.Zoom; @@ -1203,10 +1168,11 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { }); return true; }; + childLayoutTemplate = () => Docs.Create.PresElementBoxDocument(); - removeDocument = (doc: Doc) => Doc.RemoveDocFromList(this.rootDoc, this.fieldKey, doc); - getTransform = () => this.props.ScreenToLocalTransform().translate(-5, -65); // listBox padding-left and pres-box-cont minHeight - panelHeight = () => this.props.PanelHeight() - 40; + removeDocument = (doc: Doc) => Doc.RemoveDocFromList(this.Document, this.fieldKey, doc); + getTransform = () => this.ScreenToLocalBoxXf().translate(-5, -65); // listBox padding-left and pres-box-cont minHeight + panelHeight = () => this._props.PanelHeight() - 40; /** * For sorting the array so that the order is maintained when it is dropped. */ @@ -1244,11 +1210,11 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { @action selectPres = () => { - const presDocView = DocumentManager.Instance.getDocumentView(this.rootDoc); + const presDocView = DocumentManager.Instance.getDocumentView(this.Document); presDocView && SelectionManager.SelectView(presDocView, false); }; - focusElement = (doc: Doc, options: DocFocusOptions) => { + focusElement = (doc: Doc, options: FocusViewOptions) => { this.selectElement(doc); return undefined; }; @@ -1260,7 +1226,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { if (noNav) { const index = this.childDocs.indexOf(doc); if (index >= 0 && index < this.childDocs.length) { - this.rootDoc._itemIndex = index; + this.Document._itemIndex = index; } } else { this.gotoDocument(this.childDocs.indexOf(doc), this.activeItem); @@ -1359,8 +1325,8 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { case 'ArrowRight': if (e.shiftKey && this.itemIndex < this.childDocs.length - 1) { // TODO: update to work properly - this.rootDoc._itemIndex = NumCast(this.rootDoc._itemIndex) + 1; - this.addToSelectedArray(this.childDocs[this.rootDoc._itemIndex]); + this.Document._itemIndex = NumCast(this.Document._itemIndex) + 1; + this.addToSelectedArray(this.childDocs[this.Document._itemIndex]); } else { this.next(); if (this._presTimer) { @@ -1376,8 +1342,8 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { case 'ArrowLeft': if (e.shiftKey && this.itemIndex !== 0) { // TODO: update to work properly - this.rootDoc._itemIndex = NumCast(this.rootDoc._itemIndex) - 1; - this.addToSelectedArray(this.childDocs[this.rootDoc._itemIndex]); + this.Document._itemIndex = NumCast(this.Document._itemIndex) - 1; + this.addToSelectedArray(this.childDocs[this.Document._itemIndex]); } else { this.back(); if (this._presTimer) { @@ -1440,7 +1406,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { } else if (doc.config_pinView && presCollection === tagDoc && dv) { // Case B: Document is presPinView and is presCollection const scale = 1 / NumCast(doc.config_viewScale); - const viewBounds = NumListCast(doc.config_viewBounds, [0, 0, dv.props.PanelWidth(), dv.props.PanelHeight()]); + const viewBounds = NumListCast(doc.config_viewBounds, [0, 0, dv._props.PanelWidth(), dv._props.PanelHeight()]); const height = (viewBounds[3] - viewBounds[1]) * scale; const width = (viewBounds[2] - viewBounds[0]) * scale; const indWidth = width / 10; @@ -1560,46 +1526,39 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { updateMovement = action((movement: PresMovement, all?: boolean) => (all ? this.childDocs : this.selectedArray).forEach(doc => (doc.presentation_movement = movement))); @undoBatch - @action updateHideBefore = (activeItem: Doc) => { activeItem.presentation_hideBefore = !activeItem.presentation_hideBefore; this.selectedArray.forEach(doc => (doc.presentation_hideBefore = activeItem.presentation_hideBefore)); }; @undoBatch - @action updateHide = (activeItem: Doc) => { activeItem.presentation_hide = !activeItem.presentation_hide; this.selectedArray.forEach(doc => (doc.presentation_hide = activeItem.presentation_hide)); }; @undoBatch - @action updateHideAfter = (activeItem: Doc) => { activeItem.presentation_hideAfter = !activeItem.presentation_hideAfter; this.selectedArray.forEach(doc => (doc.presentation_hideAfter = activeItem.presentation_hideAfter)); }; @undoBatch - @action updateOpenDoc = (activeItem: Doc) => { activeItem.presentation_openInLightbox = !activeItem.presentation_openInLightbox; this.selectedArray.forEach(doc => (doc.presentation_openInLightbox = activeItem.presentation_openInLightbox)); }; @undoBatch - @action updateEaseFunc = (activeItem: Doc) => { activeItem.presEaseFunc = activeItem.presEaseFunc === 'linear' ? 'ease' : 'linear'; this.selectedArray.forEach(doc => (doc.presEaseFunc = activeItem.presEaseFunc)); }; @undoBatch - @action updateEffectDirection = (effect: PresEffectDirection, all?: boolean) => (all ? this.childDocs : this.selectedArray).forEach(doc => (doc.presentation_effectDirection = effect)); @undoBatch - @action updateEffect = (effect: PresEffect, bullet: boolean, all?: boolean) => (all ? this.childDocs : this.selectedArray).forEach(doc => (bullet ? (doc.presBulletEffect = effect) : (doc.presentation_effect = effect))); static _sliderBatch: any; @@ -1632,7 +1591,6 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { }; @undoBatch - @action applyTo = (array: Doc[]) => { this.updateMovement(this.activeItem.presentation_movement as PresMovement, true); this.updateEffect(this.activeItem.presentation_effect as PresEffect, false, true); @@ -1762,8 +1720,8 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { let dataField = Doc.LayoutFieldKey(tagDoc); if (Cast(tagDoc[dataField], listSpec(Doc), null)?.filter(d => d instanceof Doc) === undefined) dataField = dataField + '_annotations'; - if (DocCast(activeItem.presentation_targetDoc).annotationOn) activeItem.data = ComputedField.MakeFunction(`self.presentation_targetDoc.annotationOn["${dataField}"]`); - else activeItem.data = ComputedField.MakeFunction(`self.presentation_targetDoc["${dataField}"]`); + 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} /> @@ -1809,12 +1767,9 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { className={'presBox-dropdownOptions'} style={{ display: this._openBulletEffectDropdown ? 'grid' : 'none', color: SettingsManager.userColor, background: SettingsManager.userBackgroundColor }} onPointerDown={e => e.stopPropagation()}> - {bulletEffect(PresEffect.None)} - {bulletEffect(PresEffect.Fade)} - {bulletEffect(PresEffect.Flip)} - {bulletEffect(PresEffect.Rotate)} - {bulletEffect(PresEffect.Bounce)} - {bulletEffect(PresEffect.Roll)} + {Object.values(PresEffect) + .filter(v => isNaN(Number(v))) + .map(effect => bulletEffect(effect))} </div> </div> </div> @@ -1978,12 +1933,9 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { display: this._openEffectDropdown ? 'grid' : 'none', }} onPointerDown={e => e.stopPropagation()}> - {preseEffect(PresEffect.None)} - {preseEffect(PresEffect.Fade)} - {preseEffect(PresEffect.Flip)} - {preseEffect(PresEffect.Rotate)} - {preseEffect(PresEffect.Bounce)} - {preseEffect(PresEffect.Roll)} + {Object.values(PresEffect) + .filter(v => isNaN(Number(v))) + .map(effect => preseEffect(effect))} </div> </div> <div className="ribbon-doubleButton" style={{ display: effect === PresEffectDirection.None ? 'none' : 'inline-flex' }}> @@ -2343,13 +2295,13 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { 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 data = Cast(presCollection?.data, listSpec(Doc)); - const config_data = Cast(this.rootDoc.data, listSpec(Doc)); + const config_data = Cast(this.Document.data, listSpec(Doc)); if (data && config_data) { data.push(doc); - TabDocView.PinDoc(doc, {}); + this._props.pinToPres(doc, {}); this.gotoDocument(this.childDocs.length, this.activeItem); } else { - this.props.addDocTab(doc, OpenWhere.addRight); + this._props.addDocTab(doc, OpenWhere.addRight); } } }; @@ -2410,20 +2362,20 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { @computed get toolbarWidth(): number { - return this.props.PanelWidth(); + return this._props.PanelWidth(); } @action - toggleProperties = () => (SettingsManager.propertiesWidth = SettingsManager.propertiesWidth > 0 ? 0 : 250); + toggleProperties = () => (SettingsManager.Instance.propertiesWidth = SettingsManager.Instance.propertiesWidth > 0 ? 0 : 250); @computed get toolbar() { - const propIcon = SettingsManager.propertiesWidth > 0 ? 'angle-double-right' : 'angle-double-left'; - const propTitle = SettingsManager.propertiesWidth > 0 ? 'Close Presentation Panel' : 'Open Presentation Panel'; - const mode = StrCast(this.rootDoc._type_collection) as CollectionViewType; + const propIcon = SettingsManager.Instance.propertiesWidth > 0 ? 'angle-double-right' : 'angle-double-left'; + const propTitle = SettingsManager.Instance.propertiesWidth > 0 ? 'Close Presentation Panel' : 'Open Presentation Panel'; + const mode = StrCast(this.Document._type_collection) as CollectionViewType; const isMini: boolean = this.toolbarWidth <= 100; const activeColor = SettingsManager.userVariantColor; const inactiveColor = lightOrDark(SettingsManager.userBackgroundColor) === Colors.WHITE ? Colors.WHITE : SettingsManager.userBackgroundColor; - return mode === CollectionViewType.Carousel3D || Doc.IsInMyOverlay(this.rootDoc) ? null : ( + return mode === CollectionViewType.Carousel3D || Doc.IsInMyOverlay(this.Document) ? null : ( <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"} /> @@ -2447,7 +2399,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { </Tooltip> <Tooltip title={<div className="dash-tooltip">{propTitle}</div>}> <div className="toolbar-button" style={{ position: 'absolute', right: 4, fontSize: 16 }} onClick={this.toggleProperties}> - <FontAwesomeIcon className={'toolbar-thumbtack'} icon={propIcon} style={{ color: SettingsManager.propertiesWidth > 0 ? activeColor : inactiveColor }} /> + <FontAwesomeIcon className={'toolbar-thumbtack'} icon={propIcon} style={{ color: SettingsManager.Instance.propertiesWidth > 0 ? activeColor : inactiveColor }} /> </div> </Tooltip> </> @@ -2462,12 +2414,10 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { * presentPanel: The button to start the presentation / open minimized view of the presentation */ @computed get topPanel() { - const mode = StrCast(this.rootDoc._type_collection) as CollectionViewType; + const mode = StrCast(this.Document._type_collection) as CollectionViewType; const isMini: boolean = this.toolbarWidth <= 100; return ( - <div - className={`presBox-buttons${Doc.IsInMyOverlay(this.rootDoc) ? ' inOverlay' : ''}`} - style={{ background: Doc.ActivePresentation === this.rootDoc ? Colors.LIGHT_BLUE : undefined, display: !this.rootDoc._chromeHidden ? 'none' : undefined }}> + <div className={`presBox-buttons${Doc.IsInMyOverlay(this.Document) ? ' inOverlay' : ''}`} style={{ background: Doc.ActivePresentation === this.Document ? Colors.LIGHT_BLUE : undefined }}> {isMini ? null : ( <select className="presBox-viewPicker" style={{ display: this.layoutDoc.presentation_status === 'edit' ? 'block' : 'none' }} onPointerDown={e => e.stopPropagation()} onChange={this.viewChanged} value={mode}> <option onPointerDown={StopEvent} value={CollectionViewType.Stacking}> @@ -2495,7 +2445,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { } })}> <FontAwesomeIcon icon={'play-circle'} /> - <div style={{ display: this.props.PanelWidth() > 200 ? 'inline-flex' : 'none' }}> Present</div> + <div style={{ display: this._props.PanelWidth() > 200 ? 'inline-flex' : 'none' }}> Present</div> </div> {mode === CollectionViewType.Carousel3D || isMini ? null : ( <div @@ -2520,7 +2470,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { this.itemIndex === this.childDocs.length - 1 && (this.activeItem.presentation_indexed === undefined || NumCast(this.activeItem.presentation_indexed) === (this.progressivizedItems(this.activeItem)?.length ?? 0)); const presStart: boolean = !this.layoutDoc.presLoop && this.itemIndex === 0; - const inOverlay = Doc.IsInMyOverlay(this.rootDoc); + const inOverlay = Doc.IsInMyOverlay(this.Document); // 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' }}> @@ -2605,12 +2555,12 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { <b>1</b> </div> </Tooltip> - <div className="presPanel-button-text" onClick={() => this.gotoDocument(0, this.activeItem)} style={{ display: inOverlay || this.props.PanelWidth() > 250 ? 'inline-flex' : 'none' }}> + <div className="presPanel-button-text" onClick={() => this.gotoDocument(0, this.activeItem)} style={{ display: inOverlay || this._props.PanelWidth() > 250 ? 'inline-flex' : 'none' }}> {inOverlay ? '' : 'Slide'} {this.itemIndex + 1} {this.activeItem?.presentation_indexed !== undefined ? `(${this.activeItem.presentation_indexed}/${this.progressivizedItems(this.activeItem)?.length})` : ''} / {this.childDocs.length} </div> <div className="presPanel-divider"></div> - {this.props.PanelWidth() > 250 ? ( + {this._props.PanelWidth() > 250 ? ( <div className="presPanel-button-text" onClick={undoBatch( @@ -2656,7 +2606,6 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { }; @undoBatch - @action exitClicked = () => { this.layoutDoc.presentation_status = this._exitTrail?.() ?? this.exitMinimize(); clearTimeout(this._presTimer); @@ -2678,6 +2627,8 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { } }; + SlideIndex = (slideDoc: Doc) => DocListCast(this.dataDoc[this.presFieldKey]).indexOf(slideDoc); + RemFromMap = (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. if (!this._unmounting && this.isTree) { @@ -2691,17 +2642,17 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { render() { // needed to ensure that the childDocs are loaded for looking up fields this.childDocs.slice(); - const mode = StrCast(this.rootDoc._type_collection) as CollectionViewType; + const mode = StrCast(this.Document._type_collection) as CollectionViewType; const presEnd = !this.layoutDoc.presLoop && this.itemIndex === this.childDocs.length - 1 && (this.activeItem.presentation_indexed === undefined || NumCast(this.activeItem.presentation_indexed) === (this.progressivizedItems(this.activeItem)?.length ?? 0)); const presStart = !this.layoutDoc.presLoop && this.itemIndex === 0; - return this.props.addDocTab === returnFalse ? ( // bcz: hack!! - addDocTab === returnFalse only when this is being rendered by the OverlayView which means the doc is a mini player + return this._props.addDocTab === returnFalse ? ( // bcz: hack!! - addDocTab === returnFalse only when this is being rendered by the OverlayView which means the doc is a mini player <div className="miniPres" onClick={e => e.stopPropagation()} onPointerEnter={action(e => (this._forceKeyEvents = true))}> <div className="presPanelOverlay" - style={{ display: 'inline-flex', height: 30, background: Doc.ActivePresentation === this.rootDoc ? 'green' : '#323232', top: 0, zIndex: 3000000, boxShadow: this._presKeyEvents ? '0 0 0px 3px ' + Colors.MEDIUM_BLUE : undefined }}> + 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>}> <div className="presPanel-button" @@ -2739,7 +2690,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { </div> </div> ) : ( - <div className="presBox-cont" style={{ minWidth: Doc.IsInMyOverlay(this.rootDoc) ? PresBox.minimizedWidth : undefined }}> + <div className="presBox-cont" style={{ minWidth: Doc.IsInMyOverlay(this.Document) ? PresBox.minimizedWidth : undefined }}> {this.topPanel} {this.toolbar} {this.newDocumentToolbarDropdown} @@ -2747,19 +2698,19 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { <div className="Slide"> {mode !== CollectionViewType.Invalid ? ( <CollectionView - {...this.props} - PanelWidth={this.props.PanelWidth} + {...this._props} + PanelWidth={this._props.PanelWidth} PanelHeight={this.panelHeight} childIgnoreNativeSize={true} moveDocument={returnFalse} ignoreUnrendered={true} childDragAction="move" - setContentView={emptyFunction} + setContentViewBox={emptyFunction} //childLayoutFitWidth={returnTrue} childOpacity={returnOne} childClickScript={PresBox.navigateToDocScript} childLayoutTemplate={this.childLayoutTemplate} - childXPadding={Doc.IsComicStyle(this.rootDoc) ? 20 : undefined} + childXPadding={Doc.IsComicStyle(this.Document) ? 20 : undefined} filterAddDocument={this.addDocumentFilter} removeDocument={returnFalse} dontRegisterView={true} @@ -2767,7 +2718,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { ScreenToLocalTransform={this.getTransform} AddToMap={this.AddToMap} RemFromMap={this.RemFromMap} - hierarchyIndex={emptyPath} + hierarchyIndex={emptyPath as any as number[]} /> ) : null} </div> diff --git a/src/client/views/nodes/trails/PresElementBox.tsx b/src/client/views/nodes/trails/PresElementBox.tsx index 37f449001..ed2f25fb6 100644 --- a/src/client/views/nodes/trails/PresElementBox.tsx +++ b/src/client/views/nodes/trails/PresElementBox.tsx @@ -1,9 +1,9 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { Tooltip } from '@material-ui/core'; -import { action, computed, IReactionDisposer, observable, reaction, runInAction } from 'mobx'; +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 { Doc, DocListCast, Opt } from '../../../../fields/Doc'; -import { Height, Width } from '../../../../fields/DocSymbols'; import { Id } from '../../../../fields/FieldSymbols'; import { List } from '../../../../fields/List'; import { BoolCast, Cast, DocCast, NumCast, StrCast } from '../../../../fields/Types'; @@ -15,17 +15,16 @@ import { DragManager } from '../../../util/DragManager'; import { SettingsManager } from '../../../util/SettingsManager'; 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, DocumentViewProps } from '../../nodes/DocumentView'; +import { DocumentView } from '../../nodes/DocumentView'; import { FieldView, FieldViewProps } from '../../nodes/FieldView'; import { StyleProp } from '../../StyleProvider'; import { PresBox } from './PresBox'; import './PresElementBox.scss'; import { PresMovement } from './PresEnums'; -import React = require('react'); -import { TreeView } from '../../collections/TreeView'; /** * This class models the view a document added to presentation will have in the presentation. * It involves some functionality for its buttons and options. @@ -35,37 +34,58 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() { public static LayoutString(fieldKey: string) { return FieldView.LayoutString(PresElementBox, fieldKey); } - _heightDisposer: IReactionDisposer | undefined; + private _itemRef: React.RefObject<HTMLDivElement> = React.createRef(); + private _dragRef: React.RefObject<HTMLDivElement> = React.createRef(); + private _titleRef: React.RefObject<EditableView> = React.createRef(); + private _heightDisposer: IReactionDisposer | undefined; + readonly expandViewHeight = 100; + readonly collapsedHeight = 35; - @observable _dragging = false; - // Idea: this boolean will determine whether to automatically show the video when this preselement is selected. - // @observable static showVideo: boolean = false; - @computed get indexInPres() { - return DocListCast(this.presBox?.[StrCast(this.presBox.presFieldKey, 'data')]).indexOf(this.rootDoc); - } // the index field is where this document is in the presBox display list (since this value is different for each presentation element, the value can't be stored on the layout template which is used by all display elements) - @computed get expandViewHeight() { - return 100; - } - @computed get collapsedHeight() { - return 35; - } // the collapsed height changes depending on the state of the presBox. We could store this on the presentation element template if it's used by only one presentation - but if it's shared by multiple, then this value must be looked up - @computed get selectedArray() { - return this.presBoxView?.selectedArray; + constructor(props: FieldViewProps) { + super(props); + makeObservable(this); } + + @observable _dragging = false; + + // the presentation view that renders this slide @computed get presBoxView() { - return this.props.DocumentView?.()?.props.docViewPath().lastElement()?.ComponentView as PresBox; + return this.DocumentView?.().containerViewPath?.().lastElement()?.ComponentView as PresBox; } + + // the presentation view document that renders this slide @computed get presBox() { - return this.props.DocumentView?.().props.docViewPath().lastElement()?.rootDoc; + return this.presBoxView?.Document; + } + + // Since this node is being rendered with a template, this method retrieves + // the actual slide being rendered from the auto-generated rendering template + @computed get slideDoc() { + return this._props.TemplateDataDocument ?? this.Document; } + + // this is the document in the workspaces that is targeted by the slide @computed get targetDoc() { - return Cast(this.rootDoc.presentation_targetDoc, Doc, null) || this.rootDoc; + return Cast(this.slideDoc.presentation_targetDoc, Doc, null) || this.slideDoc; + } + + // computes index of this presentation slide in the presBox list + @computed get indexInPres() { + return this.presBoxView?.SlideIndex(this.slideDoc); + } + + @computed get selectedArray() { + return this.presBoxView?.selectedArray; + } + + @computed get videoRecordingIsInOverlay() { + return Doc.MyOverlayDocs.some(doc => doc.slides === this.slideDoc); } componentDidMount() { this.layoutDoc.layout_hideLinkButton = true; this._heightDisposer = reaction( - () => ({ expand: this.rootDoc.presentation_expandInlineButton, height: this.collapsedHeight }), + () => ({ expand: this.slideDoc.presentation_expandInlineButton, height: this.collapsedHeight }), ({ expand, height }) => (this.layoutDoc._height = height + (expand ? this.expandViewHeight : 0)), { fireImmediately: true } ); @@ -74,55 +94,41 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() { this._heightDisposer?.(); } - /** - * Returns a local transformed coordinate array for given coordinates. - */ - ScreenToLocalListTransform = (xCord: number, yCord: number) => [xCord, yCord]; - - @action - presExpandDocumentClick = () => (this.rootDoc.presentation_expandInlineButton = !this.rootDoc.presentation_expandInlineButton); - - embedHeight = (): number => this.collapsedHeight + this.expandViewHeight; - // embedWidth = () => this.props.PanelWidth(); - // embedHeight = () => Math.min(this.props.PanelWidth() - 20, this.props.PanelHeight() - this.collapsedHeight); - embedWidth = (): number => this.props.PanelWidth() / 2; - styleProvider = (doc: Doc | undefined, props: Opt<DocumentViewProps>, property: string): any => { - if (property === StyleProp.Opacity) return 1; - return this.props.styleProvider?.(doc, props, property); + 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); }; /** * The function that is responsible for rendering a preview or not for this * presentation element. */ @computed get renderEmbeddedInline() { - return !this.rootDoc.presentation_expandInlineButton || !this.targetDoc ? null : ( + return !this.slideDoc.presentation_expandInlineButton || !this.targetDoc ? null : ( <div className="presItem-embedded" style={{ height: this.embedHeight(), width: '50%' }}> <DocumentView - Document={PresBox.targetRenderedDoc(this.rootDoc)} - DataDoc={undefined} //this.targetDoc[DataSym] !== this.targetDoc && this.targetDoc[DataSym]} + Document={PresBox.targetRenderedDoc(this.slideDoc)} PanelWidth={this.embedWidth} PanelHeight={this.embedHeight} - isContentActive={this.props.isContentActive} + isContentActive={this._props.isContentActive} styleProvider={this.styleProvider} hideLinkButton={true} ScreenToLocalTransform={Transform.Identity} - renderDepth={this.props.renderDepth + 1} - docViewPath={returnEmptyDoclist} - childFilters={this.props.childFilters} - childFiltersByRanges={this.props.childFiltersByRanges} - searchFilterDocs={this.props.searchFilterDocs} - rootSelected={returnTrue} + renderDepth={this._props.renderDepth + 1} + containerViewPath={returnEmptyDoclist} + childFilters={this._props.childFilters} + childFiltersByRanges={this._props.childFiltersByRanges} + searchFilterDocs={this._props.searchFilterDocs} addDocument={returnFalse} removeDocument={returnFalse} fitContentsToBox={returnTrue} - moveDocument={this.props.moveDocument!} + moveDocument={this._props.moveDocument!} focus={emptyFunction} whenChildContentsActiveChanged={returnFalse} addDocTab={returnFalse} pinToPres={returnFalse} - bringToFront={returnFalse} /> - {/* <div className="presItem-embeddedMask" /> */} </div> ); } @@ -159,26 +165,22 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() { @computed get transition() { let transitionInS: number; - if (this.rootDoc.presentation_transition) transitionInS = NumCast(this.rootDoc.presentation_transition) / 1000; + if (this.slideDoc.presentation_transition) transitionInS = NumCast(this.slideDoc.presentation_transition) / 1000; else transitionInS = 0.5; - return this.rootDoc.presentation_movement === PresMovement.Jump || this.rootDoc.presentation_movement === PresMovement.None ? null : 'M: ' + transitionInS + 's'; + return this.slideDoc.presentation_movement === PresMovement.Jump || this.slideDoc.presentation_movement === PresMovement.None ? null : 'M: ' + transitionInS + 's'; } - private _itemRef: React.RefObject<HTMLDivElement> = React.createRef(); - private _dragRef: React.RefObject<HTMLDivElement> = React.createRef(); - private _titleRef: React.RefObject<EditableView> = React.createRef(); - @action headerDown = (e: React.PointerEvent<HTMLDivElement>) => { const element = e.target as any; e.stopPropagation(); e.preventDefault(); if (element && !(e.ctrlKey || e.metaKey || e.button === 2)) { - this.presBoxView?.regularSelect(this.rootDoc, this._itemRef.current!, this._dragRef.current!, true, false); + 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.rootDoc, this._itemRef.current!, this._dragRef.current!, e.shiftKey || e.ctrlKey || e.metaKey, e.ctrlKey || e.metaKey, e.shiftKey); + this.presBoxView?.modifierSelect(this.slideDoc, this._itemRef.current!, this._dragRef.current!, e.shiftKey || e.ctrlKey || e.metaKey, e.ctrlKey || e.metaKey, e.shiftKey); this.presBoxView?.activeItem && this.showRecording(this.presBoxView?.activeItem); }); } @@ -189,12 +191,12 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() { */ startDrag = (e: PointerEvent) => { const miniView: boolean = this.toolbarWidth <= 100; - const activeItem = this.rootDoc; + const activeItem = this.slideDoc; const dragArray = this.presBoxView?._dragArray ?? []; const dragData = new DragManager.DocumentDragData(this.presBoxView?.sortArray() ?? []); - if (!dragData.draggedDocuments.length) dragData.draggedDocuments.push(this.rootDoc); - dragData.treeViewDoc = this.presBox?._type_collection === CollectionViewType.Tree ? this.presBox : undefined; // this.props.DocumentView?.()?.props.treeViewDoc; - dragData.moveDocument = this.props.moveDocument; + if (!dragData.draggedDocuments.length) dragData.draggedDocuments.push(this.slideDoc); + dragData.treeViewDoc = this.presBox?._type_collection === CollectionViewType.Tree ? this.presBox : undefined; // this.DocumentView?.()?._props.treeViewDoc; + dragData.moveDocument = this._props.moveDocument; const dragItem: HTMLElement[] = []; const classesToRestore = new Map<HTMLElement, string>(); if (dragArray.length === 1) { @@ -238,7 +240,7 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() { }; onPointerMove = (e: PointerEvent) => { - const slide = this._itemRef.current!; + const slide = this._itemRef.current; const dragIsPresItem = DragManager.docsBeingDragged.some(d => d.presentation_targetDoc); if (slide && dragIsPresItem) { const rect = slide.getBoundingClientRect(); @@ -257,35 +259,42 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() { }; onPointerLeave = (e: any) => { - this._itemRef.current!.style.borderTop = '0px'; - this._itemRef.current!.style.borderBottom = '0px'; + const slide = this._itemRef.current; + if (slide) { + slide.style.borderTop = '0px'; + slide.style.borderBottom = '0px'; + } document.removeEventListener('pointermove', this.onPointerMove); }; @action toggleProperties = () => { - if (SettingsManager.propertiesWidth < 5) { - action(() => (SettingsManager.propertiesWidth = 250)); + if (SettingsManager.Instance.propertiesWidth < 5) { + SettingsManager.Instance.propertiesWidth = 250; } }; - removePresentationItem = undoable((e: React.MouseEvent) => { - e.stopPropagation(); - if (this.presBox && this.indexInPres < (this.presBoxView?.itemIndex || 0)) { - runInAction(() => (this.presBox!.itemIndex = (this.presBoxView?.itemIndex || 0) - 1)); - } - this.props.removeDocument?.(this.rootDoc); - this.presBoxView?.removeFromSelectedArray(this.rootDoc); - this.removeAllRecordingInOverlay(); - }, 'Remove doc from pres trail'); - - // set the value/title of the individual pres element - @undoBatch - @action - onSetValue = (value: string) => { - this.rootDoc.title = !value.trim().length ? '-untitled-' : value; - return true; - }; + removePresentationItem = undoable( + action((e: React.MouseEvent) => { + e.stopPropagation(); + if (this.presBox && this.indexInPres < (this.presBoxView?.itemIndex || 0)) { + this.presBox.itemIndex = (this.presBoxView?.itemIndex || 0) - 1; + } + this._props.removeDocument?.(this.slideDoc); + this.presBoxView?.removeFromSelectedArray(this.slideDoc); + this.removeAllRecordingInOverlay(); + }), + 'Remove doc from pres trail' + ); + + // set title of the individual pres slide + onSetValue = undoable( + action((value: string) => { + this.slideDoc.title = !value.trim().length ? '-untitled-' : value; + return true; + }), + 'set title of pres element' + ); /** * Method called for updating the view of the currently selected document @@ -294,7 +303,6 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() { * @param activeItem */ @undoBatch - @action updateCapturedContainerLayout = (presTargetDoc: Doc, activeItem: Doc) => { const targetDoc = DocCast(presTargetDoc.annotationOn) ?? presTargetDoc; activeItem.config_x = NumCast(targetDoc.x); @@ -306,130 +314,99 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() { // activeItem.config_pinLayout = true; }; - //wait i dont think i have to do anything here since by default it'll revert to the previously saved if I don't save - //so basically, don't have an onClick for this, just let it do nada for now - @undoBatch - @action - revertToPreviouslySaved = (presTargetDoc: Doc, activeItem: Doc) => { - const target = DocCast(activeItem.annotationOn) ?? activeItem; - PresBox.reversePin(activeItem, target); - }; - /** * Method called for updating the view of the currently selected document * * @param presTargetDoc * @param activeItem */ - @undoBatch - @action - updateCapturedViewContents = (presTargetDoc: Doc, activeItem: Doc) => { - const target = DocCast(presTargetDoc.annotationOn) ?? presTargetDoc; - PresBox.pinDocView(activeItem, { pinData: PresBox.pinDataTypes(target) }, target); - }; - - @computed get recordingIsInOverlay() { - return DocListCast(Doc.MyOverlayDocs.data).some(doc => doc.slides === this.rootDoc); - } + updateCapturedViewContents = undoable( + action((presTargetDoc: Doc, activeItem: Doc) => { + const target = DocCast(presTargetDoc.annotationOn) ?? presTargetDoc; + PresBox.pinDocView(activeItem, { pinData: PresBox.pinDataTypes(target) }, target); + }), + 'updated captured view contents' + ); // a previously recorded video will have timecode defined - static videoIsRecorded = (activeItem: Opt<Doc>) => { - const casted = Cast(activeItem?.recording, Doc, null); - return casted && 'layout_currentTimecode' in casted; - }; + static videoIsRecorded = (activeItem: Opt<Doc>) => 'layout_currentTimecode' in (DocCast(activeItem?.recording) ?? {}); - removeAllRecordingInOverlay = () => { - DocListCast(Doc.MyOverlayDocs.data) - .filter(doc => doc.slides === this.rootDoc) - .forEach(Doc.RemFromMyOverlay); - }; + removeAllRecordingInOverlay = () => Doc.MyOverlayDocs.filter(doc => doc.slides === this.slideDoc).forEach(Doc.RemFromMyOverlay); + /// remove all videos that have been recorded from overlay (leave videso that are being recorded to avoid losing data) static removeEveryExistingRecordingInOverlay = () => { - // Remove every recording that already exists in overlay view - DocListCast(Doc.MyOverlayDocs.data).forEach(doc => { - if (doc.slides !== null) { - // if it's a recording video, don't remove from overlay (user can lose data) - if (PresElementBox.videoIsRecorded(DocCast(doc.slides))) { - Doc.RemFromMyOverlay(doc); - } - } - }); - }; - - @undoBatch - @action - hideRecording = (e: React.MouseEvent, iconClick: boolean = false) => { - e.stopPropagation(); - this.removeAllRecordingInOverlay(); - // if (iconClick) PresElementBox.showVideo = false; - }; - - @undoBatch - @action - showRecording = (activeItem: Doc, iconClick: boolean = false) => { - // remove the overlays on switch *IF* not opened from the specific icon - if (!iconClick) PresElementBox.removeEveryExistingRecordingInOverlay(); - - if (activeItem.recording) { - Doc.AddToMyOverlay(DocCast(activeItem.recording)); - } + Doc.MyOverlayDocs.filter(doc => doc.slides !== null && PresElementBox.videoIsRecorded(DocCast(doc.slides))) // + .forEach(Doc.RemFromMyOverlay); }; - @undoBatch - @action - startRecording = (e: React.MouseEvent, activeItem: Doc) => { - e.stopPropagation(); - if (PresElementBox.videoIsRecorded(activeItem)) { - // if we already have an existing recording - this.showRecording(activeItem, true); - // // if we already have an existing recording - // Doc.AddToMyOverlay(Cast(activeItem.recording, Doc, null)); - } else { - // Remove every recording that already exists in overlay view - // this is a design decision to clear to focus in on the recoding mode - PresElementBox.removeEveryExistingRecordingInOverlay(); - - // if we dont have any recording - const recording = Docs.Create.WebCamDocument('', { - _width: 384, - _height: 216, - layout_hideDocumentButtonBar: true, - layout_hideDecorationTitle: true, - layout_hideOpenButton: true, - // hideDeleteButton: true, - cloneFieldFilter: new List<string>(['isSystem']), - }); - - // attach the recording to the slide, and attach the slide to the recording - recording.slides = activeItem; - activeItem.recording = recording; - - // make recording box appear in the bottom right corner of the screen - recording.overlayX = window.innerWidth - recording[Width]() - 20; - recording.overlayY = window.innerHeight - recording[Height]() - 20; - Doc.AddToMyOverlay(recording); - } - }; + hideRecording = undoable( + action((e: React.MouseEvent, iconClick: boolean = false) => { + e.stopPropagation(); + this.removeAllRecordingInOverlay(); + }), + 'hide video recording' + ); + + showRecording = undoable( + action((activeItem: Doc, iconClick: boolean = false) => { + // remove the overlays on switch *IF* not opened from the specific icon + if (!iconClick) PresElementBox.removeEveryExistingRecordingInOverlay(); + + activeItem.recording && Doc.AddToMyOverlay(DocCast(activeItem.recording)); + }), + 'show video recording' + ); + + startRecording = undoable( + action((e: React.MouseEvent, activeItem: Doc) => { + e.stopPropagation(); + if (PresElementBox.videoIsRecorded(activeItem)) { + // if we already have an existing recording + this.showRecording(activeItem, true); + // // if we already have an existing recording + // Doc.AddToMyOverlay(Cast(activeItem.recording, Doc, null)); + } else { + // we dont have any recording + // Remove every recording that already exists in overlay view + // this is a design decision to clear to focus in on the recoding mode + PresElementBox.removeEveryExistingRecordingInOverlay(); + + // create and add a recording to the slide + // make recording box appear in the bottom right corner of the screen + Doc.AddToMyOverlay( + (activeItem.recording = Docs.Create.WebCamDocument('', { + _width: 384, + _height: 216, + overlayX: window.innerWidth - 384 - 20, + overlayY: window.innerHeight - 216 - 20, + layout_hideDocumentButtonBar: true, + layout_hideDecorationTitle: true, + layout_hideOpenButton: true, + cloneFieldFilter: new List<string>(['isSystem']), + slides: activeItem, // attach the slide to the recording + })) + ); + } + }), + 'start video recording' + ); @undoBatch - @action lfg = (e: React.MouseEvent) => { e.stopPropagation(); // TODO: fix this bug - const { toggleChildrenRun } = this.rootDoc; - TreeView.ToggleChildrenRun.get(this.rootDoc)?.(); + const { toggleChildrenRun } = this.slideDoc; + TreeView.ToggleChildrenRun.get(this.slideDoc)?.(); - // call this.rootDoc.recurChildren() to get all the children + // call this.slideDoc.recurChildren() to get all the children // if (iconClick) PresElementBox.showVideo = false; }; @computed get toolbarWidth(): number { const presBoxDocView = DocumentManager.Instance.getDocumentView(this.presBox); - let width: number = NumCast(this.presBox?._width); - if (presBoxDocView) width = presBoxDocView.props.PanelWidth(); - if (width === 0) width = 300; - return width; + const width = NumCast(this.presBox?._width); + return presBoxDocView ? presBoxDocView._props.PanelWidth() : width ? width : 300; } // GPT @@ -439,8 +416,8 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() { const presBoxColor = StrCast(presBox?._backgroundColor); const presColorBool = presBoxColor ? presBoxColor !== Colors.WHITE && presBoxColor !== 'transparent' : false; const targetDoc = this.targetDoc; - const activeItem = this.rootDoc; - const hasChildren = BoolCast(this.rootDoc?.hasChildren); + const activeItem = this.slideDoc; + const hasChildren = BoolCast(this.slideDoc?.hasChildren); const items: JSX.Element[] = []; @@ -465,9 +442,9 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() { </Tooltip> ); items.push( - <Tooltip key="slash" title={<div className="dash-tooltip">{this.recordingIsInOverlay ? 'Hide Recording' : `${PresElementBox.videoIsRecorded(activeItem) ? 'Show' : 'Start'} recording`}</div>}> - <div className="slideButton" onClick={e => (this.recordingIsInOverlay ? this.hideRecording(e, true) : this.startRecording(e, activeItem))} style={{ fontWeight: 700 }}> - <FontAwesomeIcon icon={`video${this.recordingIsInOverlay ? '-slash' : ''}`} onPointerDown={e => e.stopPropagation()} /> + <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 }}> + <FontAwesomeIcon icon={`video${this.videoRecordingIsInOverlay ? '-slash' : ''}`} onPointerDown={e => e.stopPropagation()} /> </div> </Tooltip> ); @@ -480,8 +457,8 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() { {!activeItem.presentation_groupWithUp ? 'Not grouped with previous slide (click to group)' : activeItem.presentation_groupWithUp === 1 - ? 'Run simultaneously with previous slide (click again to run after)' - : 'Run after previous slide (click to ungroup from previous)'} + ? 'Run simultaneously with previous slide (click again to run after)' + : 'Run after previous slide (click to ungroup from previous)'} </div> }> <div @@ -503,14 +480,14 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() { ); } items.push( - <Tooltip key="eye" title={<div className="dash-tooltip">{this.rootDoc.presentation_expandInlineButton ? 'Minimize' : 'Expand'}</div>}> + <Tooltip key="eye" title={<div className="dash-tooltip">{this.slideDoc.presentation_expandInlineButton ? 'Minimize' : 'Expand'}</div>}> <div className="slideButton" onClick={e => { e.stopPropagation(); this.presExpandDocumentClick(); }}> - <FontAwesomeIcon icon={this.rootDoc.presentation_expandInlineButton ? 'eye-slash' : 'eye'} onPointerDown={e => e.stopPropagation()} /> + <FontAwesomeIcon icon={this.slideDoc.presentation_expandInlineButton ? 'eye-slash' : 'eye'} onPointerDown={e => e.stopPropagation()} /> </div> </Tooltip> ); @@ -554,30 +531,30 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() { } @computed get mainItem() { - const isSelected: boolean = this.selectedArray?.has(this.rootDoc) ? true : false; + const isSelected: boolean = this.selectedArray?.has(this.slideDoc) ? true : false; 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.rootDoc; + const activeItem: Doc = this.slideDoc; return ( <div - className={`presItem-container`} - key={this.props.Document[Id] + this.indexInPres} + className="presItem-container" + key={this.slideDoc[Id] + this.indexInPres} ref={this._itemRef} style={{ backgroundColor: presColorBool ? (isSelected ? 'rgba(250,250,250,0.3)' : 'transparent') : isSelected ? Colors.LIGHT_BLUE : 'transparent', opacity: this._dragging ? 0.3 : 1, - paddingLeft: NumCast(this.layoutDoc._xPadding, this.props.xPadding), - paddingRight: NumCast(this.layoutDoc._xPadding, this.props.xPadding), - paddingTop: NumCast(this.layoutDoc._yPadding, this.props.yPadding), - paddingBottom: NumCast(this.layoutDoc._yPadding, this.props.yPadding), + paddingLeft: NumCast(this.layoutDoc._xPadding, this._props.xPadding), + paddingRight: NumCast(this.layoutDoc._xPadding, this._props.xPadding), + paddingTop: NumCast(this.layoutDoc._yPadding, this._props.yPadding), + paddingBottom: NumCast(this.layoutDoc._yPadding, this._props.yPadding), }} onDoubleClick={action(e => { this.toggleProperties(); - this.presBoxView?.regularSelect(this.rootDoc, this._itemRef.current!, this._dragRef.current!, false); + this.presBoxView?.regularSelect(this.slideDoc, this._itemRef.current!, this._dragRef.current!, false); })} onPointerOver={this.onPointerOver} onPointerLeave={this.onPointerLeave} @@ -589,10 +566,10 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() { ) : ( <div ref={this._dragRef} - className={`presItem-slide ${isCurrent ? 'active' : ''}${this.rootDoc.runProcess ? ' testingv2' : ''}`} + className={`presItem-slide ${isCurrent ? 'active' : ''}${this.slideDoc.runProcess ? ' testingv2' : ''}`} style={{ display: 'infline-block', - backgroundColor: this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.BackgroundColor), + backgroundColor: this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.BackgroundColor), //layout_boxShadow: presBoxColor && presBoxColor !== 'white' && presBoxColor !== 'transparent' ? (isCurrent ? '0 0 0px 1.5px' + presBoxColor : undefined) : undefined, border: presBoxColor && presBoxColor !== 'white' && presBoxColor !== 'transparent' ? (isCurrent ? presBoxColor + ' solid 2.5px' : undefined) : undefined, }}> @@ -601,7 +578,7 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() { style={{ display: 'inline-flex', pointerEvents: isSelected ? undefined : 'none', - width: `calc(100% ${this.rootDoc.presentation_expandInlineButton ? '- 50%' : ''} - ${this.presButtons.length * 22}px`, + width: `calc(100% ${this.slideDoc.presentation_expandInlineButton ? '- 50%' : ''} - ${this.presButtons.length * 22}px`, cursor: isSelected ? 'text' : 'grab', }}> <div @@ -629,6 +606,6 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() { } render() { - return !(this.rootDoc instanceof Doc) || this.targetDoc instanceof Promise ? null : this.mainItem; + return !(this.slideDoc instanceof Doc) || this.targetDoc instanceof Promise ? null : this.mainItem; } } |
