diff options
| author | Sophie Zhang <sophie_zhang@brown.edu> | 2023-09-18 17:40:01 -0400 |
|---|---|---|
| committer | Sophie Zhang <sophie_zhang@brown.edu> | 2023-09-18 17:40:01 -0400 |
| commit | 013f25f01e729feee5db94900c61f4be4dd46869 (patch) | |
| tree | 765dd5f2e06d6217ca79438e1098cefc8da627bf /src/client/views/nodes | |
| parent | f5e765adff1e7b32250eb503c9724a4ac99117f3 (diff) | |
| parent | 84aa8806a62e2e957e8281d7d492139e3d8225f2 (diff) | |
Merge branch 'master' into sophie-report-manager
Diffstat (limited to 'src/client/views/nodes')
66 files changed, 5130 insertions, 1640 deletions
diff --git a/src/client/views/nodes/AudioBox.tsx b/src/client/views/nodes/AudioBox.tsx index 6558d215a..8d80f1364 100644 --- a/src/client/views/nodes/AudioBox.tsx +++ b/src/client/views/nodes/AudioBox.tsx @@ -1,14 +1,15 @@ 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 { observer } from 'mobx-react'; import { DateField } from '../../../fields/DateField'; -import { Doc, DocListCast } from '../../../fields/Doc'; +import { Doc } from '../../../fields/Doc'; import { ComputedField } from '../../../fields/ScriptField'; import { Cast, DateCast, NumCast } from '../../../fields/Types'; import { AudioField, nullAudio } from '../../../fields/URLField'; import { emptyFunction, formatTime, returnFalse, setupMoveUpEvents } from '../../../Utils'; -import { DocUtils } from '../../documents/Documents'; +import { Docs, DocUtils } from '../../documents/Documents'; import { Networking } from '../../Network'; import { DragManager } from '../../util/DragManager'; import { LinkManager } from '../../util/LinkManager'; @@ -18,6 +19,7 @@ import { ContextMenu } from '../ContextMenu'; import { ContextMenuProps } from '../ContextMenuItem'; import { ViewBoxAnnotatableComponent, ViewBoxAnnotatableProps } from '../DocComponent'; import './AudioBox.scss'; +import { DocFocusOptions } from './DocumentView'; import { FieldView, FieldViewProps } from './FieldView'; import { PinProps, PresBox } from './trails'; @@ -66,14 +68,14 @@ export class AudioBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp _stream: MediaStream | undefined; // passed to MediaRecorder, records device input audio _play: any = null; // timeout for playback - @observable _stackedTimeline: any; // CollectionStackedTimeline ref + @observable _stackedTimeline: CollectionStackedTimeline | null | undefined; // CollectionStackedTimeline ref @observable _finished: boolean = false; // has playback reached end of clip @observable _volume: number = 1; @observable _muted: boolean = false; @observable _paused: boolean = false; // is recording paused // @observable rawDuration: number = 0; // computed from the length of the audio element when loaded @computed get recordingStart() { - return DateCast(this.dataDoc[this.fieldKey + '-recordingStart'])?.date.getTime(); + return DateCast(this.dataDoc[this.fieldKey + '_recordingStart'])?.date.getTime(); } @computed get rawDuration() { return NumCast(this.dataDoc[`${this.fieldKey}_duration`]); @@ -134,16 +136,19 @@ export class AudioBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp } getAnchor = (addAsAnnotation: boolean, pinProps?: PinProps) => { - const anchor = - CollectionStackedTimeline.createAnchor( - this.rootDoc, - 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), - undefined, - undefined, - addAsAnnotation - ) || this.rootDoc; + const timecode = Cast(this.layoutDoc._layout_currentTimecode, 'number', null); + const anchor = addAsAnnotation + ? CollectionStackedTimeline.createAnchor( + this.rootDoc, + 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), + undefined, + undefined, + 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); return anchor; }; @@ -230,10 +235,10 @@ export class AudioBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp recordAudioAnnotation = async () => { this._stream = await navigator.mediaDevices.getUserMedia({ audio: true }); this._recorder = new MediaRecorder(this._stream); - this.dataDoc[this.fieldKey + '-recordingStart'] = new DateField(); + this.dataDoc[this.fieldKey + '_recordingStart'] = new DateField(); DocUtils.ActiveRecordings.push(this); this._recorder.ondataavailable = async (e: any) => { - const [{ result }] = await Networking.UploadFilesToServer({file: e.data}); + const [{ result }] = await Networking.UploadFilesToServer({ file: e.data }); if (!(result instanceof Error)) { this.props.Document[this.fieldKey] = new AudioField(result.accessPaths.agnostic.client); } @@ -359,9 +364,10 @@ export class AudioBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp 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)); - Doc.GetProto(newDoc).recordingSource = this.dataDoc; - Doc.GetProto(newDoc).recordingStart = ComputedField.MakeFunction(`self.recordingSource["${this.fieldKey}-recordingStart"]`); - Doc.GetProto(newDoc).mediaState = ComputedField.MakeFunction('self.recordingSource.mediaState'); + 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); @@ -417,7 +423,7 @@ export class AudioBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp }; // plays link - playLink = (link: Doc) => { + playLink = (link: Doc, options: DocFocusOptions) => { if (link.annotationOn === this.rootDoc) { if (!this.layoutDoc.dontAutoPlayFollowedLinks) { this.playFrom(this.timeline?.anchorStart(link) || 0, this.timeline?.anchorEnd(link)); @@ -472,8 +478,34 @@ export class AudioBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp }; // for trim button, double click displays full clip, single displays curr trim bounds + onResetPointerDown = (e: React.PointerEvent) => { + e.stopPropagation(); + this.timeline && + setupMoveUpEvents( + this, + e, + returnFalse, + returnFalse, + action(e => { + if (this.timeline?.IsTrimming !== TrimScope.None) { + this.timeline?.CancelTrimming(); + } else { + this.beginEndtime = this.timeline?.trimEnd; + this.beginStarttime = this.timeline?.trimStart; + this.startTrim(TrimScope.All); + } + }) + ); + }; + + beginEndtime: number | undefined; + beginStarttime: number | undefined; + + // for trim button, double click displays full clip, single displays curr trim bounds onClipPointerDown = (e: React.PointerEvent) => { e.stopPropagation(); + this.beginEndtime = this.timeline?.trimEnd; + this.beginStarttime = this.timeline?.trimStart; this.timeline && setupMoveUpEvents( this, @@ -524,7 +556,7 @@ export class AudioBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp r, (e, de) => { const [xp, yp] = this.props.ScreenToLocalTransform().transformPoint(de.x, de.y); - de.complete.docDragData && this.timeline.internalDocDrop(e, de, de.complete.docDragData, xp); + de.complete.docDragData && this.timeline?.internalDocDrop(e, de, de.complete.docDragData, xp); }, this.layoutDoc, undefined @@ -586,9 +618,22 @@ export class AudioBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp </div> {!this.miniPlayer && ( - <div className="audiobox-button" title={this.timeline?.IsTrimming !== TrimScope.None ? 'finish' : 'trim'} onPointerDown={this.onClipPointerDown}> - <FontAwesomeIcon icon={this.timeline?.IsTrimming !== TrimScope.None ? 'check' : 'cut'} size={'1x'} /> - </div> + <> + <Tooltip title={<>trim audio</>}> + <div className="audiobox-button" onPointerDown={this.onClipPointerDown}> + <FontAwesomeIcon icon={this.timeline?.IsTrimming !== TrimScope.None ? 'check' : 'cut'} size={'1x'} /> + </div> + </Tooltip> + {this.timeline?.IsTrimming == TrimScope.None && !NumCast(this.layoutDoc.clipStart) && NumCast(this.layoutDoc.clipEnd) === this.rawDuration ? ( + <></> + ) : ( + <Tooltip title={<>{this.timeline?.IsTrimming !== TrimScope.None ? 'Cancel trimming' : 'Edit original timeline'}</>}> + <div className="audiobox-button" onPointerDown={this.onResetPointerDown}> + <FontAwesomeIcon icon={this.timeline?.IsTrimming !== TrimScope.None ? 'cancel' : 'arrows-left-right'} size={'1x'} /> + </div> + </Tooltip> + )} + </> )} </div> <div className="controls-right"> @@ -653,12 +698,12 @@ export class AudioBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp @computed get renderTimeline() { return ( <CollectionStackedTimeline - ref={action((r: any) => (this._stackedTimeline = r))} + ref={action((r: CollectionStackedTimeline | null) => (this._stackedTimeline = r))} {...this.props} CollectionFreeFormDocumentView={undefined} dataFieldKey={this.fieldKey} fieldKey={this.annotationKey} - dictationKey={this.fieldKey + '-dictation'} + dictationKey={this.fieldKey + '_dictation'} mediaPath={this.path} renderDepth={this.props.renderDepth + 1} startTag={'_timecodeToShow' /* audioStart */} diff --git a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx index 6710cee63..4a438f826 100644 --- a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx +++ b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx @@ -40,7 +40,6 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF { key: 'opacity', val: 1 }, { key: '_currentFrame' }, { key: 'freeform_scale', val: 1 }, - { key: 'freeform_scale', val: 1 }, { key: 'freeform_panX' }, { key: 'freeform_panY' }, ]; // fields that are configured to be animatable using animation frames diff --git a/src/client/views/nodes/ColorBox.tsx b/src/client/views/nodes/ColorBox.tsx index aae759702..1b6fe5748 100644 --- a/src/client/views/nodes/ColorBox.tsx +++ b/src/client/views/nodes/ColorBox.tsx @@ -14,6 +14,8 @@ import { ActiveInkColor, ActiveInkWidth, SetActiveInkColor, SetActiveInkWidth } 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>() { @@ -81,3 +83,10 @@ export class ColorBox extends ViewBoxBaseComponent<FieldViewProps>() { ); } } + + +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.scss b/src/client/views/nodes/ComparisonBox.scss index a12f1c12b..39c864b2b 100644 --- a/src/client/views/nodes/ComparisonBox.scss +++ b/src/client/views/nodes/ComparisonBox.scss @@ -60,6 +60,8 @@ opacity: 0.8; pointer-events: all; cursor: pointer; + width: 15px; + height: 15px; } .clear-button.before { diff --git a/src/client/views/nodes/ComparisonBox.tsx b/src/client/views/nodes/ComparisonBox.tsx index ca5ec9389..b09fcd882 100644 --- a/src/client/views/nodes/ComparisonBox.tsx +++ b/src/client/views/nodes/ComparisonBox.tsx @@ -96,7 +96,7 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatabl const anchor = Docs.Create.ConfigDocument({ title: 'CompareAnchor:' + this.rootDoc.title, // set presentation timing properties for restoring view - presTransition: 1000, + presentation_transition: 1000, annotationOn: this.rootDoc, }); if (anchor) { @@ -109,12 +109,11 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatabl }; @undoBatch - clearDoc = (e: React.MouseEvent, fieldKey: string) => { - e.stopPropagation; // prevent click event action (slider movement) in registerSliding - delete this.dataDoc[fieldKey]; - }; + clearDoc = (fieldKey: string) => delete this.dataDoc[fieldKey]; + moveDoc = (doc: Doc, addDocument: (document: Doc | Doc[]) => boolean, which: string) => this.remDoc(doc, which) && addDocument(doc); addDoc = (doc: Doc, which: string) => { + if (this.dataDoc[which]) return false; this.dataDoc[which] = doc; return true; }; @@ -128,6 +127,24 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatabl whenChildContentsActiveChanged = action((isActive: boolean) => (this._isAnyChildContentActive = isActive)); + closeDown = (e: React.PointerEvent, which: string) => { + setupMoveUpEvents( + this, + e, + e => { + const de = new DragManager.DocumentDragData([DocCast(this.dataDoc[which])], 'move'); + de.moveDocument = (doc: Doc | Doc[], targetCollection: Doc | undefined, addDocument: (doc: Doc | Doc[]) => boolean): boolean => { + this.clearDoc(which); + return addDocument(doc); + }; + de.canEmbed = true; + DragManager.StartDocumentDrag([this._closeRef.current!], de, e.clientX, e.clientY); + return true; + }, + emptyFunction, + e => this.clearDoc(which) + ); + }; docStyleProvider = (doc: Opt<Doc>, props: Opt<DocumentViewProps>, property: string): any => { if (property === StyleProp.PointerEvents) return 'none'; return this.props.styleProvider?.(doc, props, property); @@ -136,13 +153,15 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatabl moveDoc2 = (doc: Doc | Doc[], targetCol: Doc | undefined, addDoc: any) => (doc instanceof Doc ? [doc] : doc).reduce((res, doc: Doc) => res && this.moveDoc(doc, addDoc, this.fieldKey + '_2'), true); remDoc1 = (doc: Doc | Doc[]) => (doc instanceof Doc ? [doc] : doc).reduce((res, doc) => res && this.remDoc(doc, this.fieldKey + '_1'), true); remDoc2 = (doc: Doc | Doc[]) => (doc instanceof Doc ? [doc] : doc).reduce((res, doc) => res && this.remDoc(doc, this.fieldKey + '_2'), true); + _closeRef = React.createRef<HTMLDivElement>(); render() { const clearButton = (which: string) => { return ( <div + ref={this._closeRef} className={`clear-button ${which}`} - onPointerDown={e => e.stopPropagation()} // prevent triggering slider movement in registerSliding - onClick={e => this.clearDoc(e, which)}> + onPointerDown={e => this.closeDown(e, which)} // prevent triggering slider movement in registerSliding + > <FontAwesomeIcon className={`clear-button ${which}`} icon="times" size="sm" /> </div> ); diff --git a/src/client/views/nodes/DataVizBox/DataVizBox.scss b/src/client/views/nodes/DataVizBox/DataVizBox.scss index cd500e9ae..385ef5a1b 100644 --- a/src/client/views/nodes/DataVizBox/DataVizBox.scss +++ b/src/client/views/nodes/DataVizBox/DataVizBox.scss @@ -1,4 +1,17 @@ .dataviz { - overflow: auto; + overflow: scroll; height: 100%; + width: 100%; + + .datatype-button{ + display: flex; + flex-direction: row; + } + .button-container { + pointer-events: unset; + } +} +.start-message { + margin: 10px; + align-self: baseline; } diff --git a/src/client/views/nodes/DataVizBox/DataVizBox.tsx b/src/client/views/nodes/DataVizBox/DataVizBox.tsx index d548ab9f1..8f32e2ba4 100644 --- a/src/client/views/nodes/DataVizBox/DataVizBox.tsx +++ b/src/client/views/nodes/DataVizBox/DataVizBox.tsx @@ -1,137 +1,182 @@ -import { action, computed, observable, ObservableMap, ObservableSet } from 'mobx'; +import { Toggle, ToggleType, Type } from 'browndash-components'; +import { action, computed, ObservableMap } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; -import { Doc, StrListCast } from '../../../../fields/Doc'; +import { Doc, Field, StrListCast } from '../../../../fields/Doc'; import { List } from '../../../../fields/List'; -import { Cast, CsvCast, NumCast, StrCast } from '../../../../fields/Types'; +import { Cast, CsvCast, StrCast } from '../../../../fields/Types'; import { CsvField } from '../../../../fields/URLField'; import { Docs } from '../../../documents/Documents'; import { ViewBoxAnnotatableComponent } from '../../DocComponent'; import { FieldView, FieldViewProps } from '../FieldView'; import { PinProps } from '../trails'; +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', LINECHART = 'lineChart', + HISTOGRAM = 'histogram', + PIECHART = 'pieChart', } @observer export class DataVizBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { - public static LayoutString(fieldKey: string) { - return FieldView.LayoutString(DataVizBox, fieldKey); + public static LayoutString(fieldStr: string) { + return FieldView.LayoutString(DataVizBox, fieldStr); } - // says we have an object and any string - // 2 ways of doing it - // @observable private pairs: { [key: string]: number | string | undefined }[] = []; - // @observable private pairs: { [key: string]: FieldResult }[] = []; - static pairSet = new ObservableMap<string, { [key: string]: string }[]>(); - @computed.struct get pairs() { - return DataVizBox.pairSet.get(CsvCast(this.rootDoc[this.fieldKey]).url.href); - } - private _chartRenderer: LineChart | undefined; - // // another way would be store a schema that defines the type of data we are expecting from an imported doc - - // method1() { - // this.pairs[0].x = 3; - // } - // method() { - // // this.pairs[0].x = 3; - // // go through the pairs - // const x = this.pairs[0].x; - // if (typeof x == 'number') { - // let x1 = Number(x); - // // let x1 = NumCast(x); - // } - // } + // 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; - // could use field result - // [key: string]: FieldResult; - // instead of numeric x,y in there, - - // TODO: nda - use onmousedown and onmouseup when dragging and changing height and width to update the height and width props only when dragging stops + // 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); + return records?.filter(record => Object.keys(record).some(key => record[key])); + } + // currently chosen visualization type: line, pie, histogram, table @computed get dataVizView(): DataVizView { - return StrCast(this.layoutDoc._dataVizView, 'table') as DataVizView; + return StrCast(this.layoutDoc._dataViz, 'table') as DataVizView; + } + + @computed get dataUrl() { + return Cast(this.dataDoc[this.fieldKey], CsvField); + } + @computed.struct get axes() { + return StrListCast(this.layoutDoc.dataViz_axes); } - @action + selectAxes = (axes: string[]) => (this.layoutDoc.dataViz_axes = new List<string>(axes)); + + @action // pinned / linked anchor doc includes selected rows, graph titles, and graph colors restoreView = (data: Doc) => { - const changedView = this.dataVizView !== data.presDataVizView && (this.layoutDoc._dataVizView = data.presDataVizView); - const changedAxes = this.axes.join('') !== StrListCast(data.presDataVizAxes).join('') && (this.layoutDoc._data_vizAxes = new List<string>(StrListCast(data.presDataVizAxes))); - const func = () => this._chartRenderer?.restoreView(data); + const changedView = this.dataVizView !== data.config_dataViz && (this.layoutDoc._dataViz = data.config_dataViz); + const changedAxes = this.axes.join('') !== StrListCast(data.config_dataVizAxes).join('') && (this.layoutDoc._dataViz_axes = new List<string>(StrListCast(data.config_dataVizAxes))); + this.layoutDoc.dataViz_selectedRows = Field.Copy(data.dataViz_selectedRows); + this.layoutDoc.dataViz_histogram_barColors = Field.Copy(data.dataViz_histogram_barColors); + this.layoutDoc.dataViz_histogram_defaultColor = data.dataViz_histogram_defaultColor; + this.layoutDoc.dataViz_pie_sliceColors = Field.Copy(data.dataViz_pie_sliceColors); + Object.keys(this.layoutDoc).map(key => { + if (key.startsWith('dataViz_histogram_title') || key.startsWith('dataViz_lineChart_title') || key.startsWith('dataViz_pieChart_title')) { + this.layoutDoc['_' + key] = data[key]; + } + }); + const func = () => this._vizRenderer?.restoreView(data); if (changedView || changedAxes) { setTimeout(func, 100); return true; } return func() ?? false; }; - getAnchor = (addAsAnnotation?: boolean, pinProps?: PinProps) => { const anchor = !pinProps ? this.rootDoc - : this._chartRenderer?.getAnchor(pinProps) ?? + : this._vizRenderer?.getAnchor(pinProps) ?? Docs.Create.ConfigDocument({ // 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*/ }); - - anchor.presDataVizView = this.dataVizView; - anchor.presDataVizAxes = this.axes.length ? new List<string>(this.axes) : undefined; - + anchor.config_dataViz = this.dataVizView; + anchor.config_dataVizAxes = this.axes.length ? new List<string>(this.axes) : undefined; + anchor.dataViz_selectedRows = Field.Copy(this.layoutDoc.dataViz_selectedRows); + anchor.dataViz_histogram_barColors = Field.Copy(this.layoutDoc.dataViz_histogram_barColors); + anchor.dataViz_histogram_defaultColor = this.layoutDoc.dataViz_histogram_defaultColor; + anchor.dataViz_pie_sliceColors = Field.Copy(this.layoutDoc.dataViz_pie_sliceColors); + Object.keys(this.layoutDoc).map(key => { + if (key.startsWith('dataViz_histogram_title') || key.startsWith('dataViz_lineChart_title') || key.startsWith('dataViz_pieChart_title')) { + anchor[key] = this.layoutDoc[key]; + } + }); this.addDocument(anchor); return anchor; }; - @computed.struct get axes() { - return StrListCast(this.layoutDoc.data_vizAxes); - } - selectAxes = (axes: string[]) => (this.layoutDoc.data_vizAxes = new List<string>(axes)); - - @computed get selectView() { - const width = this.props.PanelWidth() * 0.9; - const height = (this.props.PanelHeight() - 32) /* height of 'change view' button */ * 0.9; - const margin = { top: 10, right: 25, bottom: 50, left: 25 }; - if (!this.pairs) return 'no data'; - // prettier-ignore - switch (this.dataVizView) { - case DataVizView.TABLE: return <TableBox pairs={this.pairs} axes={this.axes} docView={this.props.DocumentView} selectAxes={this.selectAxes}/>; - case DataVizView.LINECHART: return <LineChart ref={r => (this._chartRenderer = r ?? undefined)} height={height} width={width} fieldKey={this.fieldKey} margin={margin} rootDoc={this.rootDoc} axes={this.axes} pairs={this.pairs} dataDoc={this.dataDoc} />; - } - } - @computed get dataUrl() { - return Cast(this.dataDoc[this.fieldKey], CsvField); - } - componentDidMount() { this.props.setContentView?.(this); - this.fetchData(); + if (!DataVizBox.dataset.has(CsvCast(this.rootDoc[this.fieldKey]).url.href)) this.fetchData(); } - fetchData() { - if (DataVizBox.pairSet.has(CsvCast(this.rootDoc[this.fieldKey]).url.href)) return; - DataVizBox.pairSet.set(CsvCast(this.rootDoc[this.fieldKey]).url.href, []); + fetchData = () => { + DataVizBox.dataset.set(CsvCast(this.rootDoc[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.pairSet.set(CsvCast(this.rootDoc[this.fieldKey]).url.href, res)))); - } + .then(res => res.json().then(action(res => !res.errno && DataVizBox.dataset.set(CsvCast(this.rootDoc[this.fieldKey]).url.href, res)))); + }; - // handle changing the view using a button - @action changeViewHandler(e: React.MouseEvent<HTMLButtonElement>) { - e.preventDefault(); - e.stopPropagation(); - this.layoutDoc._dataVizView = this.dataVizView === DataVizView.TABLE ? DataVizView.LINECHART : DataVizView.TABLE; - } + // toggles for user to decide which chart type to view the data in + renderVizView = () => { + const width = this.props.PanelWidth() * 0.9; + const height = (this.props.PanelHeight() - 32) /* height of 'change view' button */ * 0.9; + const margin = { top: 10, right: 25, bottom: 75, left: 45 }; + if (this.records) { + switch (this.dataVizView) { + case DataVizView.TABLE: + return <TableBox layoutDoc={this.layoutDoc} records={this.records} axes={this.axes} height={height} width={width} margin={margin} rootDoc={this.rootDoc} docView={this.props.DocumentView} selectAxes={this.selectAxes} />; + case DataVizView.LINECHART: + return ( + <LineChart + layoutDoc={this.layoutDoc} + ref={r => (this._vizRenderer = r ?? undefined)} + height={height} + width={width} + fieldKey={this.fieldKey} + margin={margin} + rootDoc={this.rootDoc} + axes={this.axes} + records={this.records} + dataDoc={this.dataDoc} + /> + ); + case DataVizView.HISTOGRAM: + return ( + <Histogram + layoutDoc={this.layoutDoc} + ref={r => (this._vizRenderer = r ?? undefined)} + height={height} + width={width} + fieldKey={this.fieldKey} + margin={margin} + rootDoc={this.rootDoc} + axes={this.axes} + records={this.records} + dataDoc={this.dataDoc} + /> + ); + case DataVizView.PIECHART: + return ( + <PieChart + layoutDoc={this.layoutDoc} + ref={r => (this._vizRenderer = r ?? undefined)} + height={height} + width={width} + fieldKey={this.fieldKey} + margin={margin} + rootDoc={this.rootDoc} + axes={this.axes} + records={this.records} + dataDoc={this.dataDoc} + /> + ); + } + } + return 'no data/visualization'; + }; render() { - return !this.pairs?.length ? ( - <div>Loading...</div> + 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" + style={{ + pointerEvents: this.props.isContentActive() === true ? "all" : "none" + }} onWheel={e => e.stopPropagation()} ref={r => r?.addEventListener( @@ -143,8 +188,13 @@ export class DataVizBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { { passive: false } ) }> - <button onClick={e => this.changeViewHandler(e)}>{this.dataVizView === DataVizView.TABLE ? DataVizView.LINECHART : DataVizView.TABLE}</button> - {this.selectView} + <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> + {this.renderVizView()} </div> ); } diff --git a/src/client/views/nodes/DataVizBox/components/Chart.scss b/src/client/views/nodes/DataVizBox/components/Chart.scss index d4f7bfb32..50dfe7f05 100644 --- a/src/client/views/nodes/DataVizBox/components/Chart.scss +++ b/src/client/views/nodes/DataVizBox/components/Chart.scss @@ -3,6 +3,55 @@ flex-direction: column; align-items: center; cursor: default; + margin-top: 10px; + overflow-y: visible; + + .graph{ + overflow: visible; + } + .graph-title{ + align-items: center; + font-size: larger; + display: flex; + flex-direction: row; + margin-top: -10px; + margin-bottom: -10px; + } + .asHistogram-checkBox { + // display: flex; + // flex-direction: row; + align-items: left; + align-self: left; + align-content: left; + justify-content: flex-end; + float: left; + left: 0; + position: relative; + margin-bottom: -35px; + } + .selected-data{ + align-items: center; + text-align: center; + display: flex; + flex-direction: row; + margin: 10px; + margin-top: -25px; + margin-bottom: 5px; + } + .slice { + &.hover { + stroke: black; + stroke-width: 2px; + } + } + + .histogram-bar{ + outline: thin solid black; + &.hover{ + outline: 3px solid black; + outline-offset: -3px; + } + } .tooltip { // make the height width bigger @@ -39,3 +88,22 @@ fill: red; } } +.tableBox { + display: flex; + flex-direction: column; +} +.table-container{ + overflow: scroll; + margin: 5px; + margin-left: 25px; + margin-right: 10px; + margin-bottom: 0; +} +.selectAll-buttons { + display: flex; + flex-direction: row; + justify-content: flex-end; + margin-top: 5px; + margin-right: 10px; + float: right; +}
\ No newline at end of file diff --git a/src/client/views/nodes/DataVizBox/components/Histogram.tsx b/src/client/views/nodes/DataVizBox/components/Histogram.tsx new file mode 100644 index 000000000..e67e2bf31 --- /dev/null +++ b/src/client/views/nodes/DataVizBox/components/Histogram.tsx @@ -0,0 +1,518 @@ +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 { observer } from 'mobx-react'; +import * as React from 'react'; +import { FaFillDrip } from 'react-icons/fa'; +import { Doc, NumListCast, StrListCast } from '../../../../../fields/Doc'; +import { List } from '../../../../../fields/List'; +import { listSpec } from '../../../../../fields/Schema'; +import { Cast, DocCast, StrCast } from '../../../../../fields/Types'; +import { Docs } from '../../../../documents/Documents'; +import { LinkManager } from '../../../../util/LinkManager'; +import { undoable } from '../../../../util/UndoManager'; +import { PinProps, PresBox } from '../../trails'; +import { scaleCreatorNumerical, yAxisCreator } from '../utils/D3Utils'; +import './Chart.scss'; + +export interface HistogramProps { + rootDoc: Doc; + layoutDoc: Doc; + axes: string[]; + records: { [key: string]: any }[]; + width: number; + height: number; + dataDoc: Doc; + fieldKey: string; + margin: { + top: number; + right: number; + bottom: number; + left: number; + }; +} + +@observer +export class Histogram extends React.Component<HistogramProps> { + private _disposers: { [key: string]: IReactionDisposer } = {}; + private _histogramRef: React.RefObject<HTMLDivElement> = React.createRef(); + private _histogramSvg: d3.Selection<SVGGElement, unknown, null, undefined> | undefined; + private numericalXData: boolean = false; // whether the data is organized by numbers rather than categoreis + private numericalYData: boolean = false; // whether the y axis is controlled by provided data rather than frequency + private maxBins = 15; // maximum number of bins that is readable on a normal sized doc + @observable _currSelected: any | undefined = undefined; // Object of selected bar + private curBarSelected: any = undefined; // histogram bin of selected bar + private selectedData: any = undefined; // Selection of selected bar + private hoverOverData: any = undefined; // Selection of bar being hovered over + + @computed get _tableDataIds() { + 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]); + } + // 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])) { + this.numericalXData = true; + } + return this._tableData.map(record => ({ [ax0]: record[this.props.axes[0]] })); + } + var ax0 = this.props.axes[0]; + var ax1 = this.props.axes[1]; + if (/\d/.test(this.props.records[0][ax0])) { + this.numericalXData = true; + } + 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]] })); + } + + @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) { + 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 + // .map(link => DocCast(link.link_anchor_1)); // then return the source of the link + } + + @computed get rangeVals(): { xMin?: number; xMax?: number; yMin?: number; yMax?: number } { + if (this.numericalXData) { + const data = this.data(this._histogramData); + return { xMin: Math.min.apply(null, data), xMax: Math.max.apply(null, data), yMin: 0, yMax: 0 }; + } + return { xMin: 0, xMax: 0, yMin: 0, yMax: 0 }; + } + + componentWillUnmount() { + Array.from(Object.keys(this._disposers)).forEach(key => this._disposers[key]()); + } + 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) => {}; + // create a document anchor that stores whatever is needed to reconstruct the viewing state (selection,zoom,etc) + getAnchor = (pinProps?: PinProps) => { + const anchor = Docs.Create.ConfigDocument({ + title: 'histogram doc selection' + this._currSelected, + }); + PresBox.pinDocView(anchor, { pinDocLayout: pinProps?.pinDocLayout, pinData: pinProps?.pinData }, this.props.rootDoc); + return anchor; + }; + + @computed get height() { + 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; + } + + // cleans data by converting numerical data to numbers and taking out empty cells + data = (dataSet: any) => { + var validData = dataSet.filter((d: { [x: string]: unknown }) => !Object.keys(dataSet[0]).some(key => !d[key] || Number.isNaN(d[key]))); + const field = dataSet[0] ? Object.keys(dataSet[0])[0] : undefined; + return !field + ? [] + : validData.map((d: { [x: string]: any }) => + !this.numericalXData // + ? d[field] + : +d[field!].replace(/\$/g, '').replace(/\%/g, '').replace(/\</g, '') + ); + }; + + // outlines the bar selected / hovered over + highlightSelectedBar = (changeSelectedVariables: boolean, svg: any, eachRectWidth: any, pointerX: any, xAxisTitle: any, yAxisTitle: any, histDataSet: any) => { + var sameAsCurrent: boolean; + var barCounter = -1; + const selected = svg.selectAll('.histogram-bar').filter((d: any) => { + barCounter++; // uses the order of bars and width of each bar to find which one the pointer is over + if (barCounter * eachRectWidth <= pointerX && pointerX <= (barCounter + 1) * eachRectWidth) { + var showSelected = this.numericalYData + ? this._histogramData.filter((data: { [x: string]: any }) => StrCast(data[xAxisTitle]).replace(/\$/g, '').replace(/\%/g, '').replace(/\</g, '') == d[0])[0] + : histDataSet.filter((data: { [x: string]: any }) => data[xAxisTitle].replace(/\$/g, '').replace(/\%/g, '').replace(/\</g, '') == d[0])[0]; + if (this.numericalXData) { + // calculating frequency + if (d[0] && d[1] && d[0] != d[1]) { + showSelected = { [xAxisTitle]: d3.min(d) + ' to ' + d3.max(d), frequency: d.length }; + } else if (!this.numericalYData) showSelected = { [xAxisTitle]: showSelected[xAxisTitle], frequency: d.length }; + } + if (changeSelectedVariables) { + // for when a bar is selected - not just hovered over + sameAsCurrent = this._currSelected ? showSelected[xAxisTitle] == this._currSelected![xAxisTitle] && showSelected[yAxisTitle] == this._currSelected![yAxisTitle] : false; + this._currSelected = sameAsCurrent ? undefined : showSelected; + this.selectedData = sameAsCurrent ? undefined : d; + } else this.hoverOverData = d; + return true; + } + return false; + }); + if (changeSelectedVariables) { + if (sameAsCurrent!) this.curBarSelected = undefined; + else this.curBarSelected = selected; + } + }; + + // draws the histogram + drawChart = (dataSet: any, width: number, height: number) => { + d3.select(this._histogramRef.current).select('svg').remove(); + d3.select(this._histogramRef.current).select('.tooltip').remove(); + + const data = this.data(dataSet); + const xAxisTitle = Object.keys(dataSet[0])[0]; + const yAxisTitle = this.numericalYData ? Object.keys(dataSet[0])[1] : 'frequency'; + const uniqueArr: unknown[] = [...new Set(data)]; + var numBins = this.numericalXData && Number.isInteger(data[0]) ? this.rangeVals.xMax! - this.rangeVals.xMin! : uniqueArr.length; + var translateXAxis = !this.numericalXData || numBins < this.maxBins ? width / (numBins + 1) / 2 : 0; + if (numBins > this.maxBins) numBins = this.maxBins; + const startingPoint = this.numericalXData ? this.rangeVals.xMin! : 0; + const endingPoint = this.numericalXData ? this.rangeVals.xMax! : numBins; + + // converts data into Objects + var histDataSet = dataSet.filter((d: { [x: string]: unknown }) => !Object.keys(dataSet[0]).some(key => !d[key] || Number.isNaN(d[key]))); + if (!this.numericalXData) { + var histStringDataSet: { [x: string]: unknown }[] = []; + if (this.numericalYData) { + for (let i = 0; i < dataSet.length; i++) { + histStringDataSet.push({ [yAxisTitle]: dataSet[i][yAxisTitle], [xAxisTitle]: dataSet[i][xAxisTitle] }); + } + } else { + for (let i = 0; i < uniqueArr.length; i++) { + histStringDataSet.push({ [yAxisTitle]: 0, [xAxisTitle]: uniqueArr[i] }); + } + for (let i = 0; i < data.length; i++) { + let barData = histStringDataSet.filter(each => each[xAxisTitle] == data[i]); + histStringDataSet.filter(each => each[xAxisTitle] == data[i])[0][yAxisTitle] = Number(barData[0][yAxisTitle]) + 1; + } + } + histDataSet = histStringDataSet; + } + + // initial graph and binning data for histogram + var svg = (this._histogramSvg = d3 + .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) + .append('g') + .attr('transform', 'translate(' + this.props.margin.left + ',' + this.props.margin.top + ')')); + var x = d3 + .scaleLinear() + .domain(this.numericalXData ? [startingPoint!, endingPoint!] : [0, numBins]) + .range([0, width]); + var histogram = d3 + .histogram() + .value(function (d) { + return d; + }) + .domain([startingPoint!, endingPoint!]) + .thresholds(x.ticks(numBins)); + var bins = histogram(data); + var eachRectWidth = width / bins.length; + var graphStartingPoint = bins[0].x1 && bins[1] ? bins[0].x1! - (bins[1].x1! - bins[1].x0!) : 0; + bins[0].x0 = graphStartingPoint; + x = x.domain([graphStartingPoint, endingPoint]).range([0, Number.isInteger(this.rangeVals.xMin!) ? width - eachRectWidth : width]); + var xAxis; + + // more calculations based on bins + // x-axis + if (!this.numericalXData) { + // reorganize to match data if the data is strings rather than numbers + // uniqueArr.sort() + histDataSet.sort(); + for (let i = 0; i < data.length; i++) { + var index = 0; + for (let j = 0; j < uniqueArr.length; j++) { + if (uniqueArr[j] == data[i]) { + index = j; + } + } + if (bins[index]) bins[index].push(data[i]); + } + bins.pop(); + eachRectWidth = width / bins.length; + bins.forEach(d => (d.x0 = d.x0!)); + xAxis = d3 + .axisBottom(x) + .ticks(bins.length > 1 ? bins.length - 1 : 1) + .tickFormat(i => uniqueArr[i.valueOf()] as string) + .tickPadding(10); + x.range([0, width - eachRectWidth]); + x.domain([0, bins.length - 1]); + translateXAxis = eachRectWidth / 2; + } else { + var allSame = true; + for (var i = 0; i < bins.length; i++) { + if (bins[i] && bins[i][0]) { + var compare = bins[i][0]; + for (let j = 1; j < bins[i].length; j++) { + if (bins[i][j] != compare) allSame = false; + } + } + } + if (allSame) { + translateXAxis = eachRectWidth / 2; + eachRectWidth = width / bins.length; + } else { + eachRectWidth = width / (bins.length + 1); + var tickDiff = bins.length >= 2 ? bins[bins.length - 2].x1! - bins[bins.length - 2].x0! : 0; + var curDomain = x.domain(); + x.domain([curDomain[0], curDomain[0] + tickDiff * bins.length]); + } + + xAxis = d3.axisBottom(x).ticks(bins.length - 1); + x.range([0, width - eachRectWidth]); + } + // y-axis + const maxFrequency = this.numericalYData + ? d3.max(histDataSet, function (d: any) { + return d[yAxisTitle] ? Number(d[yAxisTitle]!.replace(/\$/g, '').replace(/\%/g, '').replace(/\</g, '')) : 0; + }) + : d3.max(bins, function (d) { + return d.length; + }); + var y = d3.scaleLinear().range([height, 0]); + y.domain([0, +maxFrequency!]); + var yAxis = d3.axisLeft(y).ticks(maxFrequency!); + if (this.numericalYData) { + const yScale = scaleCreatorNumerical(0, Number(maxFrequency), height, 0); + yAxisCreator(svg.append('g'), width, yScale); + } else { + svg.append('g').call(yAxis); + } + svg.append('g') + .attr('transform', 'translate(' + translateXAxis + ', ' + height + ')') + .call(xAxis); + + // click/hover + const onPointClick = action((e: any) => this.highlightSelectedBar(true, svg, eachRectWidth, d3.pointer(e)[0], xAxisTitle, yAxisTitle, histDataSet)); + const onHover = action((e: any) => { + this.highlightSelectedBar(false, svg, eachRectWidth, d3.pointer(e)[0], xAxisTitle, yAxisTitle, histDataSet); + updateHighlights(); + }); + const mouseOut = action((e: any) => { + this.hoverOverData = undefined; + updateHighlights(); + }); + const updateHighlights = () => { + const hoverOverBar = this.hoverOverData; + const selectedData = this.selectedData; + svg.selectAll('rect').attr('class', function (d: any) { + return (hoverOverBar && hoverOverBar[0] == d[0]) || (selectedData && selectedData[0] == d[0]) ? 'histogram-bar hover' : 'histogram-bar'; + }); + }; + svg.on('click', onPointClick).on('mouseover', onHover).on('mouseout', mouseOut); + + // axis titles + svg.append('text') + .attr('transform', 'translate(' + width / 2 + ' ,' + (height + 40) + ')') + .style('text-anchor', 'middle') + .text(xAxisTitle); + svg.append('text') + .attr('transform', 'rotate(-90)' + ' ' + 'translate( 0, ' + -10 + ')') + .attr('x', -(height / 2)) + .attr('y', -20) + .style('text-anchor', 'middle') + .text(yAxisTitle); + d3.format('.0f'); + + // draw bars + var selected = this.selectedData; + svg.selectAll('rect') + .data(bins) + .enter() + .append('rect') + .attr( + 'transform', + this.numericalYData + ? function (d) { + const eachData = histDataSet.filter((data: { [x: string]: number }) => { + return data[xAxisTitle] == d[0]; + }); + const length = eachData.length ? eachData[0][yAxisTitle].replace(/\$/g, '').replace(/\%/g, '').replace(/\</g, '') : 0; + return 'translate(' + x(d.x0!) + ',' + y(length) + ')'; + } + : function (d) { + return 'translate(' + x(d.x0!) + ',' + y(d.length) + ')'; + } + ) + .attr( + 'height', + this.numericalYData + ? function (d) { + const eachData = histDataSet.filter((data: { [x: string]: number }) => { + return data[xAxisTitle] == d[0]; + }); + const length = eachData.length ? eachData[0][yAxisTitle].replace(/\$/g, '').replace(/\%/g, '').replace(/\</g, '') : 0; + return height - y(length); + } + : function (d) { + return height - y(d.length); + } + ) + .attr('width', eachRectWidth) + .attr( + 'class', + selected + ? function (d) { + return selected && selected[0] === d[0] ? 'histogram-bar hover' : 'histogram-bar'; + } + : function (d) { + return 'histogram-bar'; + } + ) + .attr('fill', d => { + var barColor; + const barColors = StrListCast(this.props.layoutDoc.dataViz_histogram_barColors).map(each => each.split('::')); + barColors.forEach(each => { + if (d[0] && d[0].toString() && each[0] == d[0].toString()) barColor = each[1]; + else { + const range = StrCast(each[0]).split(' to '); + 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); + }); + }; + + @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 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, '')); + + const barColors = Cast(this.props.layoutDoc.dataViz_histogram_barColors, listSpec('string'), null); + barColors.forEach(each => each.split('::')[0] === barName && barColors.splice(barColors.indexOf(each), 1)); + }; + + updateBarColors = () => { + var svg = this._histogramSvg; + if (svg) + svg.selectAll('rect').attr('fill', (d: any) => { + var barColor; + const barColors = StrListCast(this.props.layoutDoc.dataViz_histogram_barColors).map(each => each.split('::')); + barColors.forEach(each => { + if (d[0] && d[0].toString() && each[0] == d[0].toString()) barColor = each[1]; + else { + const range = StrCast(each[0]).split(' to '); + 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); + }); + }; + + render() { + this.updateBarColors(); + 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>(); + var selected = 'none'; + if (this._currSelected) { + curSelectedBarName = StrCast(this._currSelected![this.props.axes[0]].replace(/\$/g, '').replace(/\%/g, '').replace(/\</g, '')); + selected = '{ '; + Object.keys(this._currSelected).forEach(key => + key // + ? (selected += key + ': ' + this._currSelected[key] + ', ') + : '' + ); + selected = selected.substring(0, selected.length - 2) + ' }'; + } + var selectedBarColor; + 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"> + <div className="graph-title"> + <EditableText + val={StrCast(this.props.layoutDoc[titleAccessor])} + setVal={undoable( + action(val => (this.props.layoutDoc[titleAccessor] = val as string)), + 'Change Graph Title' + )} + color={'black'} + size={Size.LARGE} + fillWidth + /> + + <ColorPicker + 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')} + size={Size.XSMALL} + /> + </div> + <div ref={this._histogramRef} /> + {selected != 'none' ? ( + <div className={'selected-data'}> + Selected: {selected} + + <ColorPicker + tooltip={'Change Bar Color'} + type={Type.SEC} + icon={<FaFillDrip />} + selectedColor={selectedBarColor ? selectedBarColor : this.curBarSelected.attr('fill')} + setFinalColor={undoable(color => this.changeSelectedColor(color), 'Change Selected Bar Color')} + setSelectedColor={undoable(color => this.changeSelectedColor(color), 'Change Selected Bar Color')} + size={Size.XSMALL} + /> + + <IconButton + icon={<FontAwesomeIcon icon={'eraser'} />} + size={Size.XSMALL} + color={'black'} + type={Type.SEC} + tooltip={'Revert to the default bar color'} + onClick={undoable( + action(() => this.eraseSelectedColor()), + 'Change Selected Bar Color' + )} + /> + </div> + ) : null} + </div> + ) : ( + <span className="chart-container"> {'first use table view to select a column to graph'}</span> + ); + } + // when it is a brushed table and the incoming table doesn't have any rows selected + return <div className="chart-container">Selected rows of data from the incoming DataVizBox to display.</div>; + } +} diff --git a/src/client/views/nodes/DataVizBox/components/LineChart.tsx b/src/client/views/nodes/DataVizBox/components/LineChart.tsx index 6b564b0c9..3de7a0c4a 100644 --- a/src/client/views/nodes/DataVizBox/components/LineChart.tsx +++ b/src/client/views/nodes/DataVizBox/components/LineChart.tsx @@ -1,16 +1,15 @@ +import { EditableText, Size } from 'browndash-components'; +import * as d3 from 'd3'; import { action, computed, IReactionDisposer, observable, reaction } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; -// import d3 -import * as d3 from 'd3'; -import { Doc, DocListCast } from '../../../../../fields/Doc'; -import { Id } from '../../../../../fields/FieldSymbols'; +import { Doc, DocListCast, NumListCast, StrListCast } from '../../../../../fields/Doc'; import { List } from '../../../../../fields/List'; import { listSpec } from '../../../../../fields/Schema'; -import { Cast, DocCast } from '../../../../../fields/Types'; +import { Cast, DocCast, StrCast } from '../../../../../fields/Types'; import { Docs } from '../../../../documents/Documents'; import { DocumentManager } from '../../../../util/DocumentManager'; -import { LinkManager } from '../../../../util/LinkManager'; +import { undoable } from '../../../../util/UndoManager'; import { PinProps, PresBox } from '../../trails'; import { DataVizBox } from '../DataVizBox'; import { createLineGenerator, drawLine, minMaxRange, scaleCreatorNumerical, xAxisCreator, xGrid, yAxisCreator, yGrid } from '../utils/D3Utils'; @@ -20,13 +19,14 @@ export interface DataPoint { x: number; y: number; } -interface SelectedDataPoint extends DataPoint { +export interface SelectedDataPoint extends DataPoint { elem?: d3.Selection<d3.BaseType, unknown, SVGGElement, unknown>; } export interface LineChartProps { rootDoc: Doc; + layoutDoc: Doc; axes: string[]; - pairs: { [key: string]: any }[]; + records: { [key: string]: any }[]; width: number; height: number; dataDoc: Doc; @@ -47,24 +47,35 @@ export class LineChart extends React.Component<LineChartProps> { @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 + @computed get _tableDataIds() { + 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]); + } @computed get _lineChartData() { + var guids = StrListCast(this.props.layoutDoc.dataViz_rowIds); if (this.props.axes.length <= 1) return []; - return this.props.pairs - ?.filter(pair => (!this.incomingLinks.length ? true : Array.from(Object.keys(pair)).some(key => pair[key] && key.startsWith('select')))) - .map(pair => ({ x: Number(pair[this.props.axes[0]]), y: Number(pair[this.props.axes[1]]) })) - .sort((a, b) => (a.x < b.x ? -1 : 1)); + 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 incomingLinks() { - return LinkManager.Instance.getAllRelatedLinks(this.props.rootDoc) // out of all links - .filter(link => link.link_anchor_1 !== this.props.rootDoc) // 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 + @computed get graphTitle() { + return this.props.axes[1] + ' vs. ' + this.props.axes[0] + ' Line Chart'; } - @computed get incomingSelected() { - return this.incomingLinks // all links that are pointing to this node - .map(anchor => DocumentManager.Instance.getFirstDocumentView(anchor)?.ComponentView as DataVizBox) // get their data viz boxes - .filter(dvb => dvb) - .map(dvb => dvb.pairs?.filter(pair => pair['select' + dvb.rootDoc[Id]])) // get all the datapoints they have selected field set by incoming anchor - .lastElement(); + @computed get parentViz() { + return DocCast(this.props.rootDoc.dataViz_parentViz); + // return LinkManager.Instance.getAllRelatedLinks(this.props.rootDoc) // out of all links + // .filter(link => { + // return link.link_anchor_1 == this.props.rootDoc.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 + } + @computed get incomingHighlited() { + // return selected x and y axes + // otherwise, use the selection of whatever is linked to us + const incomingVizBox = DocumentManager.Instance.getFirstDocumentView(this.parentViz)?.ComponentView as DataVizBox; + const highlitedRowIds = NumListCast(incomingVizBox?.rootDoc?.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 } { return minMaxRange([this._lineChartData]); @@ -78,10 +89,6 @@ export class LineChart extends React.Component<LineChartProps> { ({ dataSet, w, h }) => { if (dataSet) { this.drawChart([dataSet], this.rangeVals, w, h); - // redraw annotations when the chart data has changed, or the local or inherited selection has changed - this.clearAnnotations(); - this._currSelected && this.drawAnnotations(Number(this._currSelected.x), Number(this._currSelected.y), true); - this.incomingSelected?.forEach((pair: any) => this.drawAnnotations(Number(pair[this.props.axes[0]]), Number(pair[this.props.axes[1]]))); } }, { fireImmediately: true } @@ -93,7 +100,7 @@ export class LineChart extends React.Component<LineChartProps> { // could be blue colored to make it look like anchor // this.drawAnnotations() // loop through annotations and draw them - annotations.forEach(a => this.drawAnnotations(Number(a.x), Number(a.y))); + // annotations.forEach(a => this.drawAnnotations(Number(a.x), Number(a.y))); // this.drawAnnotations(annotations.x, annotations.y); }, { fireImmediately: true } @@ -101,13 +108,13 @@ export class LineChart extends React.Component<LineChartProps> { this._disposers.highlights = reaction( () => ({ selected: this._currSelected, - incomingSelected: this.incomingSelected, + incomingHighlited: this.incomingHighlited, }), - ({ selected, incomingSelected }) => { + ({ selected, incomingHighlited }) => { // 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); - incomingSelected?.forEach((pair: any) => this.drawAnnotations(Number(pair[this.props.axes[0]]), Number(pair[this.props.axes[1]]))); + incomingHighlited?.forEach((record: any) => this.drawAnnotations(Number(record[this.props.axes[0]]), Number(record[this.props.axes[1]]))); }, { fireImmediately: true } ); @@ -143,13 +150,9 @@ export class LineChart extends React.Component<LineChartProps> { } }; - removeAnnotations(dataX: number, dataY: number) { - // loop through and remove any annotations that no longer exist - } - @action restoreView = (data: Doc) => { - const coords = Cast(data.presDataVizSelection, listSpec('number'), null); + const coords = Cast(data.config_dataVizSelection, listSpec('number'), null); if (coords?.length > 1 && (this._currSelected?.x !== coords[0] || this._currSelected?.y !== coords[1])) { this.setCurrSelected(coords[0], coords[1]); return true; @@ -167,8 +170,8 @@ 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.dataDoc); - anchor.presDataVizSelection = this._currSelected ? new List<number>([this._currSelected.x, this._currSelected.y]) : undefined; + PresBox.pinDocView(anchor, { pinDocLayout: pinProps?.pinDocLayout, pinData: pinProps?.pinData }, this.props.rootDoc); + anchor.config_dataVizSelection = this._currSelected ? new List<number>([this._currSelected.x, this._currSelected.y]) : undefined; return anchor; }; @@ -180,6 +183,14 @@ export class LineChart extends React.Component<LineChartProps> { 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) { + return ax0 + ' Line Chart'; + } else return ax1 + ' by ' + ax0 + ' Line Chart'; + } + setupTooltip() { return d3 .select(this._lineChartRef.current) @@ -197,9 +208,9 @@ export class LineChart extends React.Component<LineChartProps> { @action setCurrSelected(x?: number, y?: number) { // TODO: nda - get rid of svg element in the list? - this._currSelected = x !== undefined && y !== undefined ? { x, y } : undefined; - this.props.pairs.forEach(pair => pair[this.props.axes[0]] === x && pair[this.props.axes[1]] === y && (pair.selected = true)); - this.props.pairs.forEach(pair => (pair.selected = pair[this.props.axes[0]] === x && pair[this.props.axes[1]] === y ? true : undefined)); + if (this._currSelected && this._currSelected.x == x && this._currSelected.y == y) this._currSelected = undefined; + else this._currSelected = x !== undefined && y !== undefined ? { x, y } : undefined; + this.props.records.forEach(record => record[this.props.axes[0]] === x && record[this.props.axes[1]] === y && (record.selected = true)); } drawDataPoints(data: DataPoint[], idx: number, xScale: d3.ScaleLinear<number, number, never>, yScale: d3.ScaleLinear<number, number, never>) { @@ -219,7 +230,7 @@ export class LineChart extends React.Component<LineChartProps> { } // TODO: nda - can use d3.create() to create html element instead of appending - drawChart = (dataSet: DataPoint[][], rangeVals: { xMin?: number; xMax?: number; yMin?: number; yMax?: number }, width: number, height: number) => { + drawChart = (dataSet: any[][], rangeVals: { xMin?: number; xMax?: number; yMin?: number; yMax?: number }, width: number, height: number) => { // clearing tooltip and the current chart d3.select(this._lineChartRef.current).select('svg').remove(); d3.select(this._lineChartRef.current).select('.tooltip').remove(); @@ -238,6 +249,7 @@ export class LineChart extends React.Component<LineChartProps> { const svg = (this._lineChartSvg = d3 .select(this._lineChartRef.current) .append('svg') + .attr('class', 'graph') .attr('width', `${width + margin.left + margin.right}`) .attr('height', `${height + margin.top + margin.bottom}`) .append('g') @@ -249,13 +261,20 @@ export class LineChart extends React.Component<LineChartProps> { xAxisCreator(svg.append('g'), height, xScale); yAxisCreator(svg.append('g'), width, yScale); - // draw the plot line + // get valid data points const data = dataSet[0]; const lineGen = createLineGenerator(xScale, yScale); - drawLine(svg.append('path'), data, lineGen); - + var validData = data.filter(d => { + var valid = true; + Object.keys(data[0]).map(key => { + if (!d[key] || Number.isNaN(d[key])) valid = false; + }); + return valid; + }); + // draw the plot line + drawLine(svg.append('path'), validData, lineGen); // draw the datapoint circle - this.drawDataPoints(data, 0, xScale, yScale); + this.drawDataPoints(validData, 0, xScale, yScale); const higlightFocusPt = svg.append('g').style('display', 'none'); higlightFocusPt.append('circle').attr('r', 5).attr('class', 'circle'); @@ -293,6 +312,20 @@ export class LineChart extends React.Component<LineChartProps> { .on('mouseout', () => tooltip.transition().duration(300).style('opacity', 0)) .on('mousemove', mousemove) .on('click', onPointClick); + + // axis titles + svg.append('text') + .attr('transform', 'translate(' + width / 2 + ' ,' + (height + 40) + ')') + .style('text-anchor', 'middle') + .text(this.props.axes[0]); + svg.append('text') + .attr('transform', 'rotate(-90)' + ' ' + 'translate( 0, ' + -10 + ')') + .attr('x', -(height / 2)) + .attr('y', -20) + .attr('height', 20) + .attr('width', 20) + .style('text-anchor', 'middle') + .text(this.props.axes[1]); }; private updateTooltip( @@ -308,15 +341,40 @@ export class LineChart extends React.Component<LineChartProps> { tooltip .html(() => `<b>(${d0.x},${d0.y})</b>`) // text content for tooltip .style('pointer-events', 'none') - .style('transform', `translate(${xScale(d0.x) - this.width / 2}px,${yScale(d0.y) - 30}px)`); + .style('transform', `translate(${xScale(d0.x) - this.width}px,${yScale(d0.y)}px)`); } render() { - const selectedPt = this._currSelected ? `x: ${this._currSelected.x} y: ${this._currSelected.y}` : 'none'; - return ( - <div ref={this._lineChartRef} className="chart-container"> - <span> {this.props.axes.length < 2 ? 'first use table view to select two axes to plot' : `Selected: ${selectedPt}`}</span> - </div> - ); + 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._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"> + <div className="graph-title"> + <EditableText + val={StrCast(this.props.layoutDoc[titleAccessor])} + setVal={undoable( + action(val => (this.props.layoutDoc[titleAccessor] = val as string)), + 'Change Graph Title' + )} + color={'black'} + size={Size.LARGE} + fillWidth + /> + </div> + <div ref={this._lineChartRef} /> + {selectedPt != 'none' ? <div className={'selected-data'}> {`Selected: ${selectedPt}`}</div> : null} + </div> + ) : ( + <span className="chart-container"> {'first use table view to select two numerical axes to plot'}</span> + ); + } else + return ( + // when it is a brushed table and the incoming table doesn't have any rows selected + <div className="chart-container">Selected rows of data from the incoming DataVizBox to display.</div> + ); } } diff --git a/src/client/views/nodes/DataVizBox/components/PieChart.tsx b/src/client/views/nodes/DataVizBox/components/PieChart.tsx new file mode 100644 index 000000000..561f39141 --- /dev/null +++ b/src/client/views/nodes/DataVizBox/components/PieChart.tsx @@ -0,0 +1,401 @@ +import { ColorPicker, EditableText, Size, Type } from 'browndash-components'; +import * as d3 from 'd3'; +import { action, computed, IReactionDisposer, observable, reaction } from 'mobx'; +import { observer } from 'mobx-react'; +import * as React from 'react'; +import { FaFillDrip } from 'react-icons/fa'; +import { Doc, NumListCast, StrListCast } from '../../../../../fields/Doc'; +import { List } from '../../../../../fields/List'; +import { listSpec } from '../../../../../fields/Schema'; +import { Cast, DocCast, StrCast } from '../../../../../fields/Types'; +import { Docs } from '../../../../documents/Documents'; +import { undoable } from '../../../../util/UndoManager'; +import { PinProps, PresBox } from '../../trails'; +import './Chart.scss'; +import { Checkbox } from '@material-ui/core'; + +export interface PieChartProps { + rootDoc: Doc; + layoutDoc: Doc; + axes: string[]; + records: { [key: string]: any }[]; + width: number; + height: number; + dataDoc: Doc; + fieldKey: string; + margin: { + top: number; + right: number; + bottom: number; + left: number; + }; +} + +@observer +export class PieChart extends React.Component<PieChartProps> { + private _disposers: { [key: string]: IReactionDisposer } = {}; + private _piechartRef: React.RefObject<HTMLDivElement> = React.createRef(); + private _piechartSvg: d3.Selection<SVGGElement, unknown, null, undefined> | undefined; + private curSliceSelected: any = undefined; // d3 data of selected slice + private selectedData: any = undefined; // Selection of selected slice + private hoverOverData: any = undefined; // Selection of slice being hovered over + @observable _currSelected: any | undefined = undefined; // Object of selected slice + + @computed get _tableDataIds() { + 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]); + } + + // 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; + } + // 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 []; + + 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]] })); + } + + @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) { + 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 + // .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 = () => { + 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); + } + }, + { fireImmediately: true } + ); + }; + + @action + restoreView = (data: Doc) => {}; + // create a document anchor that stores whatever is needed to reconstruct the viewing state (selection,zoom,etc) + getAnchor = (pinProps?: PinProps) => { + const anchor = Docs.Create.ConfigDocument({ + // + title: 'piechart doc selection' + this._currSelected, + }); + PresBox.pinDocView(anchor, { pinDocLayout: pinProps?.pinDocLayout, pinData: pinProps?.pinData }, this.props.rootDoc); + return anchor; + }; + + @computed get height() { + 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; + } + + // cleans data by converting numerical data to numbers and taking out empty cells + data = (dataSet: any) => { + const validData = dataSet.filter((d: { [x: string]: unknown }) => !Object.keys(dataSet[0]).some(key => !d[key] || Number.isNaN(d[key]))); + const field = dataSet[0] ? Object.keys(dataSet[0])[0] : undefined; + return !field + ? undefined + : validData.map((d: { [x: string]: any }) => + this.byCategory + ? d[field] // + : +d[field].replace(/\$/g, '').replace(/\%/g, '').replace(/\#/g, '').replace(/\</g, '') + ); + }; + + // outlines the slice selected / hovered over + highlightSelectedSlice = (changeSelectedVariables: boolean, svg: any, arc: any, radius: any, pointer: any, pieDataSet: any) => { + var index = -1; + var sameAsCurrent: boolean; + const selected = svg.selectAll('.slice').filter((d: any) => { + index++; + var p1 = [0, 0]; // center of pie + var p3 = [arc.centroid(d)[0] * 2, arc.centroid(d)[1] * 2]; // outward peak of arc + var p2 = [radius * Math.sin(d.startAngle), -radius * Math.cos(d.startAngle)]; // start of arc + var p4 = [radius * Math.sin(d.endAngle), -radius * Math.cos(d.endAngle)]; // end of arc + + // draw an imaginary horizontal line from the pointer to see how many times it crosses a slice edge + var lineCrossCount = 0; + // if for all 4 lines + if (Math.min(p1[1], p2[1]) <= pointer[1] && pointer[1] <= Math.max(p1[1], p2[1])) { + // within y bounds + if (pointer[0] <= ((pointer[1] - p1[1]) * (p2[0] - p1[0])) / (p2[1] - p1[1]) + p1[0]) lineCrossCount++; + } // intercepts x + if (Math.min(p2[1], p3[1]) <= pointer[1] && pointer[1] <= Math.max(p2[1], p3[1])) { + if (pointer[0] <= ((pointer[1] - p2[1]) * (p3[0] - p2[0])) / (p3[1] - p2[1]) + p2[0]) lineCrossCount++; + } + if (Math.min(p3[1], p4[1]) <= pointer[1] && pointer[1] <= Math.max(p3[1], p4[1])) { + if (pointer[0] <= ((pointer[1] - p3[1]) * (p4[0] - p3[0])) / (p4[1] - p3[1]) + p3[0]) lineCrossCount++; + } + if (Math.min(p4[1], p1[1]) <= pointer[1] && pointer[1] <= Math.max(p4[1], p1[1])) { + if (pointer[0] <= ((pointer[1] - p4[1]) * (p1[0] - p4[0])) / (p1[1] - p4[1]) + p4[0]) lineCrossCount++; + } + if (lineCrossCount % 2 != 0) { + // inside the slice of it crosses an odd number of edges + var showSelected = this.byCategory ? pieDataSet[index] : this._pieChartData[index]; + if (changeSelectedVariables) { + // for when a bar is selected - not just hovered over + sameAsCurrent = this._currSelected + ? showSelected[Object.keys(showSelected)[0]] == this._currSelected![Object.keys(showSelected)[0]] && showSelected[Object.keys(showSelected)[1]] == this._currSelected![Object.keys(showSelected)[1]] + : this._currSelected === showSelected; + this._currSelected = sameAsCurrent ? undefined : showSelected; + this.selectedData = sameAsCurrent ? undefined : d; + } else this.hoverOverData = d; + return true; + } + return false; + }); + if (changeSelectedVariables) { + if (sameAsCurrent!) this.curSliceSelected = undefined; + else this.curSliceSelected = selected; + } + }; + + // draws the pie chart + drawChart = (dataSet: any, width: number, height: number) => { + d3.select(this._piechartRef.current).select('svg').remove(); + d3.select(this._piechartRef.current).select('.tooltip').remove(); + + var percentField = Object.keys(dataSet[0])[0]; + var descriptionField = Object.keys(dataSet[0])[1]!; + var radius = Math.min(width, height - this.props.margin.top - this.props.margin.bottom) / 2; + + // converts data into Objects + var data = this.data(dataSet); + var pieDataSet = dataSet.filter((d: { [x: string]: unknown }) => !Object.keys(dataSet[0]).some(key => !d[key] || Number.isNaN(d[key]))); + if (this.byCategory) { + let uniqueCategories = [...new Set(data)]; + var pieStringDataSet: { frequency: number }[] = []; + for (let i = 0; i < uniqueCategories.length; i++) { + pieStringDataSet.push({ frequency: 0, [percentField]: uniqueCategories[i] }); + } + for (let i = 0; i < data.length; i++) { + let sliceData = pieStringDataSet.filter((each: any) => each[percentField] == data[i]); + sliceData[0].frequency = sliceData[0].frequency + 1; + } + pieDataSet = pieStringDataSet; + percentField = Object.keys(pieDataSet[0])[0]; + descriptionField = Object.keys(pieDataSet[0])[1]!; + data = this.data(pieStringDataSet); + } + var trackDuplicates: { [key: string]: any } = {}; + data.forEach((eachData: any) => (!trackDuplicates[eachData] ? (trackDuplicates[eachData] = 0) : null)); + + // initial chart + var svg = (this._piechartSvg = d3 + .select(this._piechartRef.current) + .append('svg') + .attr('class', 'graph') + .attr('width', width + this.props.margin.right + this.props.margin.left) + .attr('height', height + this.props.margin.top + this.props.margin.bottom) + .append('g')); + let g = svg.append('g').attr('transform', 'translate(' + (width / 2 + this.props.margin.left) + ',' + height / 2 + ')'); + var pie = d3.pie(); + var arc = d3.arc().innerRadius(0).outerRadius(radius); + + // click/hover + const onPointClick = action((e: any) => this.highlightSelectedSlice(true, svg, arc, radius, d3.pointer(e), pieDataSet)); + const onHover = action((e: any) => { + this.highlightSelectedSlice(false, svg, arc, radius, d3.pointer(e), pieDataSet); + updateHighlights(); + }); + const mouseOut = action((e: any) => { + this.hoverOverData = undefined; + updateHighlights(); + }); + const updateHighlights = () => { + const hoverOverSlice = this.hoverOverData; + const selectedData = this.selectedData; + svg.selectAll('path').attr('class', function (d: any) { + return (selectedData && d.startAngle == selectedData.startAngle && d.endAngle == selectedData.endAngle) || (hoverOverSlice && d.startAngle == hoverOverSlice.startAngle && d.endAngle == hoverOverSlice.endAngle) + ? 'slice hover' + : 'slice'; + }); + }; + + // drawing the slices + var selected = this.selectedData; + var arcs = g.selectAll('arc').data(pie(data)).enter().append('g'); + const possibleDataPointVals: { [x: string]: any }[] = []; + pieDataSet.forEach((each: { [x: string]: any | { valueOf(): number } }) => { + var dataPointVal: { [x: string]: any } = {}; + dataPointVal[percentField] = each[percentField]; + if (descriptionField) dataPointVal[descriptionField] = each[descriptionField]; + try { + dataPointVal[percentField] = Number(dataPointVal[percentField].replace(/\$/g, '').replace(/\%/g, '').replace(/\#/g, '').replace(/\</g, '')); + } catch (error) {} + possibleDataPointVals.push(dataPointVal); + }); + const sliceColors = StrListCast(this.props.layoutDoc.dataViz_pie_sliceColors).map(each => each.split('::')); + arcs.append('path') + .attr('fill', (d, i) => { + var dataPoint; + const possibleDataPoints = possibleDataPointVals.filter((pval: any) => pval[percentField] === Number(d.data)); + if (possibleDataPoints.length == 1) dataPoint = possibleDataPoints[0]; + else { + dataPoint = possibleDataPoints[trackDuplicates[d.data.toString()]]; + trackDuplicates[d.data.toString()] = trackDuplicates[d.data.toString()] + 1; + } + var sliceColor; + if (dataPoint) { + const sliceTitle = dataPoint[this.props.axes[0]]; + const accessByName = StrCast(sliceTitle) ? StrCast(sliceTitle).replace(/\$/g, '').replace(/\%/g, '').replace(/\#/g, '').replace(/\</g, '') : sliceTitle; + sliceColors.forEach(each => each[0] == accessByName && (sliceColor = each[1])); + } + return sliceColor ? StrCast(sliceColor) : d3.schemeSet3[i] ? d3.schemeSet3[i] : d3.schemeSet3[i % d3.schemeSet3.length]; + }) + .attr( + 'class', + selected + ? function (d) { + return selected && d.startAngle == selected.startAngle && d.endAngle == selected.endAngle ? 'slice hover' : 'slice'; + } + : function (d) { + return 'slice'; + } + ) + .attr('d', arc) + .on('click', onPointClick) + .on('mouseover', onHover) + .on('mouseout', mouseOut); + + // adding labels + trackDuplicates = {}; + data.forEach((eachData: any) => (!trackDuplicates[eachData] ? (trackDuplicates[eachData] = 0) : null)); + arcs.size() < 100 && + arcs + .append('text') + .attr('transform', function (d) { + var centroid = arc.centroid(d as unknown as d3.DefaultArcObject); + var heightOffset = (centroid[1] / radius) * Math.abs(centroid[1]); + return 'translate(' + (centroid[0] + centroid[0] / (radius * 0.02)) + ',' + (centroid[1] + heightOffset) + ')'; + }) + .attr('text-anchor', 'middle') + .text(function (d) { + var dataPoint; + const possibleDataPoints = possibleDataPointVals.filter((pval: any) => pval[percentField] === Number(d.data)); + if (possibleDataPoints.length == 1) dataPoint = pieDataSet[possibleDataPointVals.indexOf(possibleDataPoints[0])]; + else { + dataPoint = pieDataSet[possibleDataPointVals.indexOf(possibleDataPoints[trackDuplicates[d.data.toString()]])]; + trackDuplicates[d.data.toString()] = trackDuplicates[d.data.toString()] + 1; + } + return dataPoint ? dataPoint[percentField]! + (!descriptionField ? '' : ' - ' + dataPoint[descriptionField])! : ''; + }); + }; + + @action changeSelectedColor = (color: string) => { + this.curSliceSelected.attr('fill', color); + const sliceTitle = this._currSelected[this.props.axes[0]]; + const sliceName = StrCast(sliceTitle) ? StrCast(sliceTitle).replace(/\$/g, '').replace(/\%/g, '').replace(/\#/g, '').replace(/\</g, '') : sliceTitle; + + const sliceColors = Cast(this.props.layoutDoc.dataViz_pie_sliceColors, listSpec('string'), null); + sliceColors.map(each => { + if (each.split('::')[0] == sliceName) sliceColors.splice(sliceColors.indexOf(each), 1); + }); + sliceColors.push(StrCast(sliceName + '::' + color)); + }; + + @action changeHistogramCheckBox = () => { + 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>(); + var selected: string; + var curSelectedSliceName = ''; + if (this._currSelected) { + 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 => { + key != '' ? (selected += key + ': ' + this._currSelected[key] + ', ') : ''; + }); + selected = selected.substring(0, selected.length - 2); + selected += ' }'; + } else selected = 'none'; + var selectedSliceColor; + 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"> + <div className="graph-title"> + <EditableText + val={StrCast(this.props.layoutDoc[titleAccessor])} + setVal={undoable( + action(val => (this.props.layoutDoc[titleAccessor] = val as string)), + 'Change Graph Title' + )} + color={'black'} + size={Size.LARGE} + fillWidth + /> + </div> + {this.props.axes.length === 1 && /\d/.test(this.props.records[0][this.props.axes[0]]) ? ( + <div className={'asHistogram-checkBox'} style={{ width: this.props.width }}> + <Checkbox color="primary" onChange={this.changeHistogramCheckBox} checked={this.props.layoutDoc.dataViz_pie_asHistogram as boolean} /> + Organize data as histogram + </div> + ) : null} + <div ref={this._piechartRef} /> + {selected != 'none' ? ( + <div className={'selected-data'}> + Selected: {selected} + + <ColorPicker + tooltip={'Change Slice Color'} + type={Type.SEC} + icon={<FaFillDrip />} + selectedColor={selectedSliceColor ? selectedSliceColor : this.curSliceSelected.attr('fill')} + setFinalColor={undoable(color => this.changeSelectedColor(color), 'Change Selected Slice Color')} + setSelectedColor={undoable(color => this.changeSelectedColor(color), 'Change Selected Slice Color')} + size={Size.XSMALL} + /> + </div> + ) : null} + </div> + ) : ( + <span className="chart-container"> {'first use table view to select a column to graph'}</span> + ); + } else + return ( + // when it is a brushed table and the incoming table doesn't have any rows selected + <div className="chart-container">Selected rows of data from the incoming DataVizBox to display.</div> + ); + } +} diff --git a/src/client/views/nodes/DataVizBox/components/TableBox.tsx b/src/client/views/nodes/DataVizBox/components/TableBox.tsx index d84e34d52..4d6027ca4 100644 --- a/src/client/views/nodes/DataVizBox/components/TableBox.tsx +++ b/src/client/views/nodes/DataVizBox/components/TableBox.tsx @@ -1,42 +1,112 @@ -import { action, computed } from 'mobx'; +import { Button, Type } from 'browndash-components'; +import { action, computed, IReactionDisposer, reaction } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; -import { Doc } from '../../../../../fields/Doc'; -import { Id } from '../../../../../fields/FieldSymbols'; +import { Doc, Field, NumListCast } from '../../../../../fields/Doc'; import { List } from '../../../../../fields/List'; -import { emptyFunction, returnFalse, setupMoveUpEvents, Utils } from '../../../../../Utils'; +import { listSpec } from '../../../../../fields/Schema'; +import { Cast, DocCast } from '../../../../../fields/Types'; +import { emptyFunction, setupMoveUpEvents, Utils } from '../../../../../Utils'; import { DragManager } from '../../../../util/DragManager'; import { DocumentView } from '../../DocumentView'; import { DataVizView } from '../DataVizBox'; +import './Chart.scss'; interface TableBoxProps { - pairs: { [key: string]: any }[]; + rootDoc: Doc; + layoutDoc: Doc; + records: { [key: string]: any }[]; selectAxes: (axes: string[]) => void; axes: string[]; + width: number; + height: number; + margin: { + top: number; + right: number; + bottom: number; + left: number; + }; docView?: () => DocumentView | undefined; } @observer export class TableBox extends React.Component<TableBoxProps> { + _inputChangedDisposer?: IReactionDisposer; + componentDidMount() { + // if the tableData changes (ie., when records are selected by the parent (input) visulization), + // then we need to remove any selected rows that are no longer part of the visualized dataset. + this._inputChangedDisposer = reaction(() => this._tableData.slice(), this.filterSelectedRowsDown, { fireImmediately: true }); + } + componentWillUnmount() { + this._inputChangedDisposer?.(); + } + @computed get _tableDataIds() { + 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]); + } + + @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 + // .map(link => DocCast(link.link_anchor_1)); // then return the source of the link + } + @computed get columns() { - return this.props.pairs.length ? Array.from(Object.keys(this.props.pairs[0])) : []; + return this._tableData.length ? Array.from(Object.keys(this._tableData[0])).filter(header => header != '' && header != undefined) : []; } + + // updates the 'dataViz_selectedRows' and 'dataViz_highlightedRows' fields to no longer include rows that aren't in the table + 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 + }; + render() { - return ( - <div className="table-container"> - <table className="table"> - <thead> - <tr className="table-row"> - {this.columns - .filter(col => !col.startsWith('select')) - .map(col => { - const header = React.createRef<HTMLElement>(); - return ( + if (this._tableData.length > 0) { + return ( + <div + className="tableBox" + tabIndex={0} + onKeyDown={e => { + if (this.props.layoutDoc && e.key === 'a' && (e.ctrlKey || e.metaKey)) { + e.stopPropagation(); + 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'} /> + </div> + <div + className="table-container" + style={{ height: this.props.height }} + 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 } + ) + }> + <table className="table"> + <thead> + <tr className="table-row"> + {this.columns.map(col => ( <th - ref={header as any} + key={this.columns.indexOf(col)} style={{ - color: this.props.axes.slice().reverse().lastElement() === col ? 'green' : this.props.axes.lastElement() === col ? 'red' : undefined, - fontWeight: this.props.axes.includes(col) ? 'bolder' : 'normal', + 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', }} onPointerDown={e => { const downX = e.clientX; @@ -45,24 +115,37 @@ 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 targetCreator = (annotationOn: Doc | undefined) => { const embedding = Doc.MakeEmbedding(this.props.docView?.()!.rootDoc!); - embedding._dataVizView = DataVizView.LINECHART; - embedding._data_vizAxes = new List<string>([col, col]); + embedding._dataViz = DataVizView.TABLE; + embedding._dataViz_axes = new List<string>([col, col]); + embedding._dataViz_parentViz = this.props.rootDoc; embedding.annotationOn = annotationOn; //this.props.docView?.()!.rootDoc!; + 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([header.current!], 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.annoDragData.linkSourceDoc.followLinkToggle = e.annoDragData.dropDocument.annotationOn === this.props.rootDoc; - // e.annoDragData.linkSourceDoc.followLinkZoom = false; - } - }, - }); + 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.followLinkZoom = false; + } + }, + } + ); return true; } return false; @@ -70,36 +153,66 @@ export class TableBox extends React.Component<TableBoxProps> { emptyFunction, action(e => { 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[0] = col; - } + 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); }) ); }}> {col} </th> - ); - })} - </tr> - </thead> - <tbody> - {this.props.pairs?.map((p, i) => { - return ( - <tr className="table-row" onClick={action(e => (p['select' + this.props.docView?.()?.rootDoc![Id]] = !p['select' + this.props.docView?.()?.rootDoc![Id]]))}> - {this.columns.map(col => ( - <td style={{ fontWeight: p['select' + this.props.docView?.()?.rootDoc![Id]] ? 'bold' : '' }}>{p[col]}</td> ))} </tr> - ); - })} - </tbody> - </table> - </div> - ); + </thead> + <tbody> + {this._tableDataIds + ?.map(rowId => ({ record: this.props.records[rowId], rowId })) + .map(({ record, rowId }) => ( + <tr + key={rowId} + className="table-row" + onClick={action(e => { + 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); + else highlited?.push(rowId); + if (!selected?.includes(rowId)) selected?.push(rowId); + } else { + // selecting a row + if (selected?.includes(rowId)) { + if (highlited?.includes(rowId)) highlited.splice(highlited.indexOf(rowId), 1); + selected.splice(selected.indexOf(rowId), 1); + } else selected?.push(rowId); + } + e.stopPropagation(); + })} + style={{ + background: NumListCast(this.props.layoutDoc.dataViz_highlitedRows).includes(rowId) ? 'lightYellow' : NumListCast(this.props.layoutDoc.dataViz_selectedRows).includes(rowId) ? 'lightgrey' : '', + width: '110%', + }}> + {this.columns.map(col => { + // each cell + 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' }}> + {record[col]} + </td> + ); + })} + </tr> + ))} + </tbody> + </table> + </div> + </div> + ); + } else + return ( + // when it is a brushed table and the incoming table doesn't have any rows selected + <div className="chart-container">Selected rows of data from the incoming DataVizBox to display.</div> + ); } } diff --git a/src/client/views/nodes/DataVizBox/utils/D3Utils.ts b/src/client/views/nodes/DataVizBox/utils/D3Utils.ts index e1ff6f8eb..10bfb0c64 100644 --- a/src/client/views/nodes/DataVizBox/utils/D3Utils.ts +++ b/src/client/views/nodes/DataVizBox/utils/D3Utils.ts @@ -34,7 +34,6 @@ export const createLineGenerator = (xScale: d3.ScaleLinear<number, number, never }; export const xAxisCreator = (g: d3.Selection<SVGGElement, unknown, null, undefined>, height: number, xScale: d3.ScaleLinear<number, number, never>) => { - console.log('x axis creator being called'); g.attr('class', 'x-axis').attr('transform', `translate(0,${height})`).call(d3.axisBottom(xScale).tickSize(15)); }; diff --git a/src/client/views/nodes/DocumentContentsView.tsx b/src/client/views/nodes/DocumentContentsView.tsx index 2d8663c9c..8ac9d6804 100644 --- a/src/client/views/nodes/DocumentContentsView.tsx +++ b/src/client/views/nodes/DocumentContentsView.tsx @@ -47,6 +47,7 @@ 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? @@ -271,6 +272,7 @@ export class DocumentContentsView extends React.Component< PhysicsSimulationBox, SchemaRowBox, ImportElementBox, + MapPushpinBox, }} bindings={bindings} jsx={layoutFrame} diff --git a/src/client/views/nodes/DocumentIcon.tsx b/src/client/views/nodes/DocumentIcon.tsx index 6e2ed72b8..bccbd66e8 100644 --- a/src/client/views/nodes/DocumentIcon.tsx +++ b/src/client/views/nodes/DocumentIcon.tsx @@ -9,6 +9,7 @@ import { action, observable } from 'mobx'; import { Id } from '../../../fields/FieldSymbols'; import { factory } from 'typescript'; import { LightboxView } from '../LightboxView'; +import { SettingsManager } from '../../util/SettingsManager'; @observer export class DocumentIcon extends React.Component<{ view: DocumentView; index: number }> { @@ -29,6 +30,7 @@ export class DocumentIcon extends React.Component<{ view: DocumentView; index: n pointerEvents: 'all', opacity: this._hovered ? 0.3 : 1, position: 'absolute', + background: SettingsManager.userBackgroundColor, transform: `translate(${(left + right) / 2}px, ${top}px)`, }}> <Tooltip title={<>{this.props.view.rootDoc.title}</>}> diff --git a/src/client/views/nodes/DocumentLinksButton.tsx b/src/client/views/nodes/DocumentLinksButton.tsx index 7723a088d..4db0bf5fa 100644 --- a/src/client/views/nodes/DocumentLinksButton.tsx +++ b/src/client/views/nodes/DocumentLinksButton.tsx @@ -18,10 +18,6 @@ import React = require('react'); import _ = require('lodash'); import { PinProps } from './trails'; -const higflyout = require('@hig/flyout'); -export const { anchorPoints } = higflyout; -export const Flyout = higflyout.default; - interface DocumentLinksButtonProps { View: DocumentView; Bottom?: boolean; diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx index 2990e2159..72c1a9806 100644 --- a/src/client/views/nodes/DocumentView.tsx +++ b/src/client/views/nodes/DocumentView.tsx @@ -4,7 +4,7 @@ import { observer } from 'mobx-react'; import { computedFn } from 'mobx-utils'; import { Bounce, Fade, Flip, LightSpeed, Roll, Rotate, Zoom } from 'react-reveal'; import { Doc, DocListCast, Field, Opt, StrListCast } from '../../../fields/Doc'; -import { AclPrivate, Animation, DocData, Width } from '../../../fields/DocSymbols'; +import { AclPrivate, Animation, AudioPlay, DocData, Width } from '../../../fields/DocSymbols'; import { Id } from '../../../fields/FieldSymbols'; import { InkTool } from '../../../fields/InkField'; import { List } from '../../../fields/List'; @@ -28,6 +28,7 @@ import { FollowLinkScript } from '../../util/LinkFollower'; import { LinkManager } from '../../util/LinkManager'; import { ScriptingGlobals } from '../../util/ScriptingGlobals'; 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'; @@ -47,7 +48,6 @@ import { DocumentLinksButton } from './DocumentLinksButton'; import './DocumentView.scss'; import { FieldViewProps } from './FieldView'; import { FormattedTextBox } from './formattedText/FormattedTextBox'; -import { KeyValueBox } from './KeyValueBox'; import { LinkAnchorBox } from './LinkAnchorBox'; import { PresEffect, PresEffectDirection } from './trails'; import { PinProps, PresBox } from './trails/PresBox'; @@ -100,6 +100,7 @@ export interface DocFocusOptions { 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 @@ -113,13 +114,14 @@ export interface DocComponentView { 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 }) => void; + brushView?: (view: { width: number; height: number; panX: number; panY: number }, transTime: number) => void; 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. shrinkWrap?: () => void; // requests a document to display all of its contents with no white space. currently only implemented (needed?) for freeform views 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 @@ -314,7 +316,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps @computed get titleHeight() { return this.props?.styleProvider?.(this.layoutDoc, this.props, StyleProp.TitleHeight) || 0; } - get pointerEvents(): 'none' | 'all' | 'visiblePainted' | undefined { + @computed get pointerEvents(): 'none' | 'all' | 'visiblePainted' | undefined { return this.props.styleProvider?.(this.Document, this.props, StyleProp.PointerEvents + (this.props.isSelected() ? ':selected' : '')); } @computed get finalLayoutKey() { @@ -428,7 +430,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps if (!this.Document.ignoreClick && this.pointerEvents !== 'none' && this.props.renderDepth >= 0 && Utils.isClick(e.clientX, e.clientY, this._downX, this._downY, this._downTime)) { let stopPropagate = true; let preventDefault = true; - (this.rootDoc._raiseWhenDragged === undefined ? DragManager.GetRaiseWhenDragged() : this.rootDoc._raiseWhenDragged) && this.props.bringToFront(this.rootDoc); + !this.rootDoc._keepZWhenDragged && this.props.bringToFront(this.rootDoc); if (this._doubleTap) { const defaultDblclick = this.props.defaultDoubleClick?.() || this.Document.defaultDoubleClick; if (this.onDoubleClickHandler?.script) { @@ -629,7 +631,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps const linkdrag = de.complete.annoDragData ?? de.complete.linkDragData; if (linkdrag) { linkdrag.linkSourceDoc = linkdrag.linkSourceGetAnchor(); - if (linkdrag.linkSourceDoc) { + if (linkdrag.linkSourceDoc && linkdrag.linkSourceDoc !== this.rootDoc) { if (de.complete.annoDragData && !de.complete.annoDragData.dropDocument) { de.complete.annoDragData.dropDocument = de.complete.annoDragData.dropDocCreator(undefined); } @@ -704,7 +706,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps if (e && !(e.nativeEvent as any).dash) { const onDisplay = () => { - 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.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. setTimeout(() => simulateMouseClick(document.elementFromPoint(e.clientX, e.clientY), e.clientX, e.clientY, e.screenX, e.screenY)); }; if (navigator.userAgent.includes('Macintosh')) { @@ -744,11 +746,11 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps 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._raiseWhenDragged !== false ? 'Keep ZIndex when dragged' : 'Allow ZIndex to change when dragged', - event: undoBatch(action(() => (this.rootDoc._raiseWhenDragged = this.rootDoc._raiseWhenDragged === undefined ? false : undefined))), + 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: 'compass' }); + !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' }); @@ -761,10 +763,10 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps !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' }); - onClicks.push({ description: 'Edit onClick Script', event: () => UndoManager.RunInBatch(() => DocUtils.makeCustomViewClicked(this.props.Document, undefined, 'onClick'), 'edit onClick'), icon: 'terminal' }); + !Doc.noviceMode && onClicks.push({ description: 'Edit onClick Script', event: () => UndoManager.RunInBatch(() => DocUtils.makeCustomViewClicked(this.props.Document, undefined, 'onClick'), 'edit onClick'), icon: 'terminal' }); !existingOnClick && cm.addItem({ description: 'OnClick...', noexpand: true, subitems: onClicks, icon: 'mouse-pointer' }); } else if (LinkManager.Links(this.Document).length) { - onClicks.push({ description: 'Select on Click', event: () => this.noOnClick(), icon: 'link' }); + 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' }); } @@ -798,7 +800,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps !more && moreItems.length && cm.addItem({ description: 'More...', subitems: moreItems, icon: 'compass' }); } const constantItems: ContextMenuProps[] = []; - if (!Doc.IsSystem(this.rootDoc)) { + 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) { @@ -806,7 +808,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps 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: 'layer-group' }); + constantItems.push({ description: 'Show Metadata', event: () => this.props.addDocTab(this.props.Document, (OpenWhere.addRight.toString() + 'KeyValue') as OpenWhere), icon: 'table-columns' }); cm.addItem({ description: 'General...', noexpand: false, subitems: constantItems, icon: 'question' }); const help = cm.findByDescription('Help...'); @@ -846,6 +848,10 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps documentationDescription = 'See text node documentation'; documentationLink = 'https://brown-dash.github.io/Dash-Documentation/documents/text/'; break; + case DocumentType.DATAVIZ: + documentationDescription = 'See DataViz node documentation'; + documentationLink = 'https://brown-dash.github.io/Dash-Documentation/documents/dataViz/'; + break; } // Add link to help documentation if (!this.props.treeViewDoc && documentationDescription && documentationLink) { @@ -862,28 +868,32 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps cm.displayMenu((e?.pageX || pageX || 0) - 15, (e?.pageY || pageY || 0) - 15); }; - rootSelected = (outsideReaction?: boolean) => this.props.isSelected(outsideReaction) || (this.props.Document.rootDocument && this.props.rootSelected?.(outsideReaction)) || false; + @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)); - isContentActive = (outsideReaction?: boolean): boolean | undefined => { + @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' || (this.rootDoc.pointerEvents === 'none' && !StrCast(this.props.LayoutTemplateString).includes(KeyValueBox.name)) + 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; - }; + } + isContentActive = (): boolean | undefined => this._isContentActive; @observable _retryThumb = 1; - thumbShown = () => { - const childHighlighted = () => - Array.from(Doc.highlightedDocs.keys()) - .concat(Array.from(Doc.brushManager.BrushedDoc.keys())) - .some(doc => Doc.AreProtosEqual(DocCast(doc.annotationOn), this.rootDoc)); + @computed get _thumbShown() { + const childHighlighted = () => false; + // Array.from(Doc.highlightedDocs.keys()) + // .concat(Array.from(Doc.brushManager.BrushedDoc.keys())) + // .some(doc => Doc.AreProtosEqual(DocCast(doc.annotationOn), this.rootDoc)); const childOverlayed = () => Array.from(DocumentManager._overlayViews).some(view => Doc.AreProtosEqual(view.rootDoc, this.rootDoc)); return !this.props.LayoutTemplateString && !this.isContentActive() && @@ -893,12 +903,15 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps ((!childHighlighted() && !childOverlayed() && !Doc.isBrushedHighlightedDegree(this.rootDoc)) || this.rootDoc._type_collection === CollectionViewType.Docking) ? true : false; - }; + } + thumbShown = () => this._thumbShown; 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 - contentPointerEvents = () => ((!this.disableClickScriptFunc && this.onClickHandler && !this.props.onBrowseClick?.() && this.isContentActive() !== true) || this.isContentActive() === false ? 'none' : this.pointerEvents); - + @computed get _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 = StrCast(this.layoutDoc.layout).includes(InkingStroke.name) && !this.props.LayoutTemplateString; @@ -954,6 +967,10 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps 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); + return filtered.some(link => link._link_displayArrow) ? 0 : undefined; + } } return this.props.styleProvider?.(doc, props, property); }; @@ -967,8 +984,8 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps TraceMobx(); return LinkManager.Instance.getAllRelatedLinks(this.rootDoc).filter( link => - Doc.AreProtosEqual(link.link_anchor_1 as Doc, this.rootDoc) || - Doc.AreProtosEqual(link.link_anchor_2 as Doc, this.rootDoc) || + (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)) ); @@ -982,7 +999,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps // the small blue dots that mark the endpoints of links TraceMobx(); if (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); + const filtered = DocUtils.FilterDocs(this.directLinks, this.props.childFilters?.() ?? [], []).filter(d => d.link_displayLine || Doc.UserDoc().showLinkLines); return filtered.map(link => ( <div className="documentView-anchorCont" key={link[Id]}> <DocumentView @@ -1014,9 +1031,9 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps audio: true, }) .then(function (stream) { - let audioTextAnnos = Cast(dataDoc[field + '-audioAnnotations-text'], listSpec('string'), null); + let audioTextAnnos = Cast(dataDoc[field + '_audioAnnotations_text'], listSpec('string'), null); if (audioTextAnnos) audioTextAnnos.push(''); - else audioTextAnnos = dataDoc[field + '-audioAnnotations-text'] = new List<string>(['']); + else audioTextAnnos = dataDoc[field + '_audioAnnotations_text'] = new List<string>(['']); DictationManager.Controls.listen({ interimHandler: value => (audioTextAnnos[audioTextAnnos.length - 1] = value), continuous: { indefinite: false }, @@ -1033,15 +1050,15 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps 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); + const audioAnnos = Cast(dataDoc[field + '_audioAnnotations'], listSpec(AudioField), null); if (audioAnnos === undefined) { - dataDoc[field + '-audioAnnotations'] = new List([audioField]); + dataDoc[field + '_audioAnnotations'] = new List([audioField]); } else { audioAnnos.push(audioField); } } }; - runInAction(() => (dataDoc.audioAnnoState = 'recording')); + //runInAction(() => (dataDoc.audioAnnoState = 'recording')); recorder.start(); const stopFunc = () => { recorder.stop(); @@ -1056,18 +1073,26 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps playAnnotation = () => { const self = this; const audioAnnoState = this.dataDoc.audioAnnoState ?? 'stopped'; - const audioAnnos = Cast(this.dataDoc[this.LayoutFieldKey + '-audioAnnotations'], listSpec(AudioField), null); + const audioAnnos = Cast(this.dataDoc[this.LayoutFieldKey + '_audioAnnotations'], listSpec(AudioField), null); const anno = audioAnnos?.lastElement(); - if (anno instanceof AudioField && audioAnnoState === 'stopped') { - 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'; + 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; + } } }; @@ -1081,9 +1106,11 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps <div className="documentView-captionWrapper" style={{ - pointerEvents: this.Document.ignoreClick ? 'none' : this.isContentActive() || this.props.isDocumentActive?.() ? 'all' : undefined, + pointerEvents: this.rootDoc.ignoreClick ? 'none' : this.isContentActive() || this.props.isDocumentActive?.() ? 'all' : undefined, minWidth: 50 * ffscale(), maxHeight: `max(100%, ${20 * ffscale()}px)`, + background: StrCast(this.layoutDoc._backgroundColor, 'rgba(0,0,0,0.2)'), + color: lightOrDark(StrCast(this.layoutDoc._backgroundColor, 'black')), }}> <FormattedTextBox {...this.props} @@ -1101,10 +1128,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps </div> ); const targetDoc = showTitle?.startsWith('_') ? this.layoutDoc : this.rootDoc; - const background = StrCast( - SharingManager.Instance.users.find(u => u.user.email === this.dataDoc.author)?.sharingDoc.headingColor, - Doc.UserDoc().layout_showTitle && [DocumentType.RTF, DocumentType.COL].includes(this.rootDoc.type as any) ? StrCast(Doc.SharingDoc().headingColor) : 'rgba(0,0,0,0.4)' - ); + const background = StrCast(SharingManager.Instance.users.find(u => u.user.email === this.dataDoc.author)?.sharingDoc.headingColor, StrCast(Doc.SharingDoc().headingColor, SettingsManager.userBackgroundColor)); const sidebarWidthPercent = +StrCast(this.layoutDoc.layout_sidebarWidthPercent).replace('%', ''); const titleView = !showTitle ? null : ( <div @@ -1114,7 +1138,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps position: this.headerMargin ? 'relative' : 'absolute', height: this.titleHeight, width: !this.headerMargin ? `calc(${sidebarWidthPercent || 100}% - 18px)` : (sidebarWidthPercent || 100) + '%', // leave room for annotation button - color: lightOrDark(background), + color: background === 'transparent' ? SettingsManager.userColor : lightOrDark(background), background, pointerEvents: (!this.disableClickScriptFunc && this.onClickHandler) || this.Document.ignoreClick ? 'none' : this.isContentActive() || this.props.isDocumentActive?.() ? 'all' : undefined, }}> @@ -1128,14 +1152,13 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps display={'block'} fontSize={10} GetValue={() => { - this.props.select(false); return showTitle.split(';').length === 1 ? showTitle + '=' + Field.toString(targetDoc[showTitle.split(';')[0]] as any as Field) : '#' + showTitle; }} SetValue={undoBatch((input: string) => { if (input?.startsWith('#')) { - if (this.props.layout_showTitle) { + if (this.rootDoc.layout_showTitle) { this.rootDoc._layout_showTitle = input?.substring(1) ? input.substring(1) : undefined; - } else { + } else if (!this.props.layout_showTitle) { Doc.UserDoc().layout_showTitle = input?.substring(1) ? input.substring(1) : 'author_date'; } } else { @@ -1193,7 +1216,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps * @returns a function that will wrap a JSX animation element wrapping any JSX element */ public static AnimationEffect(renderDoc: JSX.Element, presEffectDoc: Opt<Doc>, root: Doc) { - const dir = presEffectDoc?.presEffectDirection ?? presEffectDoc?.followLinkAnimDirection; + const dir = presEffectDoc?.presentation_effectDirection ?? presEffectDoc?.followLinkAnimDirection; const effectProps = { left: dir === PresEffectDirection.Left, right: dir === PresEffectDirection.Right, @@ -1201,10 +1224,10 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps bottom: dir === PresEffectDirection.Bottom, opposite: true, delay: 0, - duration: Cast(presEffectDoc?.presTransition, 'number', Cast(presEffectDoc?.followLinkTransitionTime, 'number', null)), + duration: Cast(presEffectDoc?.presentation_transition, 'number', Cast(presEffectDoc?.followLinkTransitionTime, 'number', null)), }; //prettier-ignore - switch (StrCast(presEffectDoc?.presEffect, StrCast(presEffectDoc?.followLinkAnimEffect))) { + switch (StrCast(presEffectDoc?.presentation_effect, StrCast(presEffectDoc?.followLinkAnimEffect))) { default: case PresEffect.None: return renderDoc; case PresEffect.Zoom: return <Zoom {...effectProps}>{renderDoc}</Zoom>; @@ -1216,16 +1239,22 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps case PresEffect.Lightspeed: return <LightSpeed {...effectProps}>{renderDoc}</LightSpeed>; } } + @computed get highlighting() { + return this.props.styleProvider?.(this.props.Document, this.props, StyleProp.Highlighting); + } + @computed get borderPath() { + return this.props.styleProvider?.(this.props.Document, this.props, StyleProp.BorderPath); + } render() { TraceMobx(); - const highlighting = this.props.styleProvider?.(this.props.Document, this.props, StyleProp.Highlighting); - const borderPath = this.props.styleProvider?.(this.props.Document, this.props, StyleProp.BorderPath); + 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.props.Document.isTemplateForField ? 'black 0.2vw 0.2vw 0.8vw' : undefined); + : 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', @@ -1241,9 +1270,9 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps onContextMenu={this.onContextMenu} onPointerDown={this.onPointerDown} onClick={this.onClick} - onPointerEnter={e => (!SnappingManager.GetIsDragging() || DragManager.CanEmbed) && Doc.BrushDoc(this.props.Document)} - onPointerOver={e => (!SnappingManager.GetIsDragging() || DragManager.CanEmbed) && Doc.BrushDoc(this.props.Document)} - onPointerLeave={e => !isParentOf(this.ContentDiv, document.elementFromPoint(e.nativeEvent.x, e.nativeEvent.y)) && Doc.UnBrushDoc(this.props.Document)} + 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, @@ -1260,7 +1289,13 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps @observer export class DocumentView extends React.Component<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; @@ -1343,7 +1378,7 @@ export class DocumentView extends React.Component<DocumentViewProps> { return this.docView?._componentView; } get allLinks() { - return this.docView?.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'; @@ -1526,7 +1561,7 @@ export class DocumentView extends React.Component<DocumentViewProps> { <div className="webBox-textHighlight"> <ObserverJsxParser autoCloseVoidElements={true} key={42} onError={(e: any) => console.log('PARSE error', e)} renderInWrapper={false} jsx={StrCast(this.textHtmlOverlay)} /> </div>, - { presEffect: this.htmlOverlayEffect ?? 'Zoom' } as any as Doc, + { presentation_effect: this.htmlOverlayEffect ?? 'Zoom' } as any as Doc, this.rootDoc )}{' '} </Fade> diff --git a/src/client/views/nodes/EquationBox.scss b/src/client/views/nodes/EquationBox.scss index 9714e1bd0..f5871db22 100644 --- a/src/client/views/nodes/EquationBox.scss +++ b/src/client/views/nodes/EquationBox.scss @@ -1,8 +1,9 @@ -@import "../global/globalCssVariables.scss"; +@import '../global/globalCssVariables.scss'; .equationBox-cont { - transform-origin: top left; + transform-origin: center; + background-color: #e7e7e7; > span { - width: 100%; + width: 100%; } -}
\ No newline at end of file +} diff --git a/src/client/views/nodes/FontIconBox/FontIconBox.tsx b/src/client/views/nodes/FontIconBox/FontIconBox.tsx index da1b89200..394108be4 100644 --- a/src/client/views/nodes/FontIconBox/FontIconBox.tsx +++ b/src/client/views/nodes/FontIconBox/FontIconBox.tsx @@ -1,26 +1,26 @@ import { IconProp } from '@fortawesome/fontawesome-svg-core'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { Button, ColorPicker, Dropdown, DropdownType, EditableText, IconButton, IListItemProps, NumberDropdown, NumberDropdownType, Popup, Size, Toggle, ToggleType, Type } from 'browndash-components'; +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 { observer } from 'mobx-react'; import * as React from 'react'; import { Doc, DocListCast, StrListCast } from '../../../../fields/Doc'; import { ScriptField } from '../../../../fields/ScriptField'; -import { BoolCast, Cast, NumCast, ScriptCast, StrCast } from '../../../../fields/Types'; +import { BoolCast, Cast, DocCast, NumCast, ScriptCast, StrCast } from '../../../../fields/Types'; +import { emptyFunction, setupMoveUpEvents, Utils } from '../../../../Utils'; import { CollectionViewType, DocumentType } from '../../../documents/DocumentTypes'; 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 { EditableView } from '../../EditableView'; -import { Colors } from '../../global/globalEnums'; +import { SelectedDocView } from '../../selectedDoc'; import { StyleProp } from '../../StyleProvider'; -import { FieldView, FieldViewProps } from '../FieldView'; import { OpenWhere } from '../DocumentView'; -import { RichTextMenu } from '../formattedText/RichTextMenu'; +import { FieldView, FieldViewProps } from '../FieldView'; import './FontIconBox.scss'; -import { SelectedDocView } from '../../selectedDoc'; -import { Utils } from '../../../../Utils'; +import TrailsIcon from './TrailsIcon'; export enum ButtonType { TextButton = 'textBtn', @@ -67,19 +67,6 @@ export class FontIconBox extends DocComponent<ButtonProps>() { } }; - static GetShowLabels() { - return BoolCast(Doc.UserDoc()._showLabel); - } - static SetShowLabels(show: boolean) { - Doc.UserDoc()._showLabel = show; - } - static GetRecognizeGestures() { - return BoolCast(Doc.UserDoc()._recognizeGestures); - } - static SetRecognizeGestures(show: boolean) { - Doc.UserDoc()._recognizeGestures = show; - } - // Determining UI Specs @computed get label() { return StrCast(this.rootDoc.icon_label, StrCast(this.rootDoc.title)); @@ -92,8 +79,7 @@ export class FontIconBox extends DocComponent<ButtonProps>() { else return null; } icon = StrCast(this.dataDoc[this.fieldKey ?? 'icon'] ?? this.dataDoc.icon, 'user') as any; - const trailsIcon = () => <img src={`/assets/${'presTrails.png'}`} style={{ width: 30, height: 30, filter: `invert(${color === Colors.DARK_GRAY ? '0%' : '100%'})` }} />; - return !icon ? null : icon === 'pres-trail' ? trailsIcon() : <FontAwesomeIcon className={`fontIconBox-icon-${this.type}`} icon={icon} color={color} />; + 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); @@ -140,11 +126,11 @@ export class FontIconBox extends DocComponent<ButtonProps>() { const color = this.props.styleProvider?.(this.rootDoc, 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))); - const label = !FontIconBox.GetShowLabels() ? null : <div className="fontIconBox-label">{this.label}</div>; return ( <NumberDropdown color={color} + background={SettingsManager.userBackgroundColor} numberDropdownType={type} showPlusMinus={false} tooltip={this.label} @@ -168,14 +154,14 @@ export class FontIconBox extends DocComponent<ButtonProps>() { return ( <div className={`menuButton ${this.type} ${active}`} - style={{ color: color, backgroundColor: backgroundColor, borderBottomLeftRadius: this.dropdown ? 0 : undefined }} + 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 || !FontIconBox.GetShowLabels() ? null : ( + {!this.label || !Doc.GetShowIconLabels() ? null : ( <div className="fontIconBox-label" style={{ color: color, backgroundColor: backgroundColor }}> {' '} {this.label}{' '} @@ -189,64 +175,69 @@ export class FontIconBox extends DocComponent<ButtonProps>() { ); } + 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; + }, + emptyFunction, + emptyFunction + ); + return false; + }; + /** * Dropdown list */ @computed get dropdownListButton() { - 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); - const script = ScriptCast(this.rootDoc.script); let noviceList: string[] = []; let text: string | undefined; - let dropdown = true; let getStyle: (val: string) => any = () => {}; let icon: IconProp = 'caret-down'; - let isViewDropdown: boolean = script?.script.originalScript.startsWith('setView'); - try { - if (isViewDropdown) { - const selectedDocs: Doc[] = SelectionManager.Docs(); - const selected = SelectionManager.Docs().lastElement(); - console.log('selected'); - if (selected) { - if (StrCast(selected.type) === DocumentType.COL) { - text = StrCast(selected._type_collection); - console.log('collection selected', text); + const isViewDropdown = script?.script.originalScript.startsWith('setView'); + if (isViewDropdown) { + const selected = SelectionManager.Docs(); + if (selected.lastElement()) { + if (StrCast(selected.lastElement().type) === DocumentType.COL) { + text = StrCast(selected.lastElement()._type_collection); + } else { + if (selected.length > 1) { + text = selected.length + ' selected'; } else { - console.log('doc selected', selected.title); - dropdown = false; - if (selectedDocs.length > 1) { - text = selectedDocs.length + ' selected'; - } else { - text = Utils.cleanDocumentType(StrCast(selected.type) as DocumentType); - icon = Doc.toIcon(selected); - } - return <Popup icon={<FontAwesomeIcon size={'1x'} icon={icon} />} text={text} type={Type.TERT} color={color} popup={<SelectedDocView selectedDocs={selectedDocs} />} fillWidth />; + text = Utils.cleanDocumentType(StrCast(selected.lastElement().type) as DocumentType); + icon = Doc.toIcon(selected.lastElement()); } - } else { - dropdown = false; - return <Button text={`None Selected`} type={Type.TERT} color={color} fillWidth inactive />; + return ( + <Popup + icon={<FontAwesomeIcon size={'1x'} icon={icon} />} + text={text} + type={Type.TERT} + color={SettingsManager.userColor} + background={SettingsManager.userVariantColor} + popup={<SelectedDocView selectedDocs={selected} />} + fillWidth + /> + ); } - noviceList = [CollectionViewType.Freeform, CollectionViewType.Schema, CollectionViewType.Stacking, CollectionViewType.NoteTaking]; } else { - text = StrCast((RichTextMenu.Instance?.TextView?.EditorView ? RichTextMenu.Instance : Doc.UserDoc()).fontFamily); - getStyle = (val: string) => { - return { fontFamily: val }; - }; + return <Button text="None Selected" type={Type.TERT} color={SettingsManager.userColor} background={SettingsManager.userVariantColor} fillWidth inactive />; } - } catch (e) { - console.log(e); + noviceList = [CollectionViewType.Freeform, CollectionViewType.Schema, CollectionViewType.Stacking, CollectionViewType.NoteTaking]; + } else { + text = script?.script.run({ this: this.layoutDoc, self: this.rootDoc, value: '', _readOnly_: true }).result; + // text = StrCast((RichTextMenu.Instance?.TextView?.EditorView ? RichTextMenu.Instance : Doc.UserDoc()).fontFamily); + getStyle = (val: string) => ({ fontFamily: val }); } - console.log('current item: ', text); - // Get items to place into the list const list: IListItemProps[] = this.buttonList .filter(value => !Doc.noviceMode || !noviceList.length || noviceList.includes(value)) .map(value => ({ - text: value.charAt(0).toUpperCase() + value.slice(1), + text: typeof value === 'string' ? value.charAt(0).toUpperCase() + value.slice(1) : StrCast(DocCast(value)?.title), val: value, style: getStyle(value), onClick: undoable(() => script.script.run({ this: this.layoutDoc, self: this.rootDoc, value }), value), @@ -256,10 +247,13 @@ export class FontIconBox extends DocComponent<ButtonProps>() { return ( <Dropdown selectedVal={text} - setSelectedVal={undoable(val => script.script.run({ this: this.layoutDoc, self: this.rootDoc, val }), `dropdown select ${this.label}`)} - color={color} - type={isViewDropdown ? Type.TERT : Type.PRIM} + setSelectedVal={undoable(value => script.script.run({ this: this.layoutDoc, self: this.rootDoc, value }), `dropdown select ${this.label}`)} + color={SettingsManager.userColor} + background={SettingsManager.userVariantColor} + type={Type.TERT} + closeOnSelect={false} dropdownType={DropdownType.SELECT} + onItemDown={this.dropdownItemDown} items={list} tooltip={this.label} fillWidth @@ -271,30 +265,66 @@ export class FontIconBox extends DocComponent<ButtonProps>() { return ScriptCast(this.rootDoc.script); } + colorBatch: UndoManager.Batch | undefined; /** * Color button */ @computed get colorButton() { const color = this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.Color); - const backgroundColor = this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.BackgroundColor); 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); return ( <ColorPicker setSelectedColor={value => { - const s = this.colorScript; - s && undoable(() => s.script.run({ this: this.layoutDoc, self: this.rootDoc, value: value, _readOnly_: false }).result, `Set ${tooltip} to ${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 }); + }} + setFinalColor={value => { + this.colorScript?.script.run({ this: this.layoutDoc, self: this.rootDoc, value: value, _readOnly_: false }); + this.colorBatch?.end(); + this.colorBatch = undefined; }} + defaultPickerType="Classic" selectedColor={curColor} type={Type.PRIM} color={color} + background={SettingsManager.userBackgroundColor} icon={this.Icon(color)!} tooltip={tooltip} label={this.label} /> ); } + @computed get multiToggleButton() { + // Determine the type of toggle button + const tooltip: string = StrCast(this.rootDoc.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; + // Colors + const color = this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.Color); + const items = DocListCast(this.rootDoc.data); + return ( + <MultiToggle + tooltip={`Toggle ${tooltip}`} + type={Type.PRIM} + color={color} + background={SettingsManager.userBackgroundColor} + label={this.label} + items={DocListCast(this.rootDoc.data).map(item => ({ + icon: <FontAwesomeIcon className={`fontIconBox-icon-${this.type}`} icon={StrCast(item.icon) as any} color={color} />, + tooltip: StrCast(item.toolTip), + val: StrCast(item.toolType), + }))} + selectedVal={StrCast(items.find(itemDoc => ScriptCast(itemDoc.onClick).script.run({ this: itemDoc, self: itemDoc, value: undefined, _readOnly_: true }).result)?.toolType)} + setSelectedVal={(val: string | number) => { + const itemDoc = items.find(item => item.toolType === val); + itemDoc && ScriptCast(itemDoc.onClick).script.run({ this: itemDoc, self: itemDoc, value: val, _readOnly_: false }); + }} + /> + ); + } @computed get toggleButton() { // Determine the type of toggle button @@ -315,6 +345,7 @@ export class FontIconBox extends DocComponent<ButtonProps>() { toggleStatus={toggleStatus} text={buttonText} color={color} + //background={SettingsManager.userBackgroundColor} icon={this.Icon(color)!} label={this.label} onPointerDown={() => script.script.run({ this: this.layoutDoc, self: this.rootDoc, value: !toggleStatus, _readOnly_: false })} @@ -354,52 +385,28 @@ export class FontIconBox extends DocComponent<ButtonProps>() { } render() { - // determine dash button metadata 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 onClickScript = ScriptCast(this.rootDoc.onClick); - const script = ScriptCast(this.rootDoc.script); - // TODO:glr Add label of button type - let button: JSX.Element = this.defaultButton; + const tooltip = StrCast(this.rootDoc.toolTip); + const scriptFunc = () => ScriptCast(this.rootDoc.onClick)?.script.run({ this: this.layoutDoc, self: this.rootDoc, _readOnly_: false }); + const btnProps = { tooltip, icon: this.Icon(color)!, label: this.label }; // prettier-ignore switch (this.type) { - case ButtonType.EditableText: - button = this.editableText; - break; - case ButtonType.DropdownList: - button = this.dropdownListButton; - break; - case ButtonType.ColorButton: - button = this.colorButton; - break; case ButtonType.NumberDropdownButton: case ButtonType.NumberInlineButton: - case ButtonType.NumberSliderButton: - button = this.numberDropdown; - break; - case ButtonType.DropdownButton: - button = this.dropdownButton; - break; - case ButtonType.ToggleButton: button = this.toggleButton; break; + case ButtonType.NumberSliderButton: return this.numberDropdown; + 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: - button = ( - <IconButton tooltip={tooltip} - color={color} icon={this.Icon(color)!} label={this.label}/> - ); - break; - case ButtonType.TextButton: - button = ( - <Button tooltip={tooltip} icon={this.Icon(color)!} text={StrCast(this.rootDoc.buttonText)} label={this.label}/> - ); - break; - case ButtonType.MenuButton: button = ( - <IconButton tooltip={tooltip} onPointerDown={() => onClickScript?.script.run({ this: this.layoutDoc, self: this.rootDoc, _readOnly_: false })} tooltipPlacement='right' size={Size.LARGE} color={color} icon={this.Icon(color)!} label={this.label}/> - ); - break; + 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)}/>; + case ButtonType.MenuButton: return <IconButton {...btnProps} color={color} + background={SettingsManager.userBackgroundColor} size={Size.LARGE} tooltipPlacement='right' onPointerDown={scriptFunc} />; } - - return button; + return this.defaultButton; } } diff --git a/src/client/views/nodes/FontIconBox/TrailsIcon.tsx b/src/client/views/nodes/FontIconBox/TrailsIcon.tsx new file mode 100644 index 000000000..09fd6e3ae --- /dev/null +++ b/src/client/views/nodes/FontIconBox/TrailsIcon.tsx @@ -0,0 +1,81 @@ +import * as React from 'react'; + +const TrailsIcon = (fill: string) => ( + <svg version="1.0" xmlns="http://www.w3.org/2000/svg" width="30" height="30" viewBox="0 0 1080.000000 1080.000000" preserveAspectRatio="xMidYMid meet"> + <g transform="translate(0.000000,1080.000000) scale(0.100000,-0.100000)" fill={fill} stroke="none"> + <path + d="M665 9253 c-74 -10 -157 -38 -240 -81 -74 -37 -107 -63 -186 -141 +-104 -104 -156 -191 -201 -334 l-23 -72 0 -3215 c0 -3072 1 -3218 18 -3280 10 +-36 39 -108 64 -160 40 -82 59 -107 142 -190 81 -81 111 -103 191 -143 52 -26 +122 -55 155 -65 57 -16 322 -17 4775 -20 3250 -2 4736 1 4784 8 256 39 486 +220 588 462 63 148 59 -96 56 3413 -3 3049 -4 3203 -21 3260 -78 260 -285 467 +-542 542 -57 17 -308 18 -4795 19 -2604 1 -4748 -1 -4765 -3z m9187 -787 c65 +-19 114 -60 143 -120 l25 -51 0 -2898 c0 -2582 -2 -2901 -15 -2934 -24 -57 +-62 -101 -108 -126 l-42 -22 -4435 -3 c-3954 -2 -4440 0 -4481 13 -26 9 -63 +33 -87 56 -79 79 -72 -205 -72 3012 0 2156 3 2889 12 2918 20 70 91 136 168 +160 14 4 2010 8 4436 8 3710 1 4418 -1 4456 -13z" + /> + <path + d="M7692 7839 c-46 -14 -109 -80 -122 -128 -7 -27 -9 -472 -8 -1443 l3 +-1403 24 -38 c13 -21 42 -50 64 -65 l41 -27 816 0 816 0 41 27 c22 15 51 44 +64 65 l24 38 0 1425 0 1425 -24 38 c-13 21 -42 50 -64 65 l-41 27 -800 2 +c-488 1 -814 -2 -834 -8z" + /> + <path + d="M1982 7699 c-46 -14 -109 -80 -122 -128 -7 -27 -10 -308 -8 -893 l3 +-853 24 -38 c13 -21 42 -50 64 -65 l41 -27 1386 0 1386 0 41 27 c22 15 51 44 +64 65 l24 38 0 876 0 875 -27 41 c-15 22 -44 51 -65 64 l-38 24 -1370 2 c-847 +1 -1383 -2 -1403 -8z" + /> + <path + d="M6413 7093 c-13 -2 -23 -9 -23 -15 0 -24 21 -307 26 -343 l5 -40 182 +-1 c200 -1 307 -15 484 -65 57 -16 107 -29 112 -29 5 0 36 75 69 168 33 92 63 +175 67 184 6 14 -10 22 -92 48 -126 39 -308 76 -447 89 -106 11 -337 13 -383 +4z" + /> + <path + d="M5840 7033 c-63 -8 -238 -29 -388 -47 -150 -18 -274 -35 -276 -37 -2 +-2 8 -89 23 -194 22 -163 29 -190 44 -193 10 -2 91 6 180 17 89 12 258 32 376 +46 118 14 216 27 218 28 7 8 -43 391 -52 392 -5 1 -62 -4 -125 -12z" + /> + <path + d="M4762 4789 c-46 -14 -109 -80 -122 -128 -7 -27 -10 -323 -8 -943 l3 +-903 24 -38 c13 -21 42 -50 64 -65 l41 -27 926 0 926 0 41 27 c22 15 51 44 64 +65 l24 38 0 926 0 925 -27 41 c-15 22 -44 51 -65 64 l-38 24 -910 2 c-557 1 +-923 -2 -943 -8z" + /> + <path + d="M8487 4297 c-26 -215 -161 -474 -307 -585 -27 -20 -49 -40 -49 -44 +-1 -3 49 -79 110 -167 l110 -161 44 31 c176 126 333 350 418 594 30 86 77 282 +77 320 0 8 -57 19 -167 34 -93 13 -182 25 -199 28 -31 5 -31 5 -37 -50z" + /> + <path + d="M3965 4233 c-106 -9 -348 -36 -415 -47 -55 -8 -75 -15 -74 -26 1 -20 +56 -374 59 -377 1 -2 46 4 101 12 159 24 409 45 526 45 l108 0 0 200 0 200 +-132 -2 c-73 -1 -151 -3 -173 -5z" + /> + <path + d="M3020 4079 c-85 -23 -292 -94 -368 -125 -97 -40 -298 -140 -305 -151 +-5 -7 172 -315 192 -336 4 -4 41 10 82 32 103 55 272 123 414 165 66 20 125 +38 132 41 11 4 -4 70 -78 348 -10 39 -14 41 -69 26z" + /> + <path + d="M6955 3538 c-21 -91 -74 -362 -72 -364 7 -7 260 -44 367 -54 146 -13 +359 -13 475 0 49 6 90 12 91 13 2 1 -12 90 -29 197 -26 155 -36 194 -47 192 +-8 -2 -85 -6 -170 -9 -160 -6 -357 7 -505 33 -103 18 -104 18 -110 -8z" + /> + <path + d="M1993 3513 c-52 -67 -71 -106 -98 -198 -35 -122 -44 -284 -21 -415 9 +-51 18 -96 21 -98 4 -5 360 79 375 88 7 4 7 24 0 60 -21 109 -7 244 31 307 +l20 31 -146 131 c-80 72 -147 131 -149 131 -2 0 -17 -17 -33 -37z" + /> + <path + d="M2210 2519 c-91 -50 -166 -92 -168 -94 -2 -1 11 -26 28 -54 l32 -51 +244 0 c134 0 244 2 244 5 0 3 -23 33 -51 67 -28 35 -72 98 -97 140 -26 43 -51 +77 -57 77 -5 0 -84 -41 -175 -90z" + /> + </g> + </svg> +); + +export default TrailsIcon; diff --git a/src/client/views/nodes/FunctionPlotBox.tsx b/src/client/views/nodes/FunctionPlotBox.tsx index 61711417f..40f48dafe 100644 --- a/src/client/views/nodes/FunctionPlotBox.tsx +++ b/src/client/views/nodes/FunctionPlotBox.tsx @@ -48,8 +48,8 @@ export class FunctionPlotBox extends ViewBoxAnnotatableComponent<FieldViewProps> annotationOn: this.rootDoc, }); PresBox.pinDocView(anchor, { pinDocLayout: pinProps?.pinDocLayout, pinData: { ...(pinProps?.pinData ?? {}), datarange: true } }, this.rootDoc); - anchor.presXRange = new List<number>(Array.from(this._plot.options.xAxis.domain)); - anchor.presYRange = new List<number>(Array.from(this._plot.options.yAxis.domain)); + anchor.config_xRange = new List<number>(Array.from(this._plot.options.xAxis.domain)); + anchor.config_yRange = new List<number>(Array.from(this._plot.options.yAxis.domain)); if (addAsAnnotation) this.addDocument(anchor); return anchor; }; diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx index d763753a5..1ab120af5 100644 --- a/src/client/views/nodes/ImageBox.tsx +++ b/src/client/views/nodes/ImageBox.tsx @@ -22,7 +22,6 @@ import { DocumentType } from '../../documents/DocumentTypes'; import { Networking } from '../../Network'; import { DocumentManager } from '../../util/DocumentManager'; import { DragManager } from '../../util/DragManager'; -import { SnappingManager } from '../../util/SnappingManager'; import { undoBatch } from '../../util/UndoManager'; import { ContextMenu } from '../../views/ContextMenu'; import { CollectionFreeFormView } from '../collections/collectionFreeForm/CollectionFreeFormView'; @@ -37,7 +36,6 @@ import { FieldView, FieldViewProps } from './FieldView'; import './ImageBox.scss'; import { PinProps, PresBox } from './trails'; import React = require('react'); -import Color = require('color'); export const pageSchema = createSchema({ googlePhotosUrl: 'string', @@ -56,6 +54,17 @@ export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp public static LayoutString(fieldKey: string) { return FieldView.LayoutString(ImageBox, fieldKey); } + + @observable public static imageRootDoc: Doc | undefined; + @observable public static imageEditorOpen: boolean = false; + @observable public static imageEditorSource: string = ''; + @observable public static addDoc: ((doc: Doc | Doc[], annotationKey?: string) => boolean) | undefined; + @action public static setImageEditorOpen(open: boolean) { + ImageBox.imageEditorOpen = open; + } + @action public static setImageEditorSource(source: string) { + ImageBox.imageEditorSource = source; + } private _ignoreScroll = false; private _forcedScroll = false; private _dropDisposer?: DragManager.DragDropDisposer; @@ -76,20 +85,20 @@ export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp }; getAnchor = (addAsAnnotation: boolean, pinProps?: PinProps) => { + const visibleAnchor = this._getAnchor?.(this._savedAnnotations, false); // use marquee anchor, otherwise, save zoom/pan as anchor const anchor = - this._getAnchor?.(this._savedAnnotations, false) ?? // use marquee anchor, otherwise, save zoom/pan as anchor + visibleAnchor ?? Docs.Create.ConfigDocument({ title: 'ImgAnchor:' + this.rootDoc.title, - presPanX: NumCast(this.layoutDoc._freeform_panX), - presPanY: NumCast(this.layoutDoc._freeform_panY), - presViewScale: Cast(this.layoutDoc._freeform_scale, 'number', null), - presTransition: 1000, + 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, }); if (anchor) { if (!addAsAnnotation) anchor.backgroundColor = 'transparent'; - /* addAsAnnotation &&*/ this.addDocument(anchor); - PresBox.pinDocView(anchor, { pinDocLayout: pinProps?.pinDocLayout, pinData: { ...(pinProps?.pinData ?? {}), pannable: true } }, this.rootDoc); + addAsAnnotation && this.addDocument(anchor); + PresBox.pinDocView(anchor, { pinDocLayout: pinProps?.pinDocLayout, pinData: { ...(pinProps?.pinData ?? {}), pannable: visibleAnchor ? false : true } }, this.rootDoc); return anchor; } return this.rootDoc; @@ -222,13 +231,13 @@ export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp croppingProto['data_nativeWidth'] = anchw; croppingProto['data_nativeHeight'] = anchh; croppingProto.freeform_scale = viewScale; - croppingProto.freeform_scaleMin = viewScale; + croppingProto.freeform_scale_min = viewScale; croppingProto.freeform_panX = anchx / viewScale; croppingProto.freeform_panY = anchy / viewScale; - croppingProto.freeform_panXMin = anchx / viewScale; - croppingProto.freeform_panXMax = anchw / viewScale; - croppingProto.freeform_panYMin = anchy / viewScale; - croppingProto.freeform_panYMax = anchh / viewScale; + croppingProto.freeform_panX_min = anchx / viewScale; + croppingProto.freeform_panX_max = anchw / viewScale; + croppingProto.freeform_panY_min = anchy / viewScale; + 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](); @@ -248,6 +257,16 @@ export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp funcs.push({ description: `Show ${this.layoutDoc._showFullRes ? 'Dynamic Res' : 'Full Res'}`, event: this.resolution, icon: 'expand' }); funcs.push({ description: 'Set Native Pixel Size', event: this.setNativeSize, icon: 'expand-arrows-alt' }); funcs.push({ description: 'Copy path', event: () => Utils.CopyText(this.choosePath(field.url)), icon: 'copy' }); + funcs.push({ + description: 'Open Image Editor', + event: action(() => { + ImageBox.setImageEditorOpen(true); + ImageBox.setImageEditorSource(this.choosePath(field.url)); + ImageBox.addDoc = this.props.addDocument; + ImageBox.imageRootDoc = this.rootDoc; + }), + icon: 'pencil-alt', + }); if (!Doc.noviceMode) { funcs.push({ description: 'Export to Google Photos', event: () => GooglePhotos.Transactions.UploadImages([this.props.Document]), icon: 'caret-square-right' }); @@ -289,13 +308,14 @@ export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp @computed private get url() { const data = Cast(this.dataDoc[this.fieldKey], ImageField); - return data ? data.url.href : undefined; + return data ? data.url?.href : undefined; } choosePath(url: URL) { + if (!url?.href) return ''; const lower = url.href.toLowerCase(); if (url.protocol === 'data') return url.href; - if (url.href.indexOf(window.location.origin) === -1 && url.href.indexOf("dashblobstore") === -1) return Utils.CorsProxy(url.href); + if (url.href.indexOf(window.location.origin) === -1 && url.href.indexOf('dashblobstore') === -1) return Utils.CorsProxy(url.href); if (!/\.(png|jpg|jpeg|gif|webp)$/.test(lower)) return `/assets/unknown-file-icon-hi.png`; const ext = extname(url.href); @@ -320,7 +340,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp if (!(data instanceof ImageField)) { return null; } - const primary = data.url.href; + const primary = data.url?.href; if (primary.includes(window.location.origin)) { return null; } @@ -414,7 +434,8 @@ export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp @computed get content() { TraceMobx(); - const backAlpha = DashColor(this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.BackgroundColor)).alpha(); + const backColor = DashColor(this.props.styleProvider?.(this.rootDoc, 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(); const { nativeWidth, nativeHeight, nativeOrientation } = this.nativeSize; @@ -482,7 +503,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp this._marqueeing = undefined; this.props.select(false); }; - focus = (anchor: Doc, options: DocFocusOptions) => this._ffref.current?.focus(anchor, options); + focus = (anchor: Doc, options: DocFocusOptions) => (anchor.type === DocumentType.CONFIG ? undefined : this._ffref.current?.focus(anchor, options)); _ffref = React.createRef<CollectionFreeFormView>(); savedAnnotations = () => this._savedAnnotations; diff --git a/src/client/views/nodes/KeyValueBox.tsx b/src/client/views/nodes/KeyValueBox.tsx index a6712a3db..673f711be 100644 --- a/src/client/views/nodes/KeyValueBox.tsx +++ b/src/client/views/nodes/KeyValueBox.tsx @@ -4,7 +4,7 @@ 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, NumCast } from '../../../fields/Types'; +import { DocCast } from '../../../fields/Types'; import { ImageField } from '../../../fields/URLField'; import { returnAll, returnAlways, returnTrue } from '../../../Utils'; import { Docs } from '../../documents/Documents'; @@ -13,6 +13,7 @@ import { CompiledScript, CompileScript, ScriptOptions } from '../../util/Scripti import { undoBatch } from '../../util/UndoManager'; import { ContextMenu } from '../ContextMenu'; import { ContextMenuProps } from '../ContextMenuItem'; +import { DocumentIconContainer } from './DocumentIcon'; import { OpenWhere } from './DocumentView'; import { FieldView, FieldViewProps } from './FieldView'; import { FormattedTextBox } from './formattedText/FormattedTextBox'; @@ -20,10 +21,6 @@ import { ImageBox } from './ImageBox'; import './KeyValueBox.scss'; import { KeyValuePair } from './KeyValuePair'; import React = require('react'); -import { DocumentManager } from '../../util/DocumentManager'; -import { ScriptingGlobals } from '../../util/ScriptingGlobals'; -import { ScriptingRepl } from '../ScriptingRepl'; -import { DocumentIconContainer } from './DocumentIcon'; export type KVPScript = { script: CompiledScript; @@ -147,7 +144,15 @@ export class KeyValueBox extends React.Component<FieldViewProps> { const self = this; const keys = Object.keys(ids).slice(); //for (const key of [...keys.filter(id => id !== 'layout' && !id.includes('_')).sort(), ...keys.filter(id => id === 'layout' || id.includes('_')).sort()]) { - for (const key of keys.sort()) { + for (const key of keys.sort((a: string, b: string) => { + const a_ = a.split('_')[0]; + const b_ = b.split('_')[0]; + if (a_ < b_) return -1; + if (a_ > b_) return 1; + if (a === a_) return -1; + if (b === b_) return 1; + return a === b ? 0 : a < b ? -1 : 1; + })) { rows.push( <KeyValuePair doc={realDoc} diff --git a/src/client/views/nodes/KeyValuePair.tsx b/src/client/views/nodes/KeyValuePair.tsx index 01acdccb7..f22cb195f 100644 --- a/src/client/views/nodes/KeyValuePair.tsx +++ b/src/client/views/nodes/KeyValuePair.tsx @@ -14,6 +14,8 @@ 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 @@ -109,11 +111,13 @@ export class KeyValuePair extends React.Component<KeyValuePairProps> { X </button> <input className="keyValuePair-td-key-check" type="checkbox" style={hover} onChange={this.handleCheck} ref={this.checkbox} /> - <div className="keyValuePair-keyField" style={{ marginLeft: 20 * (props.fieldKey.match(/_/g)?.length || 0), color: keyStyle }}> - {'('.repeat(parenCount)} - {props.fieldKey} - {')'.repeat(parenCount)} - </div> + <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 }}> + {'('.repeat(parenCount)} + {props.fieldKey} + {')'.repeat(parenCount)} + </div> + </Tooltip> </div> </td> <td className="keyValuePair-td-value" style={{ width: `${100 - this.props.keyWidth}%` }} onContextMenu={this.onContextMenu}> diff --git a/src/client/views/nodes/LinkBox.tsx b/src/client/views/nodes/LinkBox.tsx index 710d41471..efb949a47 100644 --- a/src/client/views/nodes/LinkBox.tsx +++ b/src/client/views/nodes/LinkBox.tsx @@ -18,7 +18,7 @@ export class LinkBox extends ViewBoxBaseComponent<FieldViewProps>() { this.props.setContentView?.(this); } render() { - if (this.dataDoc.treeViewOpen === undefined) setTimeout(() => (this.dataDoc.treeViewOpen = true)); + if (this.dataDoc.treeView_Open === undefined) setTimeout(() => (this.dataDoc.treeView_Open = true)); return ( <div className={`linkBox-container${this.props.isContentActive() ? '-interactive' : ''}`} style={{ background: this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.BackgroundColor) }}> <ComparisonBox diff --git a/src/client/views/nodes/LinkDocPreview.scss b/src/client/views/nodes/LinkDocPreview.scss index c68e55f73..28216394d 100644 --- a/src/client/views/nodes/LinkDocPreview.scss +++ b/src/client/views/nodes/LinkDocPreview.scss @@ -1,7 +1,6 @@ .linkDocPreview { position: absolute; pointer-events: all; - background-color: lightblue; border: 8px solid white; border-radius: 7px; box-shadow: 3px 3px 1.5px grey; @@ -11,7 +10,6 @@ cursor: pointer; .linkDocPreview-inner { - background-color: white; width: 100%; height: 100%; pointer-events: none; diff --git a/src/client/views/nodes/LinkDocPreview.tsx b/src/client/views/nodes/LinkDocPreview.tsx index 86191de63..198cbe851 100644 --- a/src/client/views/nodes/LinkDocPreview.tsx +++ b/src/client/views/nodes/LinkDocPreview.tsx @@ -13,12 +13,13 @@ import { DocumentType } from '../../documents/DocumentTypes'; 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 { SearchBox } from '../search/SearchBox'; import { DocumentView, DocumentViewSharedProps, OpenWhere } from './DocumentView'; import './LinkDocPreview.scss'; import React = require('react'); +import { DocumentManager } from '../../util/DocumentManager'; interface LinkDocPreviewProps { linkDoc?: Doc; @@ -174,9 +175,14 @@ export class LinkDocPreview extends React.Component<LinkDocPreviewProps> { LinkFollower.FollowLink(this._linkDoc, this._linkSrc, false); } else if (this.props.hrefs?.length) { const webDoc = - Array.from(SearchBox.staticSearchCollection(Doc.MyFilesystem, this.props.hrefs[0]).keys()).lastElement() ?? + 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 }); - this.props.docProps?.addDocTab(webDoc, OpenWhere.lightbox); + DocumentManager.Instance.showDocument(webDoc, { + openLocation: OpenWhere.lightbox, + willPan: true, + zoomTime: 500, + }); + //this.props.docProps?.addDocTab(webDoc, OpenWhere.lightbox); } }; @@ -198,7 +204,7 @@ export class LinkDocPreview extends React.Component<LinkDocPreviewProps> { }; @computed get previewHeader() { return !this._linkDoc || !this._markerTargetDoc || !this._targetDoc || !this._linkSrc ? null : ( - <div className="linkDocPreview-info"> + <div className="linkDocPreview-info" style={{ background: SettingsManager.userBackgroundColor }}> <div className="linkDocPreview-buttonBar" style={{ float: 'left' }}> <Tooltip title={<div className="dash-tooltip">Edit Link</div>} placement="top"> <div className="linkDocPreview-button" onPointerDown={this.editLink}> @@ -296,7 +302,7 @@ export class LinkDocPreview extends React.Component<LinkDocPreviewProps> { className="linkDocPreview" ref={this._linkDocRef} onPointerDown={this.followLinkPointerDown} - style={{ 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.scss b/src/client/views/nodes/LoadingBox.scss index d4a7e18f2..cabd4de05 100644 --- a/src/client/views/nodes/LoadingBox.scss +++ b/src/client/views/nodes/LoadingBox.scss @@ -5,34 +5,28 @@ justify-content: center; background-color: #fdfdfd; height: 100%; + width: 100%; align-items: center; - .textContainer, - .text { - overflow: hidden; + .loadingBox-textContainer, + .loadingBox-title { text-overflow: ellipsis; max-width: 80%; text-align: center; - display: flex; - flex-direction: column; + display: block; gap: 8px; align-items: center; + white-space: normal; + word-break: all; + .loadingBox-headerText { + text-align: center; + font-weight: bold; + height: auto; + width: 100%; + } + } + .loadingBox-spinner { + position: absolute; + top: 0; + left: 0; } -} - -.textContainer { - margin: 5px; -} - -.textContainer { - justify-content: center; - align-content: center; -} - -.headerText { - text-align: center; - font-weight: bold; -} - -.spinner { - text-align: center; } diff --git a/src/client/views/nodes/LoadingBox.tsx b/src/client/views/nodes/LoadingBox.tsx index fcbd0128d..bdc074e0c 100644 --- a/src/client/views/nodes/LoadingBox.tsx +++ b/src/client/views/nodes/LoadingBox.tsx @@ -1,14 +1,14 @@ -import { action, observable, runInAction } from 'mobx'; +import { observable, runInAction } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; import ReactLoading from 'react-loading'; import { Doc } from '../../../fields/Doc'; +import { Id } from '../../../fields/FieldSymbols'; import { StrCast } from '../../../fields/Types'; import { Networking } from '../../Network'; import { ViewBoxAnnotatableComponent } from '../DocComponent'; import { FieldView, FieldViewProps } from './FieldView'; import './LoadingBox.scss'; -import { Id } from '../../../fields/FieldSymbols'; /** * LoadingBox Class represents a placeholder doc for documents that are currently @@ -44,9 +44,9 @@ export class LoadingBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { this.rootDoc.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.rootDoc[Id])); // We use the guid of the overwriteDoc to track file uploads. runInAction(() => (this.progress = result.progress)); - this._timer = setTimeout(updateFunc, 1000); + !this.rootDoc.loadingError && (this._timer = setTimeout(updateFunc, 1000)); }; this._timer = setTimeout(updateFunc, 1000); } @@ -58,10 +58,14 @@ export class LoadingBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { render() { return ( <div className="loadingBoxContainer" style={{ background: !this.rootDoc.loadingError ? '' : 'red' }}> - <div className="textContainer"> - <p className="headerText">{StrCast(this.rootDoc.loadingError, 'Loading ' + (this.progress.replace('[download]', '') || '(can take several minutes)'))}</p> - <span className="text">{StrCast(this.rootDoc.title)}</span> - {this.rootDoc.loadingError ? null : <ReactLoading type={'spinningBubbles'} color={'blue'} height={100} width={100} />} + <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 : ( + <div className="loadingBox-spinner"> + <ReactLoading type="spinningBubbles" color="blue" height={100} width={100} /> + </div> + )} </div> </div> ); diff --git a/src/client/views/nodes/MapBox/MapAnchorMenu.scss b/src/client/views/nodes/MapBox/MapAnchorMenu.scss new file mode 100644 index 000000000..6990bdcf1 --- /dev/null +++ b/src/client/views/nodes/MapBox/MapAnchorMenu.scss @@ -0,0 +1,54 @@ +.anchorMenu-addTag { + display: grid; + width: 200px; + padding: 5px; + grid-template-columns: 90px 20px 90px; +} +.anchorMenu-highlighter { + padding-right: 5px; + .antimodeMenu-button { + padding: 0; + padding: 0; + padding-right: 0px; + padding-left: 0px; + width: 5px; + } +} +.anchor-color-preview-button { + width: 25px !important; + .anchor-color-preview { + display: flex; + flex-direction: column; + padding-right: 3px; + width: unset !important; + .color-preview { + width: 60%; + top: 80%; + height: 4px; + position: relative; + top: unset; + width: 15px; + margin-top: 5px; + display: block; + } + } +} + +.color-wrapper { + display: flex; + flex-wrap: wrap; + justify-content: space-between; + + button.color-button { + width: 20px; + height: 20px; + border-radius: 15px !important; + margin: 3px; + border: 2px solid transparent !important; + padding: 3px; + + &.active { + border: 2px solid white; + } + } +}
\ No newline at end of file diff --git a/src/client/views/nodes/MapBox/MapAnchorMenu.tsx b/src/client/views/nodes/MapBox/MapAnchorMenu.tsx new file mode 100644 index 000000000..f0827936b --- /dev/null +++ b/src/client/views/nodes/MapBox/MapAnchorMenu.tsx @@ -0,0 +1,143 @@ +import React = require('react'); +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { action, computed, IReactionDisposer, observable, ObservableMap, reaction } from 'mobx'; +import { observer } from 'mobx-react'; +import { ColorState } from 'react-color'; +import { Doc, Opt } from '../../../../fields/Doc'; +import { returnFalse, setupMoveUpEvents, unimplementedFunction, Utils } from '../../../../Utils'; +import { SelectionManager } from '../../../util/SelectionManager'; +import { AntimodeMenu, AntimodeMenuProps } from '../../AntimodeMenu'; +import { LinkPopup } from '../../linking/LinkPopup'; +import { gptAPICall, GPTCallType } from '../../../apis/gpt/GPT'; +// import { GPTPopup, GPTPopupMode } from './../../GPTPopup/GPTPopup'; +import { EditorView } from 'prosemirror-view'; +import './MapAnchorMenu.scss'; +import { ColorPicker, Group, IconButton, Popup, Size, Toggle, ToggleType, Type } from 'browndash-components'; +import { StrCast } from '../../../../fields/Types'; +import { DocumentType } from '../../../documents/DocumentTypes'; +import { SettingsManager } from '../../../util/SettingsManager'; + +@observer +export class MapAnchorMenu extends AntimodeMenu<AntimodeMenuProps> { + static Instance: MapAnchorMenu; + + private _disposer: IReactionDisposer | undefined; + private _disposer2: IReactionDisposer | undefined; + private _commentCont = React.createRef<HTMLButtonElement>(); + + 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 StartCropDrag: (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 LinkNote: () => void = unimplementedFunction; + // public MakeTargetToggle: () => void = unimplementedFunction; + // public ShowTargetTrail: () => void = unimplementedFunction; + public IsTargetToggler: () => boolean = returnFalse; + public get Active() { + return this._left > 0; + } + + constructor(props: Readonly<{}>) { + super(props); + + MapAnchorMenu.Instance = this; + MapAnchorMenu.Instance._canFade = false; + } + + componentWillUnmount() { + this._disposer?.(); + this._disposer2?.(); + } + + componentDidMount() { + this._disposer2 = reaction( + () => this._opacity, + opacity => { + if (!opacity) { + } + }, + { fireImmediately: true } + ); + this._disposer = reaction( + () => SelectionManager.Views().slice(), + selected => { + MapAnchorMenu.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) + // ); + // }; + + static top = React.createRef<HTMLDivElement>(); + // public get Top(){ + // return this.top + // } + + render() { + const buttons = ( + <> + { + <IconButton + tooltip="Delete Pin" // + onPointerDown={this.Delete} + icon={<FontAwesomeIcon icon="trash-alt" />} + color={SettingsManager.userColor} + /> + } + { + <IconButton + tooltip="Link Note to Pin" // + onPointerDown={this.LinkNote} + icon={<FontAwesomeIcon icon="sticky-note" />} + color={SettingsManager.userColor} + /> + } + { + <IconButton + tooltip="Center on pin" // + onPointerDown={this.Center} + icon={<FontAwesomeIcon icon="compress-arrows-alt" />} + color={SettingsManager.userColor} + /> + } + {/* {this.IsTargetToggler !== returnFalse && ( + <Toggle + tooltip={'Make target visibility toggle on click'} + type={Type.PRIM} + toggleType={ToggleType.BUTTON} + toggleStatus={this.IsTargetToggler()} + onClick={this.MakeTargetToggle} + icon={<FontAwesomeIcon icon="thumbtack" />} + color={SettingsManager.userColor} + /> + )} */} + </> + ); + + return this.getElement( + <div ref={MapAnchorMenu.top} style={{ width: '100%', display: 'flex' }}> + {buttons} + </div> + ); + } +} diff --git a/src/client/views/nodes/MapBox/MapBox.scss b/src/client/views/nodes/MapBox/MapBox.scss index fb15520f6..242677231 100644 --- a/src/client/views/nodes/MapBox/MapBox.scss +++ b/src/client/views/nodes/MapBox/MapBox.scss @@ -1,87 +1,108 @@ -@import "../../global/globalCssVariables.scss"; +@import '../../global/globalCssVariables.scss'; .mapBox { - width: 100%; - height: 100%; - overflow: hidden; - display: flex; - - .mapBox-infoWindow { - background-color: white; - opacity: 0.75; - padding: 12; - font-size: 17; - } + width: 100%; + height: 100%; + overflow: hidden; + display: flex; - .mapBox-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); - } + .mapBox-infoWindow { + background-color: white; + opacity: 0.75; + padding: 12; + font-size: 17; + } + .mapBox-searchbar { + display: flex; + flex-direction: row; + width: calc(100% - 40px); + .editableText-container { + width: 100%; + font-size: 16px !important; } - - .mapBox-wrapper { + input { width: 100%; - .mapBox-input { - box-sizing: border-box; - border: 1px solid transparent; - width: 240px; - height: 32px; - padding: 0 12px; - border-radius: 3px; - box-shadow: 0 2px 6px rgba(0, 0, 0, 0.3); - font-size: 14px; - outline: none; - text-overflow: ellipses; - position: absolute; - left: 50%; - margin-left: -120px; - } } + } + .mapBox-topbar { + display: flex; + flex-direction: row; + } - .mapBox-sidebar-handle { - top: 0; - //top: calc(50% - 17.5px); // use this to center vertically -- make sure it looks okay for slide views - width: 10px; - height: 100%; - max-height: 35px; - background: lightgray; - border-radius: 20px; - cursor:grabbing; + .mapBox-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); } - .mapBox-addMarker { + } + + .mapBox-wrapper { + width: 100%; + .mapBox-input { + box-sizing: border-box; + border: 1px solid transparent; + width: 240px; + height: 32px; + padding: 0 12px; + border-radius: 3px; + box-shadow: 0 2px 6px rgba(0, 0, 0, 0.3); + font-size: 14px; + outline: none; + text-overflow: ellipses; + position: absolute; left: 50%; - margin-left: 120px; - right: unset !important; - margin-top: -10; - height: max-content; - } - .searchbox { - display:none; - } - .mapBox-addMarker { - display:none; + margin-left: -120px; } + } + .mapBox-sidebar { + position: absolute; + right: 0; + height: 100%; + } + + .mapBox-sidebar-handle { + top: 0; + //top: calc(50% - 17.5px); // use this to center vertically -- make sure it looks okay for slide views + width: 10px; + height: 100%; + max-height: 35px; + background: lightgray; + border-radius: 20px; + cursor: grabbing; + } + .mapBox-addMarker { + left: 50%; + margin-left: 120px; + right: unset !important; + margin-top: -10; + height: max-content; + } + .searchbox { + display: none; + } + .mapBox-addMarker { + display: none; + } } .mapBox:hover { .mapBox-addMarker { - display:block; + display: block; } .searchbox { - display :block; + display: block; } } diff --git a/src/client/views/nodes/MapBox/MapBox.tsx b/src/client/views/nodes/MapBox/MapBox.tsx index de0b57fd7..d7469e530 100644 --- a/src/client/views/nodes/MapBox/MapBox.tsx +++ b/src/client/views/nodes/MapBox/MapBox.tsx @@ -1,31 +1,33 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { Autocomplete, GoogleMap, GoogleMapProps, Marker } from '@react-google-maps/api'; import BingMapsReact from 'bingmaps-react'; -import { action, computed, IReactionDisposer, observable, ObservableMap, runInAction } from 'mobx'; +import { Button, EditableText, IconButton, Type } from 'browndash-components'; +import { action, computed, IReactionDisposer, observable, ObservableMap, reaction, 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 { Doc, DocListCast, Field, Opt } from '../../../../fields/Doc'; +import { DocCss, Highlight, Width } from '../../../../fields/DocSymbols'; 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 { 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 { DocumentManager } from '../../../util/DocumentManager'; import { DragManager } from '../../../util/DragManager'; +import { LinkManager } from '../../../util/LinkManager'; import { SnappingManager } from '../../../util/SnappingManager'; -import { UndoManager } from '../../../util/UndoManager'; +import { Transform } from '../../../util/Transform'; +import { undoable, 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 { DocumentView } from '../DocumentView'; import { FieldView, FieldViewProps } from '../FieldView'; -import { PinProps } from '../trails'; +import { PinProps, PresBox } from '../trails'; +import { MapAnchorMenu } from './MapAnchorMenu'; import './MapBox.scss'; -import { MapBoxInfoWindow } from './MapBoxInfoWindow'; - +// amongus /** * MapBox architecture: * Main component: MapBox.tsx @@ -39,30 +41,7 @@ import { MapBoxInfoWindow } from './MapBoxInfoWindow'; * 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 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 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 @@ -80,245 +59,56 @@ document.head.appendChild(script); // }, // }); -// 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 MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps & FieldViewProps & Partial<GoogleMapProps>>() { - static UseBing = true; - 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)); +export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps & FieldViewProps>() { public static LayoutString(fieldKey: string) { return FieldView.LayoutString(MapBox, fieldKey); } - public get SidebarKey() { - return this.fieldKey + '_sidebar'; - } - private _setPreviewCursor: undefined | ((x: number, y: number, drag: boolean, hide: boolean) => void); - @computed get inlineTextAnnotations() { - return this.allMapMarkers.filter(a => a.textInlineAnnotations); - } + 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 _disposers: { [key: string]: IReactionDisposer } = {}; + private _setPreviewCursor: undefined | ((x: number, y: number, drag: boolean, hide: boolean, doc: Opt<Doc>) => void); - @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() { + // this list contains pushpins and configs + @computed get allAnnotations() { return DocListCast(this.dataDoc[this.annotationKey]); } - @observable private toggleAddMarker = false; - private _mainCont: React.RefObject<HTMLDivElement> = React.createRef(); - - @observable _showSidebar = false; + @computed get allPushpins() { + return this.allAnnotations.filter(anno => anno.type === DocumentType.PUSHPIN); + } @computed get SidebarShown() { - return this._showSidebar || this.layoutDoc._layout_showSidebar ? true : false; + 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'; } - - static _canAnnotate = true; - static _hadSelection: boolean = false; - private _sidebarRef = React.createRef<SidebarAnnos>(); - private _ref: React.RefObject<HTMLDivElement> = React.createRef(); componentDidMount() { + this._unmounting = false; 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 = 'mapBox-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.MapMarkerDocument(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.mapZoom, 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.mapZoom = 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; - }; + _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 @@ -327,35 +117,42 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps * @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.MapMarkerDocument(NumCast(doc.lat), NumCast(doc.lng), false, [doc], {}); - this.addDocument(marker, this.annotationKey); - } + 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) => { - if (this.layoutDoc._layout_showSidebar) this.toggleSidebar(); - const docs = doc instanceof Doc ? [doc] : doc; - return this.removeDocument(doc, sidebarKey); - }; + sidebarRemoveDocument = (doc: Doc | Doc[], sidebarKey?: string) => this.removeMapDocument(doc, sidebarKey); /** * Toggle sidebar onclick the tiny comment button on the top right corner @@ -374,11 +171,11 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps const fullWidth = this.layoutDoc[Width](); const mapWidth = fullWidth - this.sidebarWidth(); if (this.sidebarWidth() + localDelta[0] > 0) { - this._showSidebar = true; + 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._showSidebar = false; + this.layoutDoc._layout_showSidebar = false; this.layoutDoc._width = mapWidth; this.layoutDoc._layout_sidebarWidthPercent = '0%'; } @@ -388,60 +185,7 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps () => 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, - }) - ); - }; + sidebarWidth = () => (Number(this.sidebarWidthPercent.substring(0, this.sidebarWidthPercent.length - 1)) / 100) * this.props.PanelWidth(); /** * Handles toggle of sidebar on click the little comment button @@ -466,12 +210,28 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps // 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); }; + 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); }; @@ -483,7 +243,7 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps return false; }; - setPreviewCursor = (func?: (x: number, y: number, drag: boolean, hide: boolean) => void) => (this._setPreviewCursor = func); + setPreviewCursor = (func?: (x: number, y: number, drag: boolean, hide: boolean, doc: Opt<Doc>) => void) => (this._setPreviewCursor = func); @action onMarqueeDown = (e: React.PointerEvent) => { @@ -504,48 +264,13 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps }; @action finishMarquee = (x?: number, y?: number) => { this._marqueeing = undefined; - this._isAnnotating = false; - x !== undefined && y !== undefined && this._setPreviewCursor?.(x, y, false, false); + x !== undefined && y !== undefined && this._setPreviewCursor?.(x, y, false, false, this.rootDoc); }; - addDocumentWrapper = (doc: Doc | Doc[], annotationKey?: string) => { - return this.addDocument(doc, annotationKey); - }; + addDocumentWrapper = (doc: Doc | Doc[], annotationKey?: string) => this.addDocument(doc, annotationKey); pointerEvents = () => (this.props.isContentActive() && !MarqueeOptionsMenu.Instance.isShown() ? 'all' : 'none'); - @computed get annotationLayer() { - return ( - <div className="mapBox-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)); @@ -582,24 +307,409 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps } else { this._bingSearchManager.geocode({ where: query, - callback: action((r: any) => { - res(r.results[0].location); - }), + callback: action((r: any) => res(r.results[0].location)), errorCallback: (e: any) => reject(), }); } }); }; + @observable + bingSearchBarContents: any = this.rootDoc.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; + + @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; + } + }; + + getView = async (doc: Doc) => { + if (this._sidebarRef?.current?.makeDocUnfiltered(doc) && !this.SidebarShown) this.toggleSidebar(); + 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.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'); + + this.recolorPin(this.selectedPin, 'green'); + + MapAnchorMenu.Instance.Delete = this.deleteSelectedPin; + MapAnchorMenu.Instance.Center = this.centerOnSelectedPin; + MapAnchorMenu.Instance.LinkNote = this.createNoteAnnotation; + + 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); + 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.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), + 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.rootDoc, + }); + if (anchor) { + if (!addAsAnnotation) anchor.backgroundColor = 'transparent'; + addAsAnnotation && this.addDocument(anchor); + PresBox.pinDocView(anchor, { pinDocLayout: pinProps?.pinDocLayout, pinData: { ...(pinProps?.pinData ?? {}), map: true } }, this.rootDoc); + return anchor; + } + return this.rootDoc; + }; + + 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.rootDoc, 'latitude', this.selectedPin.latitude, 'remove'); + Doc.setDocFilter(this.rootDoc, 'longitude', this.selectedPin.longitude, 'remove'); + Doc.setDocFilter(this.rootDoc, '-linkedTo', `mapPin=${Field.toString(DocCast(this.selectedPin.mapPin))}`, '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: defaultCenter.lat, longitude: defaultCenter.lng }, + // 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.rootDoc.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.rootDoc.latitude, lng: this.rootDoc.longitude, zoom: this.rootDoc.map_zoom, mapType: this.rootDoc.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.props.ScreenToLocalTransform().transformPoint(e.clientX, e.clientY); + const x = cpt[0] - (this.props.PanelWidth() - this.sidebarWidth()) / 2; + const y = cpt[1] - 32 /* height of search bar */ - this.props.PanelHeight() / 2; + const location = this._bingMap.current.tryPixelToLocation(new this.MicrosoftMaps.Point(x, y)); + this.createPushpin(location.latitude, location.longitude); + break; + } + target = target.parentElement; + } + }, + e => { + const createPin = () => this.createPushpin(this.rootDoc.latitude, this.rootDoc.longitude, this.rootDoc.map); + if (this.bingSearchBarContents) { + this.bingSearch().then(createPin); + } else createPin(); + } + ); }; - bingMapReady = (map: any) => (this._bingMap = map.map); + + 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.rootDoc[DocCss]; + if (MapBox._rerenderDelay) { + // prettier-ignore + this._rerenderTimeout = this._rerenderTimeout ?? + setTimeout(action(() => { + if ((window as any).Microsoft?.Maps?.Internal._WorkDispatcher) { + MapBox._rerenderDelay = 0; + } + this._rerenderTimeout = undefined; + this.rootDoc[DocCss] = this.rootDoc[DocCss] + 1; + }), MapBox._rerenderDelay); + return null; + } + const renderAnnotations = (childFilters?: () => string[]) => null; return ( <div className="mapBox" ref={this._ref}> @@ -608,47 +718,87 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps onWheel={e => e.stopPropagation()} onPointerDown={async e => { e.button === 0 && !e.ctrlKey && e.stopPropagation(); - // just a simple test of bing maps geocode api - // const loc = await this.bingGeocode(this._bingMap, 'Philadelphia, PA'); - // this._bingMap.current.setView({ - // mapTypeId: this.MicrosoftMaps.MapTypeId.aerial, - // center: new this.MicrosoftMaps.Location(loc.latitude, loc.longitude), - // zoom: 15, - // }); }} - style={{ width: `calc(100% - ${this.layout_sidebarWidthPercent})`, pointerEvents: this.pointerEvents() }}> + style={{ width: `calc(100% - ${this.sidebarWidthPercent})`, pointerEvents: this.pointerEvents() }}> <div style={{ mixBlendMode: 'multiply' }}>{renderAnnotations(this.transparentFilter)}</div> {renderAnnotations(this.opaqueFilter)} {SnappingManager.GetIsDragging() ? null : renderAnnotations()} - {this.annotationLayer} - - {!MapBox.UseBing ? null : <BingMapsReact onMapReady={this.bingMapReady} bingMapsKey={bingApiKey} height="100%" mapOptions={this.bingMapOptions} width="100%" viewOptions={this.bingViewOptions} />} - <div style={{ display: MapBox.UseBing ? 'none' : undefined }}> - <GoogleMap mapContainerStyle={mapContainerStyle} onZoomChanged={this.zoomChanged} onCenterChanged={this.centered} onLoad={this.loadHandler} options={mapOptions}> - <Autocomplete onLoad={this.setSearchBox} onPlaceChanged={this.handlePlaceChanged}> - <input className="mapBox-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 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> + + <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} @@ -667,7 +817,7 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps )} </div> {/* </LoadScript > */} - <div className="mapBox-sidebar" style={{ position: 'absolute', right: 0, height: '100%', width: `${this.layout_sidebarWidthPercent}`, backgroundColor: `${this.sidebarColor}` }}> + <div className="mapBox-sidebar" style={{ width: `${this.sidebarWidthPercent}`, backgroundColor: `${this.sidebarColor}` }}> <SidebarAnnos ref={this._sidebarRef} {...this.props} diff --git a/src/client/views/nodes/MapBox/MapBox2.tsx b/src/client/views/nodes/MapBox/MapBox2.tsx index a9154c5bb..407a91dd0 100644 --- a/src/client/views/nodes/MapBox/MapBox2.tsx +++ b/src/client/views/nodes/MapBox/MapBox2.tsx @@ -98,9 +98,9 @@ export class MapBox2 extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps public get SidebarKey() { return this.fieldKey + '_sidebar'; } - private _setPreviewCursor: undefined | ((x: number, y: number, drag: boolean, hide: boolean) => void); + private _setPreviewCursor: undefined | ((x: number, y: number, drag: boolean, hide: boolean, doc: Opt<Doc>) => void); @computed get inlineTextAnnotations() { - return this.allMapMarkers.filter(a => a.textInlineAnnotations); + return this.allMapMarkers.filter(a => a.text_inlineAnnotations); } @observable private _map: google.maps.Map = null as unknown as google.maps.Map; @@ -229,7 +229,7 @@ export class MapBox2 extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps map: map, }); map.panTo(position); - const mapMarker = Docs.Create.MapMarkerDocument(NumCast(position.lat()), NumCast(position.lng()), false, [], {}); + const mapMarker = Docs.Create.PushpinDocument(NumCast(position.lat()), NumCast(position.lng()), false, [], {}); this.addDocument(mapMarker, this.annotationKey); }; @@ -260,7 +260,7 @@ export class MapBox2 extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps // } else { // alert("Your geolocation is not supported by browser.") // }; - map.setZoom(NumCast(this.dataDoc.mapZoom, 2.5)); + 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()) { @@ -292,7 +292,7 @@ export class MapBox2 extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps this._loadPending = false; this.layoutDoc.freeform_fitContentsToBox && this.fitBounds(this._map); } - this.dataDoc.mapZoom = this._map.getZoom(); + this.dataDoc.map_zoom = this._map.getZoom(); }; /** @@ -333,7 +333,7 @@ export class MapBox2 extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps if (existingMarker) { Doc.AddDocToList(existingMarker, 'data', doc); } else { - const marker = Docs.Create.MapMarkerDocument(NumCast(doc.lat), NumCast(doc.lng), false, [doc], {}); + const marker = Docs.Create.PushpinDocument(NumCast(doc.lat), NumCast(doc.lng), false, [doc], {}); this.addDocument(marker, this.annotationKey); } } @@ -502,7 +502,7 @@ export class MapBox2 extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps @action finishMarquee = (x?: number, y?: number) => { this._marqueeing = undefined; this._isAnnotating = false; - x !== undefined && y !== undefined && this._setPreviewCursor?.(x, y, false, false); + x !== undefined && y !== undefined && this._setPreviewCursor?.(x, y, false, false, this.props.Document); }; addDocumentWrapper = (doc: Doc | Doc[], annotationKey?: string) => { diff --git a/src/client/views/nodes/MapBox/MapBoxInfoWindow.tsx b/src/client/views/nodes/MapBox/MapBoxInfoWindow.tsx index 577101445..66c47d131 100644 --- a/src/client/views/nodes/MapBox/MapBoxInfoWindow.tsx +++ b/src/client/views/nodes/MapBox/MapBoxInfoWindow.tsx @@ -47,7 +47,9 @@ export class MapBoxInfoWindow extends React.Component<MapBoxInfoWindowProps & Vi 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}> + <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 diff --git a/src/client/views/nodes/MapBox/MapPushpinBox.tsx b/src/client/views/nodes/MapBox/MapPushpinBox.tsx new file mode 100644 index 000000000..42bada0ef --- /dev/null +++ b/src/client/views/nodes/MapBox/MapPushpinBox.tsx @@ -0,0 +1,34 @@ +import { observer } from 'mobx-react'; +// import { SettingsManager } from '../../../util/SettingsManager'; +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); + } + componentWillUnmount() { + this.mapBoxView.deletePushpin(this.rootDoc); + } + + @computed get mapBoxView() { + return this.props.DocumentView?.()?.props.docViewPath().lastElement()?.ComponentView as MapBox; + } + @computed get mapBox() { + return this.props.DocumentView?.().props.docViewPath().lastElement()?.rootDoc; + } + + render() { + return <div />; + } +} diff --git a/src/client/views/nodes/PDFBox.tsx b/src/client/views/nodes/PDFBox.tsx index fd4c6366b..39e27d15b 100644 --- a/src/client/views/nodes/PDFBox.tsx +++ b/src/client/views/nodes/PDFBox.tsx @@ -213,7 +213,7 @@ export class PDFBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps ); } - brushView = (view: { width: number; height: number; panX: number; panY: number }) => this._pdfViewer?.brushView(view); + 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) { @@ -224,7 +224,7 @@ export class PDFBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps }; focus = (anchor: Doc, options: DocFocusOptions) => { this._initialScrollTarget = anchor; - return this._pdfViewer?.scrollFocus(anchor, NumCast(anchor.y, NumCast(anchor.presViewScroll)), options); + return this._pdfViewer?.scrollFocus(anchor, NumCast(anchor.y, NumCast(anchor.config_scrollTop)), options); }; getView = async (doc: Doc) => { @@ -243,14 +243,13 @@ export class PDFBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps title: StrCast(this.rootDoc.title + '@' + NumCast(this.layoutDoc._layout_scrollTop)?.toFixed(0)), annotationOn: this.rootDoc, }); - const annoAnchor = this._pdfViewer?._getAnchor(this._pdfViewer.savedAnnotations(), true); - const anchor = annoAnchor ?? docAnchor(); + 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); anchor.text = ele?.textContent ?? ''; - anchor.textHtml = ele?.innerHTML; - if (addAsAnnotation || annoAnchor) { - this.addDocument(anchor); - } + anchor.text_html = ele?.innerHTML; + addAsAnnotation && this.addDocument(anchor); + return anchor; }; @@ -439,7 +438,7 @@ export class PDFBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps return PDFBox.sidebarResizerWidth + nativeDiff * (this.props.NativeDimScaling?.() || 1); }; @undoBatch - toggleSidebarType = () => (this.rootDoc.sidebar_collectionType = this.rootDoc.sidebar_collectionType === CollectionViewType.Freeform ? CollectionViewType.Stacking : CollectionViewType.Freeform); + toggleSidebarType = () => (this.rootDoc[this.SidebarKey + '_type_collection'] = this.rootDoc[this.SidebarKey + '_type_collection'] === CollectionViewType.Freeform ? CollectionViewType.Stacking : CollectionViewType.Freeform); specificContextMenu = (e: React.MouseEvent): void => { const cm = ContextMenu.Instance; const options = cm.findByDescription('Options...'); @@ -555,7 +554,7 @@ export class PDFBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps }; return ( <div className={'formattedTextBox-sidebar' + (Doc.ActiveTool !== InkTool.None ? '-inking' : '')} style={{ width: '100%', right: 0, backgroundColor: `white` }}> - {renderComponent(StrCast(this.layoutDoc.sidebar_collectionType))} + {renderComponent(StrCast(this.layoutDoc[this.SidebarKey + '_type_collection']))} </div> ); } diff --git a/src/client/views/nodes/RecordingBox/RecordingBox.tsx b/src/client/views/nodes/RecordingBox/RecordingBox.tsx index 04f11a5df..481e43feb 100644 --- a/src/client/views/nodes/RecordingBox/RecordingBox.tsx +++ b/src/client/views/nodes/RecordingBox/RecordingBox.tsx @@ -1,31 +1,38 @@ import { action, observable } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; +import { DateField } from '../../../../fields/DateField'; +import { Doc, DocListCast } from '../../../../fields/Doc'; +import { Id } from '../../../../fields/FieldSymbols'; +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 { 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 { FieldView } from '../FieldView'; +import { media_state } from '../AudioBox'; +import { FieldView, FieldViewProps } from '../FieldView'; import { VideoBox } from '../VideoBox'; import { RecordingView } from './RecordingView'; -import { DocumentType } from '../../../documents/DocumentTypes'; -import { Presentation } from '../../../util/TrackMovements'; -import { Doc } from '../../../../fields/Doc'; -import { Id } from '../../../../fields/FieldSymbols'; -import { DocCast } from '../../../../fields/Types'; @observer -export class RecordingBox extends ViewBoxBaseComponent() { +export class RecordingBox extends ViewBoxBaseComponent<FieldViewProps>() { public static LayoutString(fieldKey: string) { return FieldView.LayoutString(RecordingBox, fieldKey); } private _ref: React.RefObject<HTMLDivElement> = React.createRef(); - constructor(props: any) { - super(props); - } - componentDidMount() { + this.props.setContentView?.(this); Doc.SetNativeWidth(this.dataDoc, 1280); Doc.SetNativeHeight(this.dataDoc, 720); } @@ -34,9 +41,7 @@ export class RecordingBox extends ViewBoxBaseComponent() { @observable videoDuration: number | undefined = undefined; @action - setVideoDuration = (duration: number) => { - this.videoDuration = duration; - }; + setVideoDuration = (duration: number) => (this.videoDuration = duration); @action setResult = (info: Upload.AccessPathInfo, presentation?: Presentation) => { @@ -46,20 +51,209 @@ export class RecordingBox extends ViewBoxBaseComponent() { this.dataDoc.layout = VideoBox.LayoutString(this.fieldKey); this.dataDoc[this.props.fieldKey] = new VideoField(this.result.accessPaths.client); - this.dataDoc[this.fieldKey + '-recorded'] = true; + this.dataDoc[this.fieldKey + '_recorded'] = true; // stringify the presentation and store it if (presentation?.movements) { const presCopy = { ...presentation }; presCopy.movements = presentation.movements.map(movement => ({ ...movement, doc: movement.doc[Id] })) as any; - this.dataDoc[this.fieldKey + '-presentation'] = JSON.stringify(presCopy); + this.dataDoc[this.fieldKey + '_presentation'] = JSON.stringify(presCopy); } }; + @undoBatch + @action + public static WorkspaceStopRecording() { + const remDoc = RecordingBox.screengrabber?.rootDoc; + if (remDoc) { + //if recordingbox is true; when we press the stop button. changed vals temporarily to see if changes happening + RecordingBox.screengrabber?.Pause?.(); + setTimeout(() => { + RecordingBox.screengrabber?.Finish?.(); + remDoc.overlayX = 70; //was 100 + remDoc.overlayY = 590; + RecordingBox.screengrabber = undefined; + }, 100); + //could break if recording takes too long to turn into videobox. If so, either increase time on setTimeout below or find diff place to do this + setTimeout(() => Doc.RemFromMyOverlay(remDoc), 1000); + Doc.UserDoc().workspaceRecordingState = media_state.Paused; + Doc.AddDocToList(Doc.UserDoc(), 'workspaceRecordings', remDoc); + } + } + + /** + * This method toggles whether or not we are currently using the RecordingBox to record with the topbar button + * @param _readOnly_ + * @returns + */ + @undoBatch + @action + public static WorkspaceStartRecording(value: string) { + const screengrabber = + value === 'Record Workspace' + ? Docs.Create.ScreenshotDocument({ + title: `${new DateField()}-${Doc.ActiveDashboard?.title ?? ''}`, + _width: 205, + _height: 115, + }) + : Docs.Create.WebCamDocument(`${new DateField()}-${Doc.ActiveDashboard?.title ?? ''}`, { + title: `${new DateField()}-${Doc.ActiveDashboard?.title ?? ''}`, + _width: 205, + _height: 115, + }); + screengrabber.overlayX = 70; //was -400 + screengrabber.overlayY = 590; //was 0 + Doc.GetProto(screengrabber)[Doc.LayoutFieldKey(screengrabber) + '_trackScreen'] = true; + Doc.AddToMyOverlay(screengrabber); //just adds doc to overlay + DocumentManager.Instance.AddViewRenderedCb(screengrabber, docView => { + RecordingBox.screengrabber = docView.ComponentView as RecordingBox; + RecordingBox.screengrabber.Record?.(); + }); + Doc.UserDoc().workspaceRecordingState = media_state.Recording; + } + + /** + * This method changes the menu depending on whether or not we are in playback mode + * @param value RecordingBox rootdoc + */ + @undoBatch + @action + public static replayWorkspace(value: Doc) { + Doc.UserDoc().currentRecording = value; + value.overlayX = 70; + value.overlayY = window.innerHeight - 180; + Doc.AddToMyOverlay(value); + DocumentManager.Instance.AddViewRenderedCb(value, docView => { + Doc.UserDoc().currentRecording = docView.rootDoc; + SelectionManager.SelectSchemaViewDoc(value); + RecordingBox.resumeWorkspaceReplaying(value); + }); + } + + /** + * Adds the recording box to the canvas + * @param value current recordingbox + */ + @undoBatch + @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); + Doc.UserDoc().currentRecording = undefined; + Doc.UserDoc().workspaceReplayingState = undefined; + Doc.UserDoc().workspaceRecordingState = undefined; + } + + @action + public static resumeWorkspaceReplaying(doc: Doc) { + const docView = DocumentManager.Instance.getDocumentView(doc); + if (docView?.ComponentView instanceof VideoBox) { + docView.ComponentView.Play(); + } + Doc.UserDoc().workspaceReplayingState = media_state.Playing; + } + + @action + public static pauseWorkspaceReplaying(doc: Doc) { + const docView = DocumentManager.Instance.getDocumentView(doc); + const videoBox = docView?.ComponentView as VideoBox; + if (videoBox) { + videoBox.Pause(); + } + Doc.UserDoc().workspaceReplayingState = media_state.Paused; + } + + @action + public static stopWorkspaceReplaying(value: Doc) { + Doc.RemFromMyOverlay(value); + Doc.UserDoc().currentRecording = undefined; + Doc.UserDoc().workspaceReplayingState = undefined; + Doc.UserDoc().workspaceRecordingState = undefined; + Doc.RemFromMyOverlay(value); + } + + @undoBatch + @action + public static removeWorkspaceReplaying(value: Doc) { + Doc.RemoveDocFromList(Doc.UserDoc(), 'workspaceRecordings', value); + Doc.RemFromMyOverlay(value); + Doc.UserDoc().currentRecording = undefined; + Doc.UserDoc().workspaceReplayingState = undefined; + Doc.UserDoc().workspaceRecordingState = undefined; + } + + Record: undefined | (() => void); + Pause: undefined | (() => void); + Finish: undefined | (() => void); + getControls = (record: () => void, pause: () => void, finish: () => void) => { + this.Record = record; + this.Pause = pause; + this.Finish = finish; + }; render() { return ( <div className="recordingBox" ref={this._ref}> - {!this.result && <RecordingView setResult={this.setResult} setDuration={this.setVideoDuration} id={DocCast(this.rootDoc.proto)?.[Id] || ''} />} + {!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] || ''} + /> + )} </div> ); } + static screengrabber: RecordingBox | undefined; } + +ScriptingGlobals.add(function stopWorkspaceRecording() { + RecordingBox.WorkspaceStopRecording(); +}); + +ScriptingGlobals.add(function stopWorkspaceReplaying(value: Doc) { + RecordingBox.stopWorkspaceReplaying(value); +}); +ScriptingGlobals.add(function removeWorkspaceReplaying(value: Doc) { + RecordingBox.removeWorkspaceReplaying(value); +}); + +ScriptingGlobals.add(function getCurrentRecording() { + return Doc.UserDoc().currentRecording; +}); +ScriptingGlobals.add(function getWorkspaceRecordings() { + return new List<any>(['Record Workspace', `Record Webcam`, ...DocListCast(Doc.UserDoc().workspaceRecordings)]); +}); +ScriptingGlobals.add(function isWorkspaceRecording() { + return Doc.UserDoc().workspaceRecordingState === media_state.Recording; +}); +ScriptingGlobals.add(function isWorkspaceReplaying() { + return Doc.UserDoc().workspaceReplayingState; +}); +ScriptingGlobals.add(function replayWorkspace(value: Doc | string, _readOnly_: boolean) { + if (_readOnly_) return DocCast(Doc.UserDoc().currentRecording) ?? 'Record Workspace'; + if (typeof value === 'string') RecordingBox.WorkspaceStartRecording(value); + else RecordingBox.replayWorkspace(value); +}); +ScriptingGlobals.add(function pauseWorkspaceReplaying(value: Doc, _readOnly_: boolean) { + RecordingBox.pauseWorkspaceReplaying(value); +}); +ScriptingGlobals.add(function resumeWorkspaceReplaying(value: Doc, _readOnly_: boolean) { + RecordingBox.resumeWorkspaceReplaying(value); +}); + +ScriptingGlobals.add(function startRecordingDrag(value: { doc: Doc | string; e: React.PointerEvent }) { + if (DocCast(value.doc)) { + DragManager.StartDocumentDrag([value.e.target as HTMLElement], new DragManager.DocumentDragData([DocCast(value.doc)], 'embed'), value.e.clientX, value.e.clientY); + value.e.preventDefault(); + return true; + } +}); +ScriptingGlobals.add(function renderDropdown() { + if (!Doc.UserDoc().workspaceRecordings || DocListCast(Doc.UserDoc().workspaceRecordings).length === 0) { + return true; + } + return false; +}); diff --git a/src/client/views/nodes/RecordingBox/RecordingView.tsx b/src/client/views/nodes/RecordingBox/RecordingView.tsx index 51eb774e2..f7ed82643 100644 --- a/src/client/views/nodes/RecordingBox/RecordingView.tsx +++ b/src/client/views/nodes/RecordingBox/RecordingView.tsx @@ -21,6 +21,8 @@ interface IRecordingViewProps { setResult: (info: Upload.AccessPathInfo, presentation?: Presentation) => void; setDuration: (seconds: number) => void; id: string; + getControls: (record: () => void, pause: () => void, finish: () => void) => void; + forceTrackScreen: boolean; } const MAXTIME = 100000; @@ -60,14 +62,14 @@ export function RecordingView(props: IRecordingViewProps) { useEffect(() => { if (finished) { // make the total presentation that'll match the concatted video - let concatPres = trackScreen && TrackMovements.Instance.concatPresentations(videos.map(v => v.presentation as Presentation)); + let concatPres = (trackScreen || props.forceTrackScreen) && TrackMovements.Instance.concatPresentations(videos.map(v => v.presentation as Presentation)); // this async function uses the server to create the concatted video and then sets the result to it's accessPaths (async () => { const videoFiles = videos.map((vid, i) => new File(vid.videoChunks, `segvideo${i}.mkv`, { type: vid.videoChunks[0].type, lastModified: Date.now() })); // upload the segments to the server and get their server access paths - const serverPaths: string[] = (await Networking.UploadFilesToServer(videoFiles.map(file => ({file})))).map(res => (res.result instanceof Error ? '' : res.result.accessPaths.agnostic.server)); + const serverPaths: string[] = (await Networking.UploadFilesToServer(videoFiles.map(file => ({ file })))).map(res => (res.result instanceof Error ? '' : res.result.accessPaths.agnostic.server)); // concat the segments together using post call const result: Upload.AccessPathInfo | Error = await Networking.PostToServer('/concatVideos', serverPaths); @@ -132,7 +134,7 @@ export function RecordingView(props: IRecordingViewProps) { videoRecorder.current.onstart = (event: any) => { setRecording(true); // start the recording api when the video recorder starts - trackScreen && TrackMovements.Instance.start(); + (trackScreen || props.forceTrackScreen) && TrackMovements.Instance.start(); }; videoRecorder.current.onstop = () => { @@ -147,7 +149,7 @@ export function RecordingView(props: IRecordingViewProps) { // depending on if a presenation exists, add it to the video const presentation = TrackMovements.Instance.yieldPresentation(); - setVideos(videos => [...videos, presentation != null && trackScreen ? { ...nextVideo, presentation } : nextVideo]); + setVideos(videos => [...videos, presentation != null && (trackScreen || props.forceTrackScreen) ? { ...nextVideo, presentation } : nextVideo]); } // reset the temporary chunks @@ -159,12 +161,9 @@ export function RecordingView(props: IRecordingViewProps) { }; // if this is called, then we're done recording all the segments - const finish = (e: React.PointerEvent) => { - e.stopPropagation(); - + const finish = () => { // call stop on the video recorder if active videoRecorder.current?.state !== 'inactive' && videoRecorder.current?.stop(); - // end the streams (audio/video) to remove recording icon const stream = videoElementRef.current!.srcObject; stream instanceof MediaStream && stream.getTracks().forEach(track => track.stop()); @@ -176,8 +175,7 @@ export function RecordingView(props: IRecordingViewProps) { setFinished(true); }; - const pause = (e: React.PointerEvent) => { - e.stopPropagation(); + const pause = () => { // if recording, then this is just a new segment videoRecorder.current?.state === 'recording' && videoRecorder.current.stop(); }; @@ -217,6 +215,10 @@ export function RecordingView(props: IRecordingViewProps) { return toTwoDigit(minutes) + ' : ' + toTwoDigit(seconds); }; + useEffect(() => { + props.getControls(record, pause, finish); + }, []); + return ( <div className="recording-container"> <div className="video-wrapper"> @@ -227,7 +229,19 @@ export function RecordingView(props: IRecordingViewProps) { </div> <div className="controls"> <div className="controls-inner-container"> - <div className="record-button-wrapper">{recording ? <button className="stop-button" onPointerDown={pause} /> : <button className="record-button" onPointerDown={start} />}</div> + <div className="record-button-wrapper"> + {recording ? ( + <button + className="stop-button" + onPointerDown={e => { + e.stopPropagation(); + pause(); + }} + /> + ) : ( + <button className="record-button" onPointerDown={start} /> + )} + </div> {!recording && (videos.length > 0 ? ( @@ -236,7 +250,12 @@ export function RecordingView(props: IRecordingViewProps) { <MdBackspace onPointerDown={undoPrevious} /> </IconContext.Provider> <IconContext.Provider value={{ color: '#cc1c08', className: 'video-edit-buttons' }}> - <FaCheckCircle onPointerDown={finish} /> + <FaCheckCircle + onPointerDown={e => { + e.stopPropagation(); + finish(); + }} + /> </IconContext.Provider> </div> ) : ( @@ -244,7 +263,7 @@ export function RecordingView(props: IRecordingViewProps) { <label className="track-screen"> <input type="checkbox" - checked={trackScreen} + checked={trackScreen || props.forceTrackScreen} onChange={e => { setTrackScreen(e.target.checked); }} diff --git a/src/client/views/nodes/ScreenshotBox.scss b/src/client/views/nodes/ScreenshotBox.scss index 6fb5ea7b3..1e9b64a0b 100644 --- a/src/client/views/nodes/ScreenshotBox.scss +++ b/src/client/views/nodes/ScreenshotBox.scss @@ -1,6 +1,5 @@ .screenshotBox { transform-origin: top left; - background: white; color: black; // .screenshotBox-viewer { // opacity: 0.99; // hack! overcomes some kind of Chrome weirdness where buttons (e.g., snapshot) disappear at some point as the video is resized larger @@ -12,46 +11,39 @@ #CANCAN { canvas { - width:100% !important; + width: 100% !important; height: 100% !important; } } -.screenshotBox-content, .screenshotBox-content-interactive, .screenshotBox-cont-fullScreen { +.screenshotBox-content, +.screenshotBox-content-interactive, +.screenshotBox-cont-fullScreen { width: 100%; z-index: -1; // 0; // logically this should be 0 (or unset) which would give us transparent brush strokes over videos. However, this makes Chrome crawl to a halt position: absolute; } -.screenshotBox-content, .screenshotBox-content-interactive, .screenshotBox-content-fullScreen { - height: Auto; +.screenshotBox-content, +.screenshotBox-content-interactive, +.screenshotBox-content-fullScreen { + height: Auto; } .screenshotBox-uiButtons { - background:dimgray; - border: orange solid 1px; position: absolute; right: 25; top: 0; - width:25; + width: 22; height: 25; - .screenshotBox-snapshot{ - color : white; - top :0px; - right : 5px; - position: absolute; - background-color:rgba(50, 50, 50, 0.2); - transform-origin: left top; - pointer-events:all; - } - .screenshotBox-recorder{ - color : white; - top :0px; + .screenshotBox-recorder { + color: white; + top: 4px; left: 5px; position: absolute; - background-color:rgba(50, 50, 50, 0.2); + background-color: rgba(50, 50, 50, 0.2); transform-origin: left top; - pointer-events:all; + pointer-events: all; } } diff --git a/src/client/views/nodes/ScreenshotBox.tsx b/src/client/views/nodes/ScreenshotBox.tsx index 312b3c619..ebb8a3374 100644 --- a/src/client/views/nodes/ScreenshotBox.tsx +++ b/src/client/views/nodes/ScreenshotBox.tsx @@ -17,6 +17,7 @@ import { DocUtils } from '../../documents/Documents'; import { DocumentType } from '../../documents/DocumentTypes'; import { Networking } from '../../Network'; import { CaptureManager } from '../../util/CaptureManager'; +import { SettingsManager } from '../../util/SettingsManager'; import { CollectionFreeFormView } from '../collections/collectionFreeForm/CollectionFreeFormView'; import { CollectionStackedTimeline } from '../collections/CollectionStackedTimeline'; import { ContextMenu } from '../ContextMenu'; @@ -25,6 +26,8 @@ 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'; declare class MediaRecorder { constructor(e: any, options?: any); // whatever MediaRecorder has @@ -116,7 +119,7 @@ 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) { @@ -179,7 +182,7 @@ export class ScreenshotBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatabl ref={r => { this._videoRef = r; setTimeout(() => { - if (this.rootDoc.mediaState === 'pendingRecording' && this._videoRef) { + if (this.rootDoc.mediaState === media_state.PendingRecording && this._videoRef) { this.toggleRecording(); } }, 100); @@ -218,26 +221,38 @@ export class ScreenshotBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatabl // } return null; } + Record = () => !this._screenCapture && this.toggleRecording(); + Pause = () => this._screenCapture && this.toggleRecording(); + toggleRecording = async () => { if (!this._screenCapture) { this._audioRec = new MediaRecorder(await navigator.mediaDevices.getUserMedia({ audio: true })); const aud_chunks: any = []; this._audioRec.ondataavailable = (e: any) => aud_chunks.push(e.data); this._audioRec.onstop = async (e: any) => { - const [{ result }] = await Networking.UploadFilesToServer(aud_chunks.map((file: any) => ({file}))); + const [{ 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 = () => (this.dataDoc[this.props.fieldKey + '-recordingStart'] = new DateField(new Date())); + this._videoRec.onstart = () => { + if (this.dataDoc[this.props.fieldKey + '_trackScreen']) TrackMovements.Instance.start(); + this.dataDoc[this.props.fieldKey + '_recordingStart'] = new DateField(new Date()); + }; this._videoRec.ondataavailable = (e: any) => vid_chunks.push(e.data); this._videoRec.onstop = async (e: any) => { - console.log('screenshotbox: upload'); + const presentation = TrackMovements.Instance.yieldPresentation(); + if (presentation?.movements) { + const presCopy = { ...presentation }; + presCopy.movements = presentation.movements.map(movement => ({ ...movement, doc: movement.doc[Id] })) as any; + 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 [{ result }] = await Networking.UploadFilesToServer({file}); + const [{ result }] = await Networking.UploadFilesToServer({ file }); this.dataDoc[this.fieldKey + '_duration'] = (new Date().getTime() - this.recordingStart!) / 1000; if (!(result instanceof Error)) { // convert this screenshotBox into normal videoBox @@ -270,14 +285,15 @@ export class ScreenshotBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatabl }; setupDictation = () => { - if (this.dataDoc[this.fieldKey + '-dictation']) return; + 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 textField = Doc.LayoutFieldKey(dictationText); dictationText._layout_autoHeight = false; const dictationTextProto = Doc.GetProto(dictationText); - dictationTextProto.recordingSource = this.dataDoc; - dictationTextProto.recordingStart = ComputedField.MakeFunction(`self.recordingSource["${this.props.fieldKey}-recordingStart"]`); - dictationTextProto.mediaState = ComputedField.MakeFunction('self.recordingSource.mediaState'); - this.dataDoc[this.fieldKey + '-dictation'] = dictationText; + dictationTextProto[`${textField}_recordingSource`] = this.dataDoc; + dictationTextProto[`${textField}_recordingStart`] = ComputedField.MakeFunction(`self.${textField}_recordingSource.${this.fieldKey}_recordingStart`); + dictationTextProto.mediaState = ComputedField.MakeFunction(`self.${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()); @@ -313,11 +329,11 @@ export class ScreenshotBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatabl </> </CollectionFreeFormView> </div> - <div style={{ background: 'white', position: 'relative', height: this.formattedPanelHeight() }}> - {!(this.dataDoc[this.fieldKey + '-dictation'] instanceof Doc) ? null : ( + <div style={{ background: SettingsManager.userColor, position: 'relative', height: this.formattedPanelHeight() }}> + {!(this.dataDoc[this.fieldKey + '_dictation'] instanceof Doc) ? null : ( <FormattedTextBox {...this.props} - Document={DocCast(this.dataDoc[this.fieldKey + '-dictation'])} + Document={DocCast(this.dataDoc[this.fieldKey + '_dictation'])} fieldKey={'text'} PanelHeight={this.formattedPanelHeight} select={emptyFunction} @@ -335,8 +351,8 @@ export class ScreenshotBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatabl </div> </div> {!this.props.isSelected() ? null : ( - <div className="screenshotBox-uiButtons"> - <div className="screenshotBox-recorder" key="snap" onPointerDown={this.toggleRecording}> + <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" /> </div> </div> diff --git a/src/client/views/nodes/ScriptingBox.tsx b/src/client/views/nodes/ScriptingBox.tsx index 3ad3c911d..7c8a1849e 100644 --- a/src/client/views/nodes/ScriptingBox.tsx +++ b/src/client/views/nodes/ScriptingBox.tsx @@ -610,7 +610,6 @@ export class ScriptingBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatable ' ': { dataProvider: (token: any) => this.handleToken(token), component: (blob: any) => { - console.log('Blob', blob); return this.renderFuncListElement(blob.entity); }, output: (item: any, trigger: any) => { @@ -621,7 +620,6 @@ export class ScriptingBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatable '.': { dataProvider: (token: any) => this.handleToken(token), component: (blob: any) => { - console.log('Blob', blob); return this.renderFuncListElement(blob.entity); }, output: (item: any, trigger: any) => { diff --git a/src/client/views/nodes/VideoBox.scss b/src/client/views/nodes/VideoBox.scss index 5e1359441..f803715ad 100644 --- a/src/client/views/nodes/VideoBox.scss +++ b/src/client/views/nodes/VideoBox.scss @@ -84,7 +84,7 @@ width: 0; height: 0; position: relative; - z-index: 100001; + z-index: 2000; } .videoBox-ui { diff --git a/src/client/views/nodes/VideoBox.tsx b/src/client/views/nodes/VideoBox.tsx index 1f52c2d92..d7f7c9b73 100644 --- a/src/client/views/nodes/VideoBox.tsx +++ b/src/client/views/nodes/VideoBox.tsx @@ -26,11 +26,10 @@ import { CollectionStackedTimeline, TrimScope } from '../collections/CollectionS import { ContextMenu } from '../ContextMenu'; import { ContextMenuProps } from '../ContextMenuItem'; import { ViewBoxAnnotatableComponent, ViewBoxAnnotatableProps } from '../DocComponent'; -import { DocumentDecorations } from '../DocumentDecorations'; import { MarqueeAnnotator } from '../MarqueeAnnotator'; import { AnchorMenu } from '../pdf/AnchorMenu'; import { StyleProp } from '../StyleProvider'; -import { OpenWhere } from './DocumentView'; +import { DocFocusOptions, DocumentView, OpenWhere } from './DocumentView'; import { FieldView, FieldViewProps } from './FieldView'; import { RecordingBox } from './RecordingBox'; import { PinProps, PresBox } from './trails'; @@ -58,6 +57,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp 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 @@ -103,14 +103,14 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp // 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.props.Document[this.props.fieldKey + '_audio'], AudioField, null); const vfield = Cast(this.dataDoc[this.fieldKey], VideoField, null); return field?.url.href ?? vfield?.url.href ?? ''; } // returns the presentation data if it exists, null otherwise @computed get presentation() { - const data = this.dataDoc[this.fieldKey + '-presentation']; + const data = this.dataDoc[this.fieldKey + '_presentation']; return data ? JSON.parse(StrCast(data)) : null; } @@ -125,6 +125,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp } componentDidMount() { + this.unmounting = false; this.props.setContentView?.(this); // this tells the DocumentView that this VideoBox is the "content" of the document. this allows the DocumentView to indirectly call getAnchor() on the VideoBox when making a link. if (this.youtubeVideoId) { const youtubeaspect = 400 / 315; @@ -136,7 +137,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp this.layoutDoc._height = NumCast(this.layoutDoc._width) / youtubeaspect; } } - this.player && this.setPlayheadTime(0); + this.player && this.setPlayheadTime(this.timeline.clipStart || 0); document.addEventListener('keydown', this.keyEvents, true); if (this.presentation) { @@ -145,6 +146,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp } componentWillUnmount() { + this.unmounting = true; this.removeCurrentlyPlaying(); this.Pause(); Object.keys(this._disposers).forEach(d => this._disposers[d]?.()); @@ -192,7 +194,6 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp this._finished = false; start = this.timeline.trimStart; } - try { this._audioPlayer && this.player && (this._audioPlayer.currentTime = this.player?.currentTime); update && this.player && this.playFrom(start, undefined, true); @@ -387,7 +388,9 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp const timecode = Cast(this.layoutDoc._layout_currentTimecode, 'number', null); const marquee = AnchorMenu.Instance.GetAnchor?.(undefined, addAsAnnotation); if (!addAsAnnotation && marquee) marquee.backgroundColor = 'transparent'; - const anchor = CollectionStackedTimeline.createAnchor(this.rootDoc, this.dataDoc, this.annotationKey, timecode ? timecode : undefined, undefined, marquee, addAsAnnotation) || this.rootDoc; + 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); return anchor; }; @@ -409,7 +412,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp // updates video time @action updateTimecode = () => { - this.player && (this.layoutDoc._layout_currentTimecode = this.player.currentTime); + !this.unmounting && this.player && (this.layoutDoc._layout_currentTimecode = this.player.currentTime); try { this._youtubePlayer && (this.layoutDoc._layout_currentTimecode = this._youtubePlayer.getCurrentTime?.()); } catch (e) { @@ -524,7 +527,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp icon: 'expand-arrows-alt', }); // if the videobox was turned from a recording box - if (this.dataDoc[this.fieldKey + '-recorded'] === true) { + if (this.dataDoc[this.fieldKey + '_recorded'] === true) { subitems.push({ description: 'Recreate recording', event: () => { @@ -533,7 +536,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp this.dataDoc[this.props.fieldKey] = ''; this.dataDoc[this.fieldKey + '_duration'] = ''; // delete assoicated presentation data - this.dataDoc[this.fieldKey + '-presentation'] = ''; + this.dataDoc[this.fieldKey + '_presentation'] = ''; }, icon: 'expand-arrows-alt', }); @@ -624,7 +627,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp () => !this._playing && this.Seek(NumCast(this.layoutDoc._layout_currentTimecode)) ); this._disposers.youtubeReactionDisposer = reaction( - () => Doc.ActiveTool === InkTool.None && this.props.isSelected(true) && !SnappingManager.GetIsDragging() && !DocumentDecorations.Instance.Interacting, + () => Doc.ActiveTool === InkTool.None && this.props.isSelected(true) && !SnappingManager.GetIsDragging() && !DocumentView.Interacting, interactive => (iframe.style.pointerEvents = interactive ? 'all' : 'none'), { fireImmediately: true } ); @@ -848,11 +851,11 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp zoom = (zoom: number) => this.timeline?.setZoom(zoom); // plays link - playLink = (doc: Doc) => { - const startTime = Math.max(0, this._stackedTimeline?.anchorStart(doc) || 0); + playLink = (doc: Doc, options: DocFocusOptions) => { + const startTime = Math.max(0, NumCast(doc.config_clipStart, this._stackedTimeline?.anchorStart(doc) || 0)); const endTime = this.timeline?.anchorEnd(doc); if (startTime !== undefined) { - if (!this.layoutDoc.dontAutoPlayFollowedLinks) endTime ? this.playFrom(startTime, endTime) : this.playFrom(startTime); + if (options.playMedia) endTime ? this.playFrom(startTime, endTime) : this.playFrom(startTime); else this.Seek(startTime); } }; @@ -959,7 +962,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp {...this.props} dataFieldKey={this.fieldKey} fieldKey={this.annotationKey} - dictationKey={this.fieldKey + '-dictation'} + dictationKey={this.fieldKey + '_dictation'} mediaPath={this.audiopath} thumbnails={() => StrListCast(this.dataDoc[this.fieldKey + '_thumbnails'])} renderDepth={this.props.renderDepth + 1} @@ -1040,7 +1043,6 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp this.props.bringToFront(cropping); return cropping; }; - savedAnnotations = () => this._savedAnnotations; render() { const borderRad = this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.BorderRounding); @@ -1078,8 +1080,8 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp fieldKey={this.annotationKey} isAnnotationOverlay={true} annotationLayerHostsContent={true} - PanelWidth={this.panelWidth} - PanelHeight={this.panelHeight} + PanelWidth={this.props.PanelWidth} + PanelHeight={this.props.PanelHeight} isAnyChildContentActive={returnFalse} ScreenToLocalTransform={this.screenToLocalTransform} childFilters={this.timelineDocFilter} @@ -1120,7 +1122,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp @computed get UIButtons() { const bounds = this.props.docViewPath().lastElement().getBounds(); const width = (bounds?.right || 0) - (bounds?.left || 0); - const curTime = NumCast(this.layoutDoc._layout_currentTimecode) - (this.timeline?.clipStart || 0); + const curTime = NumCast(this.layoutDoc._layout_currentTimecode); return ( <> <div className="videobox-button" title={this._playing ? 'play' : 'pause'} onPointerDown={this.onPlayDown}> @@ -1129,7 +1131,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp {this.timeline && width > 150 && ( <div className="timecode-controls"> - <div className="timecode-current">{formatTime(curTime)}</div> + <div className="timecode-current">{formatTime(curTime - (this.timeline?.clipStart || 0))}</div> {this._fullScreen || (this.heightPercent === 100 && width > 200) ? ( <div className="timeline-slider"> diff --git a/src/client/views/nodes/WebBox.tsx b/src/client/views/nodes/WebBox.tsx index 34a1229ba..27c19105f 100644 --- a/src/client/views/nodes/WebBox.tsx +++ b/src/client/views/nodes/WebBox.tsx @@ -50,8 +50,8 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps public static openSidebarWidth = 250; public static sidebarResizerWidth = 5; static webStyleSheet = addStyleSheet(); - private _setPreviewCursor: undefined | ((x: number, y: number, drag: boolean, hide: boolean) => void); - private _setBrushViewer: undefined | ((view: { width: number; height: number; panX: number; panY: number }) => void); + 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 _disposers: { [name: string]: IReactionDisposer } = {}; @@ -91,7 +91,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps return DocListCast(this.dataDoc[this.annotationKey]); } @computed get inlineTextAnnotations() { - return this.allAnnotations.filter(a => a.textInlineAnnotations); + return this.allAnnotations.filter(a => a.text_inlineAnnotations); } @computed get webField() { return Cast(this.rootDoc[this.props.fieldKey], WebField)?.url; @@ -125,11 +125,15 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps this._searchRef.current?.setRangeText(searchString); }); } - if (clear) { - this._iframe?.contentWindow?.getSelection()?.empty(); - } - if (searchString) { - (this._iframe?.contentWindow as any)?.find(searchString, false, bwd, true); + try { + if (clear) { + this._iframe?.contentWindow?.getSelection()?.empty(); + } + if (searchString) { + (this._iframe?.contentWindow as any)?.find(searchString, false, bwd, true); + } + } catch (e) { + console.log('WebBox search error', e); } return true; }; @@ -192,8 +196,13 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps }); this._disposers.urlchange = reaction( () => WebCast(this.rootDoc.data), + url => this.submitURL(false, false) + ); + this._disposers.titling = reaction( + () => StrCast(this.rootDoc.title), url => { - this.submitURL(url.url.href, false, false); + url.startsWith('www') && this.setData('http://' + url); + url.startsWith('http') && this.setData(url); } ); @@ -277,8 +286,8 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps return this._savedAnnotations; }; - setBrushViewer = (func?: (view: { width: number; height: number; panX: number; panY: number }) => void) => (this._setBrushViewer = func); - brushView = (view: { width: number; height: number; panX: number; panY: number }) => this._setBrushViewer?.(view); + 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); @@ -299,7 +308,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps 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(); - const webUrl = WebCast(doc.presData)?.url; + const webUrl = WebCast(doc.config_data)?.url; if (this._url && webUrl && webUrl.href !== this._url) this.setData(webUrl.href); if (this._sidebarRef?.current?.makeDocUnfiltered(doc) && !this.SidebarShown) this.toggleSidebar(false); return new Promise<Opt<DocumentView>>(res => DocumentManager.Instance.AddViewRenderedCb(doc, dv => res(dv))); @@ -321,8 +330,9 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps ele.append(contents); } } catch (e) {} + const visibleAnchor = this._getAnchor(this._savedAnnotations, false); const anchor = - this._getAnchor(this._savedAnnotations, false) ?? + visibleAnchor ?? Docs.Create.ConfigDocument({ title: StrCast(this.rootDoc.title + ' ' + this.layoutDoc._layout_scrollTop), y: NumCast(this.layoutDoc._layout_scrollTop), @@ -330,9 +340,8 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps }); PresBox.pinDocView(anchor, { pinDocLayout: pinProps?.pinDocLayout, pinData: { ...(pinProps?.pinData ?? {}), scrollable: pinProps?.pinData ? true : false, pannable: true } }, this.rootDoc); anchor.text = ele?.textContent ?? ''; - anchor.textHtml = ele?.innerHTML; - //addAsAnnotation && - this.addDocumentWrapper(anchor); + anchor.text_html = ele?.innerHTML; + addAsAnnotation && this.addDocumentWrapper(anchor); return anchor; }; @@ -360,14 +369,13 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps }; @action iframeDown = (e: PointerEvent) => { - const sel = this._iframe?.contentWindow?.getSelection?.(); 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._setPreviewCursor?.(e.clientX, e.clientY, false, true, this.rootDoc); 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) { + 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 @@ -413,6 +421,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps } _iframetimeout: any = undefined; + @observable _warning = 0; @action iframeLoaded = (e: any) => { const iframe = this._iframe; @@ -426,6 +435,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps try { href = iframe?.contentWindow?.location.href; } catch (e) { + runInAction(() => this._warning++); href = undefined; } let requrlraw = decodeURIComponent(href?.replace(Utils.prepend('') + '/corsProxy/', '') ?? this._url.toString()); @@ -458,12 +468,19 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps // { passive: false } // ); const initHeights = () => { - this._scrollHeight = Math.max(this._scrollHeight, (iframeContent.body.children[0] as any)?.scrollHeight || 0); + 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)); } }; + const swidth = Math.max(NumCast(this.layoutDoc.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; + } initHeights(); this._iframetimeout && clearTimeout(this._iframetimeout); this._iframetimeout = setTimeout( @@ -541,7 +558,6 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps goTo = (scrollTop: number, duration: number, easeFunc: 'linear' | 'ease' | undefined) => { if (this._outerRef.current) { - const iframeHeight = Math.max(scrollTop, this._scrollHeight - this.panelHeight()); if (duration) { smoothScroll(duration, [this._outerRef.current], scrollTop, easeFunc); this.setDashScrollTop(scrollTop, duration); @@ -606,9 +622,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps ); }; @action - submitURL = (newUrl?: string, preview?: boolean, dontUpdateIframe?: boolean) => { - if (!newUrl) return; - if (!newUrl.startsWith('http')) newUrl = 'http://' + newUrl; + submitURL = (preview?: boolean, dontUpdateIframe?: boolean) => { try { if (!preview) { if (this._webPageHasBeenRendered) { @@ -664,9 +678,9 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps !Doc.noviceMode && funcs.push({ description: (this.layoutDoc[this.fieldKey + '_useCors'] ? "Don't Use" : 'Use') + ' Cors', event: () => (this.layoutDoc[this.fieldKey + '_useCors'] = !this.layoutDoc[this.fieldKey + '_useCors']), icon: 'snowflake' }); funcs.push({ - description: (this.layoutDoc.allowScripts ? 'Prevent' : 'Allow') + ' Scripts', + description: (this.dataDoc[this.fieldKey + '_allowScripts'] ? 'Prevent' : 'Allow') + ' Scripts', event: () => { - this.layoutDoc.allowScripts = !this.layoutDoc.allowScripts; + this.dataDoc[this.fieldKey + '_allowScripts'] = !this.dataDoc[this.fieldKey + '_allowScripts']; if (this._iframe) { runInAction(() => (this._hackHide = true)); setTimeout(action(() => (this._hackHide = false))); @@ -715,7 +729,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps if (sel?.empty) sel.empty(); // Chrome else if (sel?.removeAllRanges) sel.removeAllRanges(); // Firefox if (x !== undefined && y !== undefined) { - this._setPreviewCursor?.(x, y, false, false); + this._setPreviewCursor?.(x, y, false, false, this.rootDoc); ContextMenu.Instance.closeMenu(); ContextMenu.Instance.setIgnoreEvents(false); if (e?.button === 2 || e?.altKey) { @@ -740,16 +754,20 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps } if (field instanceof WebField) { const url = this.layoutDoc[this.fieldKey + '_useCors'] ? Utils.CorsProxy(this._webUrl) : this._webUrl; + const scripts = this.dataDoc[this.fieldKey + '_allowScripts'] || this._webUrl.includes('wikipedia.org') || this._webUrl.includes('google.com') || this._webUrl.startsWith('https://bing'); + //if (!scripts) console.log('No scripts for: ' + url); return ( <iframe + key={this._warning} className="webBox-iframe" ref={action((r: HTMLIFrameElement | null) => (this._iframe = r))} + style={{ pointerEvents: this._isAnyChildContentActive || DocumentView.Interacting ? '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. // the 'allow-top-navigation' and 'allow-top-navigation-by-user-activation' attributes are left out to prevent iframes from redirecting the top-level Dash page // sandbox={"allow-forms allow-modals allow-orientation-lock allow-pointer-lock allow-popups allow-popups-to-escape-sandbox allow-presentation allow-same-origin allow-scripts"} />; - sandbox={`${this.layoutDoc.allowScripts ? 'allow-scripts' : ''} allow-forms allow-modals allow-orientation-lock allow-pointer-lock allow-popups allow-popups-to-escape-sandbox allow-presentation allow-same-origin`} + sandbox={`${scripts ? 'allow-scripts' : ''} allow-forms allow-modals allow-orientation-lock allow-pointer-lock allow-popups allow-popups-to-escape-sandbox allow-presentation allow-same-origin`} /> ); } @@ -757,7 +775,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps } addDocumentWrapper = (doc: Doc | Doc[], annotationKey?: string) => { - (doc instanceof Doc ? [doc] : doc).forEach(doc => (doc.presData = new WebField(this._url))); + (doc instanceof Doc ? [doc] : doc).forEach(doc => (doc.config_data = new WebField(this._url))); return this.addDocument(doc, annotationKey); }; @@ -989,7 +1007,7 @@ 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) => void) => (this._setPreviewCursor = func); + 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)); diff --git a/src/client/views/nodes/WebBoxRenderer.js b/src/client/views/nodes/WebBoxRenderer.js index eb8064780..425ef3e54 100644 --- a/src/client/views/nodes/WebBoxRenderer.js +++ b/src/client/views/nodes/WebBoxRenderer.js @@ -42,21 +42,21 @@ var ForeignHtmlRenderer = function (styleSheets) { url = CorsProxy(new URL(webUrl).origin + inurl); } else if (!inurl.startsWith('http') && !inurl.startsWith('//')) { url = CorsProxy(webUrl + '/' + inurl); - } else if (inurl.startsWith('https')) { + } else if (inurl.startsWith('https') && !inurl.startsWith(window.location.origin)) { url = CorsProxy(inurl); } xhr.open('GET', url); xhr.responseType = 'blob'; xhr.onreadystatechange = async function () { - if (xhr.readyState === 4 && xhr.status === 200) { + if (xhr.readyState === XMLHttpRequest.DONE && xhr.status === 200) { const resBase64 = await binaryStringToBase64(xhr.response); resolve({ resourceUrl: inurl, resourceBase64: resBase64, }); - } else if (xhr.readyState === 4) { + } else if (xhr.readyState === XMLHttpRequest.DONE) { console.log("COULDN'T FIND: " + (inurl.startsWith('/') ? webUrl + inurl : inurl)); resolve({ resourceUrl: '', diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.scss b/src/client/views/nodes/formattedText/FormattedTextBox.scss index 109b62e6f..348bdd79e 100644 --- a/src/client/views/nodes/formattedText/FormattedTextBox.scss +++ b/src/client/views/nodes/formattedText/FormattedTextBox.scss @@ -84,7 +84,6 @@ audiotag:hover { height: 11; } -.formattedTextBox-outer-selected, .formattedTextBox-outer { position: relative; overflow: auto; @@ -92,9 +91,6 @@ audiotag:hover { width: 100%; height: unset; } -.formattedTextBox-outer-selected { - cursor: text; -} .formattedTextBox-sidebar-handle { position: absolute; @@ -148,10 +144,8 @@ audiotag:hover { } .formattedTextBox-inner-rounded, -.formattedTextBox-inner-rounded-selected, .formattedTextBox-inner, -.formattedTextBox-inner-minimal, -.formattedTextBox-inner-selected { +.formattedTextBox-inner-minimal { height: 100%; white-space: pre-wrap; .ProseMirror:hover { @@ -169,17 +163,6 @@ audiotag:hover { border-width: 1px; } } -.formattedTextBox-inner-rounded-selected, -.formattedTextBox-inner-selected { - > .ProseMirror { - padding: 10px; - } -} -.formattedTextBox-outer-selected { - > .ProseMirror:hover { - background: unset; - } -} .gpt-typing-wrapper { padding: 10px; @@ -640,7 +623,6 @@ footnote::before { } } - .formattedTextBox-outer-selected, .formattedTextBox-outer { position: relative; overflow: auto; diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx index a0eb328a1..360dfca04 100644 --- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx +++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx @@ -11,9 +11,10 @@ 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 { BsMarkdownFill } from 'react-icons/bs'; import { DateField } from '../../../../fields/DateField'; -import { Doc, DocListCast, StrListCast, Field, Opt } from '../../../../fields/Doc'; -import { AclAdmin, AclAugment, AclEdit, AclSelfEdit, DocCss, Height, Width, ForceServerWrite, UpdatingFromServer } from '../../../../fields/DocSymbols'; +import { Doc, DocListCast, Field, Opt } from '../../../../fields/Doc'; +import { AclAdmin, AclAugment, AclEdit, AclSelfEdit, DocCss, ForceServerWrite, Height, UpdatingFromServer, Width } from '../../../../fields/DocSymbols'; import { Id } from '../../../../fields/FieldSymbols'; import { InkTool } from '../../../../fields/InkField'; import { List } from '../../../../fields/List'; @@ -25,11 +26,10 @@ import { BoolCast, Cast, DocCast, FieldValue, NumCast, ScriptCast, StrCast } fro 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, gptImageCall } from '../../../apis/gpt/GPT'; +import { gptAPICall, GPTCallType } from '../../../apis/gpt/GPT'; import { DocServer } from '../../../DocServer'; import { Docs, DocUtils } from '../../../documents/Documents'; -import { CollectionViewType, DocumentType } from '../../../documents/DocumentTypes'; -import { Networking } from '../../../Network'; +import { CollectionViewType } from '../../../documents/DocumentTypes'; import { DictationManager } from '../../../util/DictationManager'; import { DocumentManager } from '../../../util/DocumentManager'; import { DragManager } from '../../../util/DragManager'; @@ -38,7 +38,7 @@ import { LinkManager } from '../../../util/LinkManager'; import { RTFMarkup } from '../../../util/RTFMarkup'; import { SelectionManager } from '../../../util/SelectionManager'; import { SnappingManager } from '../../../util/SnappingManager'; -import { undoBatch, UndoManager } from '../../../util/UndoManager'; +import { undoable, undoBatch, UndoManager } from '../../../util/UndoManager'; import { CollectionFreeFormView } from '../../collections/collectionFreeForm/CollectionFreeFormView'; import { CollectionStackingView } from '../../collections/CollectionStackingView'; import { CollectionTreeView } from '../../collections/CollectionTreeView'; @@ -49,6 +49,7 @@ import { DocumentButtonBar } from '../../DocumentButtonBar'; 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'; @@ -70,7 +71,10 @@ import { schema } from './schema_rts'; import { SummaryView } from './SummaryView'; import applyDevTools = require('prosemirror-dev-tools'); import React = require('react'); -const translateGoogleApi = require('translate-google-api'); +import { media_state } from '../AudioBox'; +import { setCORS } from 'google-translate-api-browser'; +// 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; @@ -92,10 +96,12 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps static _userStyleSheet: any = addStyleSheet(); static _hadSelection: boolean = false; 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 _finishingLink = false; private _searchIndex = 0; private _lastTimedMark: Mark | undefined = undefined; private _cachedLinks: Doc[] = []; @@ -151,11 +157,11 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps @computed get layout_autoHeightMargins() { return this.titleHeight + NumCast(this.layoutDoc._layout_autoHeightMargins); } - @computed get _recording() { - return this.dataDoc?.mediaState === 'recording'; + @computed get _recordingDictation() { + return this.dataDoc?.mediaState === media_state.Recording; } - set _recording(value) { - !this.dataDoc.recordingSource && (this.dataDoc.mediaState = value ? 'recording' : undefined); + set _recordingDictation(value) { + !this.dataDoc[`${this.fieldKey}_recordingSource`] && (this.dataDoc.mediaState = value ? media_state.Recording : undefined); } @computed get config() { this._keymap = buildKeymap(schema, this.props); @@ -245,7 +251,9 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps if (!pinProps && this._editorView?.state.selection.empty) return this.rootDoc; const anchor = Docs.Create.ConfigDocument({ title: StrCast(this.rootDoc.title), annotationOn: this.rootDoc }); this.addDocument(anchor); + this._finishingLink = true; this.makeLinkAnchor(anchor, OpenWhere.addRight, undefined, 'Anchored Selection', false, addAsAnnotation); + this._finishingLink = false; PresBox.pinDocView(anchor, { pinDocLayout: pinProps?.pinDocLayout, pinData: { ...(pinProps?.pinData ?? {}), scrollable: true } }, this.rootDoc); return anchor; }; @@ -261,19 +269,36 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps AnchorMenu.Instance.OnAudio = (e: PointerEvent) => { !this.layoutDoc.layout_showSidebar && this.toggleSidebar(); const anchor = this.makeLinkAnchor(undefined, OpenWhere.addRight, undefined, 'Anchored Selection', true, true); + setTimeout(() => { const target = this._sidebarRef.current?.anchorMenuClick(anchor); if (target) { anchor.followLinkAudio = true; - DocumentViewInternal.recordAudioAnnotation(Doc.GetProto(target), Doc.LayoutFieldKey(target)); + 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)); + let reactionDisposer = reaction( + () => target.mediaState, + action(dictation => { + if (!dictation) { + Doc.GetProto(target).audioAnnoState = 'stopped'; + stopFunc(); + reactionDisposer(); + } + }) + ); target.title = ComputedField.MakeFunction(`self["text_audioAnnotations_text"].lastElement()`); } }); }; - AnchorMenu.Instance.Highlight = action((color: string, isLinkButton: boolean) => { - this._editorView?.state && RichTextMenu.Instance.setHighlight(color); - return undefined; - }); + AnchorMenu.Instance.Highlight = undoable( + action((color: string, isLinkButton: boolean) => { + this._editorView?.state && RichTextMenu.Instance.setHighlight(color); + return undefined; + }), + 'highlght text' + ); AnchorMenu.Instance.onMakeAnchor = () => this.getAnchor(true); AnchorMenu.Instance.StartCropDrag = unimplementedFunction; /** @@ -326,7 +351,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps textChange && (dataDoc[this.fieldKey + '_modificationDate'] = new DateField(new Date(Date.now()))); if ((!prevData && !protoData) || newText || (!newText && !templateData)) { // if no template, or there's text that didn't come from the layout template, write it to the document. (if this is driven by a template, then this overwrites the template text which is intended) - if (this.props.isContentActive() && removeSelection(newJson) !== removeSelection(prevData?.Data)) { + if ((this._finishingLink || this.props.isContentActive()) && removeSelection(newJson) !== removeSelection(prevData?.Data)) { const numstring = NumCast(dataDoc[this.fieldKey], null); dataDoc[this.fieldKey] = numstring !== undefined ? Number(newText) : new RichTextField(newJson, newText); dataDoc[this.fieldKey + '_noTemplate'] = true; // mark the data field as being split from the template if it has been edited @@ -368,7 +393,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps let link; LinkManager.Links(this.dataDoc).forEach((l, i) => { const anchor = (l.link_anchor_1 as Doc).annotationOn ? (l.link_anchor_1 as Doc) : (l.link_anchor_2 as Doc).annotationOn ? (l.link_anchor_2 as Doc) : undefined; - if (anchor && (anchor.annotationOn as Doc).mediaState === 'recording') { + if (anchor && (anchor.annotationOn as Doc).mediaState === media_state.Recording) { linkTime = NumCast(anchor._timecodeToShow /* audioStart */); linkAnchor = anchor; link = l; @@ -414,7 +439,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps DocListCast(Doc.MyPublishedDocs.data).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(); + // this.prepareForTyping(); } oldAutoLinks.filter(oldLink => !newAutoLinks.has(oldLink) && oldLink.link_anchor_2 !== this.rootDoc).forEach(LinkManager.Instance.deleteLink); }; @@ -435,7 +460,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps const cfield = ComputedField.WithoutComputed(() => FieldValue(this.dataDoc.title)); if (!(cfield instanceof ComputedField)) { - this.dataDoc.title = prefix + str.substring(0, Math.min(40, str.length)) + (str.length > 40 ? '...' : ''); + 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); } @@ -457,8 +482,12 @@ 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.Document).find(link => Doc.AreProtosEqual(Cast(link.link_anchor_1, Doc, null), this.rootDoc) && Doc.AreProtosEqual(Cast(link.link_anchor_2, Doc, null), target)) || - DocUtils.MakeLink(this.props.Document, target, { link_relationship: LinkManager.AutoKeywords })!); + (LinkManager.Links(this.rootDoc).find( + link => + Doc.AreProtosEqual(Cast(link.link_anchor_1, Doc, null), this.rootDoc) && // + Doc.AreProtosEqual(Cast(link.link_anchor_2, Doc, null), target) + ) || + DocUtils.MakeLink(this.rootDoc, target, { link_relationship: LinkManager.AutoKeywords })!); newAutoLinks.add(alink); const allAnchors = [{ href: Doc.localServerPath(target), title: 'a link', anchorId: this.props.Document[Id] }]; allAnchors.push(...(node.marks.find((m: Mark) => m.type.name === schema.marks.autoLinkAnchor.name)?.attrs.allAnchors ?? [])); @@ -566,7 +595,14 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps draggedDoc._freeform_fitContentsToBox = true; Doc.SetContainer(draggedDoc, this.rootDoc); const view = this._editorView!; - view.dispatch(view.state.tr.insert(view.posAtCoords({ left: de.x, top: de.y })!.pos, node)); + try { + 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; + } } } } // otherwise, fall through to outer collection to handle drop @@ -633,7 +669,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps addStyleSheetRule(FormattedTextBox._userStyleSheet, 'UM-remote', { background: 'yellow' }); } if (highlights.includes('My Text')) { - addStyleSheetRule(FormattedTextBox._userStyleSheet, 'UM-' + Doc.CurrentUserEmail.replace('.', '').replace('@', ''), { background: 'moccasin' }); + addStyleSheetRule(FormattedTextBox._userStyleSheet, 'UM-' + Doc.CurrentUserEmail.replace(/\./g, '').replace(/@/g, ''), { background: 'moccasin' }); } if (highlights.includes('Todo Items')) { addStyleSheetRule(FormattedTextBox._userStyleSheet, 'UT-todo', { outline: 'black solid 1px' }); @@ -673,7 +709,10 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps toggleSidebar = (preview: boolean = false) => { const prevWidth = 1 - this.sidebarWidth() / Number(getComputedStyle(this._ref.current!).width.replace('px', '')); if (preview) this._showSidebar = true; - else this.layoutDoc._layout_showSidebar = (this.layoutDoc._layout_sidebarWidthPercent = StrCast(this.layoutDoc._layout_sidebarWidthPercent, '0%') === '0%' ? '50%' : '0%') !== '0%'; + 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._width = !preview && this.SidebarShown ? NumCast(this.layoutDoc._width) * 2 : Math.max(20, NumCast(this.layoutDoc._width) * prevWidth); }; @@ -723,7 +762,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps @undoBatch showTargetTrail = (anchor: Doc) => { - const trail = DocCast(anchor.presTrail); + const trail = DocCast(anchor.presentationTrail); if (trail) { Doc.ActivePresentation = trail; this.props.addDocTab(trail, OpenWhere.replaceRight); @@ -829,7 +868,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps event: () => (this.layoutDoc._layout_enableAltContentUI = !this.layoutDoc._layout_enableAltContentUI), icon: !this.Document._layout_enableAltContentUI ? 'eye-slash' : 'eye', }); - uicontrols.push({ description: 'Show Highlights...', noexpand: true, subitems: highlighting, icon: 'hand-point-right' }); + !Doc.noviceMode && uicontrols.push({ description: 'Show Highlights...', noexpand: true, subitems: highlighting, icon: 'hand-point-right' }); !Doc.noviceMode && uicontrols.push({ description: 'Broadcast Message', @@ -840,7 +879,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps const appearance = cm.findByDescription('Appearance...'); const appearanceItems = appearance && 'subitems' in appearance ? appearance.subitems : []; - appearanceItems.push({ description: 'Change Perspective...', noexpand: true, subitems: changeItems, icon: 'external-link-alt' }); + 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 && @@ -891,16 +930,17 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps event: () => (this.layoutDoc._layout_autoHeight = !this.layoutDoc._layout_autoHeight), icon: this.Document._layout_autoHeight ? 'lock' : 'unlock', }); - optionItems.push({ description: `show markdown options`, event: RTFMarkup.Instance.open, icon: 'text' }); + optionItems.push({ description: `show markdown options`, event: RTFMarkup.Instance.open, icon: <BsMarkdownFill /> }); !options && cm.addItem({ description: 'Options...', subitems: optionItems, icon: 'eye' }); this._downX = this._downY = Number.NaN; }; - animateRes = (resIndex: number) => { - if (resIndex < this.gptRes.length) { - this.dataDoc.text = (this.dataDoc.text as RichTextField)?.Text + this.gptRes[resIndex]; + animateRes = (resIndex: number, newText: string) => { + if (resIndex < newText.length) { + const marks = this._editorView?.state.storedMarks ?? []; + this._editorView?.dispatch(this._editorView.state.tr.setStoredMarks(marks).insertText(newText[resIndex]).setStoredMarks(marks)); setTimeout(() => { - this.animateRes(resIndex + 1); + this.animateRes(resIndex + 1, newText); }, 20); } }; @@ -908,56 +948,35 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps askGPT = action(async () => { try { let res = await gptAPICall((this.dataDoc.text as RichTextField)?.Text, GPTCallType.COMPLETION); - if (res) { - this.gptRes = res; - this.animateRes(0); + if (!res) { + console.error('GPT call failed'); + this.animateRes(0, 'Something went wrong.'); + } else { + this.animateRes(0, res); } } catch (err) { - console.log(err); - this.dataDoc.text = (this.dataDoc.text as RichTextField)?.Text + 'Something went wrong'; + console.error('GPT call failed'); + this.animateRes(0, 'Something went wrong.'); } }); generateImage = async () => { - console.log('Generate image from text: ', (this.dataDoc.text as RichTextField)?.Text); - try { - let image_url = await gptImageCall((this.dataDoc.text as RichTextField)?.Text); - if (image_url) { - const [result] = await Networking.PostToServer('/uploadRemoteImage', { sources: [image_url] }); - const source = Utils.prepend(result.accessPaths.agnostic.client); - const newDoc = Docs.Create.ImageDocument(source, { - x: NumCast(this.rootDoc.x) + NumCast(this.layoutDoc._width) + 10, - y: NumCast(this.rootDoc.y), - _height: 200, - _width: 200, - data_nativeWidth: result.nativeWidth, - data_nativeHeight: result.nativeHeight, - }); - if (Doc.IsInMyOverlay(this.rootDoc)) { - newDoc.overlayX = this.rootDoc.x; - newDoc.overlayY = NumCast(this.rootDoc.y) + NumCast(this.rootDoc._height); - Doc.AddToMyOverlay(newDoc); - } else { - this.props.addDocument?.(newDoc); - } - // Create link between prompt and image - DocUtils.MakeLink(this.rootDoc, newDoc, { link_relationship: 'Image Prompt' }); - } - } catch (err) { - console.log(err); - return ''; - } + GPTPopup.Instance?.setTextAnchor(this.getAnchor(false)); + GPTPopup.Instance?.setImgTargetDoc(this.rootDoc); + GPTPopup.Instance.addToCollection = this.props.addDocument; + GPTPopup.Instance.setImgDesc((this.dataDoc.text as RichTextField)?.Text); + GPTPopup.Instance.generateImage(); }; breakupDictation = () => { - if (this._editorView && this._recording) { + if (this._editorView && this._recordingDictation) { this.stopDictation(true); this._break = true; const state = this._editorView.state; const to = state.selection.to; const updated = TextSelection.create(state.doc, to, to); this._editorView.dispatch(state.tr.setSelection(updated).insertText('\n', to)); - if (this._recording) { + if (this._recordingDictation) { this.recordDictation(); } } @@ -1130,7 +1149,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._cachedLinks = LinkManager.Links(this.Document); - this._disposers.breakupDictation = reaction(() => DocumentManager.Instance.RecordingEvent, this.breakupDictation); + this._disposers.breakupDictation = reaction(() => Doc.RecordingEvent, this.breakupDictation); this._disposers.layout_autoHeight = reaction( () => this.layout_autoHeight, layout_autoHeight => layout_autoHeight && this.tryUpdateScrollHeight() @@ -1230,7 +1249,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps this._disposers.selected = reaction( () => this.props.isSelected(), action(selected => { - selected && this.prepareForTyping(); + //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 } @@ -1241,26 +1260,20 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps RichTextMenu.Instance?.updateMenu(this._editorView, undefined, this.props); setTimeout(this.autoLink, 20); } - // Accessing editor and text doc for gpt assisted text edits - if (this._editorView && selected) { - AnchorMenu.Instance?.setEditorView(this._editorView); - AnchorMenu.Instance?.setTextDoc(this.dataDoc); - } }), { fireImmediately: true } ); if (!this.props.dontRegisterView) { this._disposers.record = reaction( - () => this._recording, + () => this._recordingDictation, () => { this.stopDictation(true); - if (this._recording) { - this.recordDictation(); - } - } + this._recordingDictation && this.recordDictation(); + }, + { fireImmediately: true } ); - if (this._recording) setTimeout(this.recordDictation); + if (this._recordingDictation) setTimeout(this.recordDictation); } var quickScroll: string | undefined = ''; this._disposers.scroll = reaction( @@ -1543,21 +1556,24 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps // add user mark for any first character that was typed since the user mark that gets set in KeyPress won't have been called yet. prepareForTyping = () => { - this._editorView?.dispatch( - this._editorView?.state.tr.setStoredMarks([ - ...(this._editorView.state.storedMarks?.filter(mark => ![schema.marks.em, schema.marks.underline, schema.marks.pFontFamily, schema.marks.pFontSize, schema.marks.strong, schema.marks.pFontColor].includes(mark.type)) ?? []), - ...[schema.marks.user_mark.create({ userid: Doc.CurrentUserEmail, modified: Math.floor(Date.now() / 1000) })], - ...(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().fontWeight === 'bold' ? [schema.mark(schema.marks.strong)] : []), - ]) - ); + if (!this._editorView) return; + const docDefaultMarks = [ + ...(Doc.UserDoc().fontColor !== 'transparent' && Doc.UserDoc().fontColor ? [schema.mark(schema.marks.pFontColor, { color: StrCast(Doc.UserDoc().fontColor) })] : []), + ...(Doc.UserDoc().fontStyle === 'italics' ? [schema.mark(schema.marks.em)] : []), + ...(Doc.UserDoc().textDecoration === 'underline' ? [schema.mark(schema.marks.underline)] : []), + ...(Doc.UserDoc().fontFamily ? [schema.mark(schema.marks.pFontFamily, { family: this.props.styleProvider?.(this.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().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; + } Object.values(this._disposers).forEach(disposer => disposer?.()); this.endUndoTypingBatch(); FormattedTextBox.LiveTextUndo?.end(); @@ -1570,7 +1586,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps onPointerDown = (e: React.PointerEvent): void => { if ((e.nativeEvent as any).handledByInnerReactInstance) { - return e.stopPropagation(); + return; //e.stopPropagation(); } else (e.nativeEvent as any).handledByInnerReactInstance = true; if (this.Document.forceActive) e.stopPropagation(); @@ -1594,20 +1610,16 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps } }); } - if (this._recording && !e.ctrlKey && e.button === 0) { + if (this._recordingDictation && !e.ctrlKey && e.button === 0) { this.breakupDictation(); - e.preventDefault(); - e.stopPropagation(); } this._downX = e.clientX; this._downY = e.clientY; FormattedTextBoxComment.textBox = this; if (e.button === 0 && (this.props.rootSelected(true) || this.props.isSelected(true)) && !e.altKey && !e.ctrlKey && !e.metaKey) { if (e.clientX < this.ProseRef!.getBoundingClientRect().right) { - // stop propagation if not in sidebar - // bcz: Change. drag selecting requires that preventDefault is NOT called. This used to happen in DocumentView, - // but that's changed, so this shouldn't be needed. - //e.stopPropagation(); // if the text box is selected, then it consumes all down events + // 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 document.addEventListener('pointerup', this.onSelectEnd); } } @@ -1769,13 +1781,14 @@ 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.sidebar_collectionType === 'translation' && !this.fieldKey.includes('translation') && curText.endsWith(' ') && curText !== this._lastText) { + if (this.layoutDoc[this.SidebarKey + '_type_collection'] === 'translation' && !this.fieldKey.includes('translation') && curText.endsWith(' ') && curText !== this._lastText) { try { - translateGoogleApi(curText, { from: 'en', to: 'es' }).then((result1: any) => { + translate(curText, { from: 'en', to: 'es' }).then((result1: any) => { setTimeout( () => - translateGoogleApi(result1[0], { from: 'es', to: 'en' }).then((result: any) => { - this.dataDoc[this.fieldKey + '_translation'] = result1 + '\r\n\r\n' + result[0]; + 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 ); @@ -1813,7 +1826,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps if (state.selection.empty || !this._rules!.EnteringStyle) { this._rules!.EnteringStyle = false; } - e.stopPropagation(); + 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))) { @@ -1832,6 +1845,9 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps case 'Tab': e.preventDefault(); break; + case 'c': + this._editorView?.state.selection.empty && (stopPropagation = false); + break; default: if (this._lastTimedMark?.attrs.userid === Doc.CurrentUserEmail) break; case ' ': @@ -1841,6 +1857,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps } break; } + if (stopPropagation) e.stopPropagation(); this.startUndoTypingBatch(); }; ondrop = (e: React.DragEvent) => { @@ -1898,7 +1915,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps .scale(1 / NumCast(this.layoutDoc._freeform_scale, 1) / (this.props.NativeDimScaling?.() || 1)); @computed get audioHandle() { - return !this._recording ? null : ( + return !this._recordingDictation ? null : ( <div className="formattedTextBox-dictation" onPointerDown={e => @@ -1907,7 +1924,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps e, returnFalse, emptyFunction, - action(e => (this._recording = !this._recording)) + action(e => (this._recordingDictation = !this._recordingDictation)) ) }> <FontAwesomeIcon className="formattedTextBox-audioFont" style={{ color: 'red' }} icon={'microphone'} size="sm" /> @@ -1959,6 +1976,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps <div onPointerDown={e => setupMoveUpEvents(this, e, returnFalse, emptyFunction, () => SelectionManager.SelectView(this.props.DocumentView?.()!, false), true)}> <ComponentTag {...this.props} + ref={this._sidebarTagRef as any} setContentView={emptyFunction} NativeWidth={returnZero} NativeHeight={returnZero} @@ -1981,25 +1999,25 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps fitContentsToBox={this.fitContentsToBox} noSidebar={true} treeViewHideTitle={true} - fieldKey={this.layoutDoc.sidebar_collectionType === 'translation' ? `${this.fieldKey}_translation` : `${this.fieldKey}_sidebar`} + fieldKey={this.layoutDoc[this.SidebarKey + '_type_collection'] === 'translation' ? `${this.fieldKey}_translation` : `${this.fieldKey}_sidebar`} /> </div> ); }; return ( <div className={'formattedTextBox-sidebar' + (Doc.ActiveTool !== InkTool.None ? '-inking' : '')} style={{ width: `${this.layout_sidebarWidthPercent}`, backgroundColor: `${this.sidebarColor}` }}> - {renderComponent(StrCast(this.layoutDoc.sidebar_collectionType))} + {renderComponent(StrCast(this.layoutDoc[this.SidebarKey + '_type_collection']))} </div> ); } cycleAlternateText = () => { if (this.layoutDoc._layout_enableAltContentUI) { - const usePath = this.rootDoc[`${this.props.fieldKey}_usePath`]; + const usePath = this.rootDoc[`_${this.props.fieldKey}_usePath`]; this.rootDoc[`_${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.rootDoc[`_${this.props.fieldKey}_usePath`]; return ( <Tooltip title={ @@ -2036,24 +2054,47 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps } @observable _isHovering = false; onPassiveWheel = (e: WheelEvent) => { + if (e.clientX > this.ProseRef!.getBoundingClientRect().right) { + if (this.rootDoc[this.SidebarKey + '_type_collection'] === CollectionViewType.Freeform) { + // if the scrolled freeform is a child of the sidebar component, we need to let the event go through + // so react can let the freeform view handle it. We prevent default to stop any containing views from scrolling + e.preventDefault(); + } + return; + } + // if scrollTop is 0, then don't let wheel trigger scroll on any container (which it would since onScroll won't be triggered on this) if (this.props.isContentActive() && !this.props.allowScroll) { - if (!NumCast(this.layoutDoc._layout_scrollTop) && e.deltaY <= 0) e.preventDefault(); + // prevent default if selected || child is active but this doc isn't scrollable + if ( + (this._scrollRef.current?.scrollHeight ?? 0) <= Math.ceil(Number(this.layoutDoc._height)) && // + (this.props.isSelected() || this.isAnyChildContentActive()) + ) { + e.preventDefault(); + } e.stopPropagation(); } }; _oldWheel: any; + @computed get fontColor() { + return this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.Color); + } + @computed get fontSize() { + return this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.FontSize); + } + @computed get fontFamily() { + return this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.FontFamily); + } + @computed get fontWeight() { + return this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.FontWeight); + } render() { TraceMobx(); - const active = this.props.isContentActive(); const scale = (this.props.NativeDimScaling?.() || 1) * NumCast(this.layoutDoc._freeform_scale, 1); const rounded = StrCast(this.layoutDoc.layout_borderRounding) === '100%' ? '-rounded' : ''; - if (!active && FormattedTextBoxComment.textBox === this) setTimeout(FormattedTextBoxComment.Hide); - const minimal = this.props.ignoreAutoHeight; + 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 selPad = (active && !this.layoutDoc._createDocOnCR) || minimal ? Math.min(paddingY, Math.min(paddingX, 10)) : 0; - const selPaddingClass = active && !this.layoutDoc._createDocOnCR && paddingY >= 10 ? '-selected' : ''; 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' > return styleFromLayoutString?.height === '0px' ? null : ( <div @@ -2070,23 +2111,23 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps ? {} : { transform: `scale(${scale})`, - transformOrigin: 'top left', width: `${100 / scale}%`, height: `${100 / scale}%`, }), - display: !this.props.isContentActive() && this.props.thumbShown?.() ? 'none' : undefined, + // display: !this.props.isContentActive() && this.props.thumbShown?.() ? 'none' : undefined, transition: 'inherit', // overflowY: this.layoutDoc._layout_autoHeight ? "hidden" : undefined, - color: this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.Color), - fontSize: this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.FontSize), - fontFamily: this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.FontFamily), - fontWeight: this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.FontWeight), + color: this.fontColor, + fontSize: this.fontSize, + fontFamily: this.fontFamily, + fontWeight: this.fontWeight, ...styleFromLayoutString, }}> <div className="formattedTextBox-cont" ref={this._ref} style={{ + cursor: this.props.isContentActive() ? 'text' : undefined, overflow: this.layout_autoHeight && this.props.CollectionFreeFormDocumentView?.() ? 'hidden' : undefined, //x this breaks viewing an layout_autoHeight doc in its own tab, or in the lightbox 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', @@ -2101,31 +2142,30 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps onPointerDown={this.onPointerDown} onDoubleClick={this.onDoubleClick}> <div - className={`formattedTextBox-outer${active ? '-selected' : ''}`} + className="formattedTextBox-outer" ref={this._scrollRef} style={{ - width: this.props.dontSelectOnLoad ? '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} onDrop={this.ondrop}> <div - className={minimal ? 'formattedTextBox-minimal' : `formattedTextBox-inner${rounded}${selPaddingClass}`} + className={`formattedTextBox-inner${rounded}`} ref={this.createDropTarget} style={{ padding: StrCast(this.layoutDoc._textBoxPadding), - paddingLeft: StrCast(this.layoutDoc._textBoxPaddingX, `${paddingX - selPad}px`), - paddingRight: StrCast(this.layoutDoc._textBoxPaddingX, `${paddingX - selPad}px`), - paddingTop: StrCast(this.layoutDoc._textBoxPaddingY, `${paddingY - selPad}px`), - paddingBottom: StrCast(this.layoutDoc._textBoxPaddingY, `${paddingY - selPad}px`), - // pointerEvents: !active && IsFollowLinkScript(this.layoutDoc.onClick) ? 'none' : undefined, + paddingLeft: StrCast(this.layoutDoc._textBoxPaddingX, `${paddingX}px`), + paddingRight: StrCast(this.layoutDoc._textBoxPaddingX, `${paddingX}px`), + paddingTop: StrCast(this.layoutDoc._textBoxPaddingY, `${paddingY}px`), + paddingBottom: StrCast(this.layoutDoc._textBoxPaddingY, `${paddingY}px`), }} /> </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 ? null : this.sidebarHandle} + {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.overlayAlternateIcon : null} + {this.layoutDoc._layout_enableAltContentUI && !this.layoutDoc._chromeHidden ? this.overlayAlternateIcon : null} </div> </div> ); diff --git a/src/client/views/nodes/formattedText/ProsemirrorExampleTransfer.ts b/src/client/views/nodes/formattedText/ProsemirrorExampleTransfer.ts index 112a0d87e..ec11079b4 100644 --- a/src/client/views/nodes/formattedText/ProsemirrorExampleTransfer.ts +++ b/src/client/views/nodes/formattedText/ProsemirrorExampleTransfer.ts @@ -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 (GetEffectiveAcl(props.DataDoc) != AclEdit && GetEffectiveAcl(props.DataDoc) != AclAugment && GetEffectiveAcl(props.DataDoc) != AclAdmin) return true; + if (props.DataDoc && GetEffectiveAcl(props.DataDoc) != AclEdit && GetEffectiveAcl(props.DataDoc) != AclAugment && GetEffectiveAcl(props.DataDoc) != 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.tsx b/src/client/views/nodes/formattedText/RichTextMenu.tsx index 7c3e4baad..e3ac4fb9d 100644 --- a/src/client/views/nodes/formattedText/RichTextMenu.tsx +++ b/src/client/views/nodes/formattedText/RichTextMenu.tsx @@ -21,6 +21,7 @@ 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 @@ -104,7 +105,7 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> { _disposer: IReactionDisposer | undefined; componentDidMount() { this._disposer = reaction( - () => SelectionManager.Views(), + () => SelectionManager.Views().slice(), views => this.updateMenu(undefined, undefined, undefined) ); } @@ -149,19 +150,31 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> { setMark = (mark: Mark, state: EditorState, dispatch: any, dontToggle: boolean = false) => { if (mark) { - const node = (state.selection as NodeSelection).node; + const liFirst = numberRange(state.selection.$from.depth + 1).find(i => state.selection.$from.node(i)?.type === state.schema.nodes.list_item); + const liTo = numberRange(state.selection.$to.depth + 1).find(i => state.selection.$to.node(i)?.type === state.schema.nodes.list_item); + const olFirst = numberRange(state.selection.$from.depth + 1).find(i => state.selection.$from.node(i)?.type === state.schema.nodes.ordered_list); + const nodeOl = (liFirst && liTo && state.selection.$from.node(liFirst) !== state.selection.$to.node(liTo) && olFirst) || (!liFirst && !liTo && olFirst); + const newPos = nodeOl ? numberRange(state.selection.from).findIndex(i => state.doc.nodeAt(i)?.type === state.schema.nodes.ordered_list) : state.selection.from; + const node = (state.selection as NodeSelection).node ?? (newPos >= 0 ? state.doc.nodeAt(newPos) : undefined); if (node?.type === schema.nodes.ordered_list) { let attrs = node.attrs; if (mark.type === schema.marks.pFontFamily) attrs = { ...attrs, fontFamily: mark.attrs.family }; if (mark.type === schema.marks.pFontSize) attrs = { ...attrs, fontSize: mark.attrs.fontSize }; if (mark.type === schema.marks.pFontColor) attrs = { ...attrs, fontColor: mark.attrs.color }; - const tr = updateBullets(state.tr.setNodeMarkup(state.selection.from, node.type, attrs), state.schema); - dispatch(tr.setSelection(new NodeSelection(tr.doc.resolve(state.selection.from)))); - } else if (dontToggle) { - const tr = state.tr.addMark(state.selection.from, state.selection.to, mark); - dispatch(tr.setSelection(new TextSelection(tr.doc.resolve(state.selection.from), tr.doc.resolve(state.selection.to)))); // bcz: need to redo the selection because ctrl-a selections disappear otherwise - } else { - toggleMark(mark.type, mark.attrs)(state, dispatch); + const tr = updateBullets(state.tr.setNodeMarkup(newPos, node.type, attrs), state.schema); + dispatch(tr.setSelection(new TextSelection(tr.doc.resolve(state.selection.from), tr.doc.resolve(state.selection.to)))); + } + { + const state = this.view?.state; + const tr = this.view?.state.tr; + if (tr && state) { + if (dontToggle) { + tr.addMark(state.selection.from, state.selection.to, mark); + dispatch(tr.setSelection(new TextSelection(tr.doc.resolve(state.selection.from), tr.doc.resolve(state.selection.to)))); // bcz: need to redo the selection because ctrl-a selections disappear otherwise + } else { + toggleMark(mark.type, mark.attrs)(state, dispatch); + } + } } } }; diff --git a/src/client/views/nodes/formattedText/RichTextRules.ts b/src/client/views/nodes/formattedText/RichTextRules.ts index ac1e7ce5d..3e2afd2ce 100644 --- a/src/client/views/nodes/formattedText/RichTextRules.ts +++ b/src/client/views/nodes/formattedText/RichTextRules.ts @@ -243,13 +243,13 @@ export class RichTextRules { // create a text display of a metadata field on this or another document, or create a hyperlink portal to another document // [[<fieldKey> : <Doc>]] - // [[:Doc]] => hyperlink + // [[:docTitle]] => hyperlink // [[fieldKey]] => show field // [[fieldKey=value]] => show field and also set its value - // [[fieldKey:Doc]] => show field of doc + // [[fieldKey:docTitle]] => show field of doc new InputRule(new RegExp(/\[\[([a-zA-Z_\? \-0-9]*)(=[a-zA-Z_@\? /\-0-9]*)?(:[a-zA-Z_@:\.\? \-0-9]+)?\]\]$/), (state, match, start, end) => { const fieldKey = match[1]; - const docId = match[3]?.replace(':', ''); + const docTitle = match[3]?.replace(':', ''); const value = match[2]?.substring(1); const linkToDoc = (target: Doc) => { const rstate = this.TextBox.EditorView?.state; @@ -266,12 +266,12 @@ export class RichTextRules { } }; if (!fieldKey) { - if (docId) { - const target = DocServer.QUERY_SERVER_CACHE(docId); - if (target) setTimeout(() => linkToDoc(target)); - else DocServer.GetRefField(docId).then(docx => linkToDoc((docx instanceof Doc && docx) || Docs.Create.FreeformDocument([], { title: docId + '(auto)', _width: 500, _height: 500 }, docId))); - - return state.tr.deleteRange(end - 1, end).deleteRange(start, start + 3); + if (docTitle) { + const target = DocServer.FindDocByTitle(docTitle); + if (target) { + setTimeout(() => linkToDoc(target)); + return state.tr.deleteRange(end - 1, end).deleteRange(start, start + 3); + } } return state.tr; } @@ -279,7 +279,8 @@ export class RichTextRules { const num = value.match(/^[0-9.]$/); this.Document[DocData][fieldKey] = value === 'true' ? true : value === 'false' ? false : num ? Number(value) : value; } - const fieldView = state.schema.nodes.dashField.create({ fieldKey, docId, hideKey: false }); + const target = DocServer.FindDocByTitle(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/marks_rts.ts b/src/client/views/nodes/formattedText/marks_rts.ts index 7e17008bb..6f07588b3 100644 --- a/src/client/views/nodes/formattedText/marks_rts.ts +++ b/src/client/views/nodes/formattedText/marks_rts.ts @@ -348,7 +348,7 @@ export const marks: { [index: string]: MarkSpec } = { excludes: 'user_mark', group: 'inline', toDOM(node: any) { - const uid = node.attrs.userid.replace('.', '').replace('@', ''); + const uid = node.attrs.userid.replace(/\./g, '').replace(/@/g, ''); const min = Math.round(node.attrs.modified / 60); const hr = Math.round(min / 60); const day = Math.round(hr / 60 / 24); diff --git a/src/client/views/nodes/generativeFill/GenerativeFill.scss b/src/client/views/nodes/generativeFill/GenerativeFill.scss new file mode 100644 index 000000000..c2669a950 --- /dev/null +++ b/src/client/views/nodes/generativeFill/GenerativeFill.scss @@ -0,0 +1,97 @@ +$navHeight: 5rem; +$canvasSize: 1024px; +$scale: 0.5; + +.generativeFillContainer { + position: absolute; + top: 0; + left: 0; + z-index: 9999; + height: 100vh; + width: 100vw; + display: flex; + flex-direction: column; + overflow: hidden; + + .generativeFillControls { + flex-shrink: 0; + height: $navHeight; + color: #000000; + background-color: #ffffff; + z-index: 999; + width: 100%; + display: flex; + gap: 3rem; + justify-content: space-between; + align-items: center; + border-bottom: 1px solid #c7cdd0; + padding: 0 2rem; + + h1 { + font-size: 1.5rem; + } + } + + .drawingArea { + cursor: none; + touch-action: none; + position: relative; + flex-grow: 1; + display: flex; + justify-content: center; + align-items: center; + width: 100%; + background-color: #f0f4f6; + + canvas { + display: block; + position: absolute; + transform-origin: 50% 50%; + } + + .pointer { + pointer-events: none; + position: absolute; + border-radius: 50%; + width: 50px; + height: 50px; + border: 1px solid #ffffff; + transform: translate(-50%, -50%); + display: flex; + justify-content: center; + align-items: center; + + .innerPointer { + width: 100%; + height: 100%; + border: 1px solid #000000; + border-radius: 50%; + } + } + + .iconContainer { + position: absolute; + top: 2rem; + left: 2rem; + display: flex; + flex-direction: column; + gap: 2rem; + } + + .editsBox { + position: absolute; + top: 2rem; + right: 2rem; + display: flex; + flex-direction: column; + gap: 1rem; + + img { + transition: all 0.2s ease-in-out; + &:hover { + opacity: 0.8; + } + } + } + } +} diff --git a/src/client/views/nodes/generativeFill/GenerativeFill.tsx b/src/client/views/nodes/generativeFill/GenerativeFill.tsx new file mode 100644 index 000000000..1ec6d6e3f --- /dev/null +++ b/src/client/views/nodes/generativeFill/GenerativeFill.tsx @@ -0,0 +1,590 @@ +import { Checkbox, FormControlLabel, Slider, TextField } from '@mui/material'; +import { IconButton } from 'browndash-components'; +import { useEffect, useRef, useState } from 'react'; +import { CgClose } from 'react-icons/cg'; +import { IoMdRedo, IoMdUndo } from 'react-icons/io'; +import { Doc, DocListCast } from '../../../../fields/Doc'; +import { List } from '../../../../fields/List'; +import { NumCast } from '../../../../fields/Types'; +import { Utils } from '../../../../Utils'; +import { Docs, DocUtils } from '../../../documents/Documents'; +import { Networking } from '../../../Network'; +import { CollectionDockingView } from '../../collections/CollectionDockingView'; +import { OpenWhereMod } from '../DocumentView'; +import { ImageBox } from '../ImageBox'; +import './GenerativeFill.scss'; +import Buttons from './GenerativeFillButtons'; +import { BrushHandler } from './generativeFillUtils/BrushHandler'; +import { activeColor, canvasSize, eraserColor, freeformRenderSize, newCollectionSize, offsetDistanceY, offsetX } from './generativeFillUtils/generativeFillConstants'; +import { CursorData, ImageDimensions, Point } from './generativeFillUtils/generativeFillInterfaces'; +import { APISuccess, ImageUtility } from './generativeFillUtils/ImageHandler'; +import { PointerHandler } from './generativeFillUtils/PointerHandler'; +import React = require('react'); + +enum BrushStyle { + ADD, + SUBTRACT, + MARQUEE, +} + +interface GenerativeFillProps { + imageEditorOpen: boolean; + imageEditorSource: string; + imageRootDoc: Doc | undefined; + addDoc: ((doc: Doc | Doc[], annotationKey?: string) => boolean) | undefined; +} + +// Added field on image doc: gen_fill_children: List of children Docs + +const GenerativeFill = ({ imageEditorOpen, imageEditorSource, imageRootDoc, addDoc }: GenerativeFillProps) => { + const canvasRef = useRef<HTMLCanvasElement>(null); + const canvasBackgroundRef = useRef<HTMLCanvasElement>(null); + const drawingAreaRef = useRef<HTMLDivElement>(null); + const [cursorData, setCursorData] = useState<CursorData>({ + x: 0, + y: 0, + width: 150, + }); + const [isBrushing, setIsBrushing] = useState(false); + const [canvasScale, setCanvasScale] = useState(0.5); + // format: array of [image source, corresponding image Doc] + const [edits, setEdits] = useState<(string | Doc)[][]>([]); + const [edited, setEdited] = useState(false); + const [brushStyle, setBrushStyle] = useState<BrushStyle>(BrushStyle.ADD); + const [input, setInput] = useState(''); + const [loading, setLoading] = useState(false); + const [canvasDims, setCanvasDims] = useState<ImageDimensions>({ + width: canvasSize, + height: canvasSize, + }); + // whether to create a new collection or not + const [isNewCollection, setIsNewCollection] = useState(true); + // the current image in the main canvas + const currImg = useRef<HTMLImageElement | null>(null); + // the unedited version of each generation (parent) + const originalImg = useRef<HTMLImageElement | null>(null); + const originalDoc = useRef<Doc | null>(null); + // stores history of data urls + const undoStack = useRef<string[]>([]); + // stores redo stack + const redoStack = useRef<string[]>([]); + + // references to keep track of tree structure + const newCollectionRef = useRef<Doc | null>(null); + const parentDoc = useRef<Doc | null>(null); + const childrenDocs = useRef<Doc[]>([]); + + // Undo and Redo + const handleUndo = () => { + const ctx = ImageUtility.getCanvasContext(canvasRef); + if (!ctx || !currImg.current || !canvasRef.current) return; + + const target = undoStack.current[undoStack.current.length - 1]; + if (!target) { + ImageUtility.drawImgToCanvas(currImg.current, canvasRef, canvasDims.width, canvasDims.height); + } else { + redoStack.current = [...redoStack.current, canvasRef.current.toDataURL()]; + const img = new Image(); + img.src = target; + ImageUtility.drawImgToCanvas(img, canvasRef, canvasDims.width, canvasDims.height); + undoStack.current = undoStack.current.slice(0, -1); + } + }; + + const handleRedo = () => { + const ctx = ImageUtility.getCanvasContext(canvasRef); + if (!ctx || !currImg.current || !canvasRef.current) return; + + const target = redoStack.current[redoStack.current.length - 1]; + if (!target) { + } else { + undoStack.current = [...undoStack.current, canvasRef.current?.toDataURL()]; + const img = new Image(); + img.src = target; + ImageUtility.drawImgToCanvas(img, canvasRef, canvasDims.width, canvasDims.height); + redoStack.current = redoStack.current.slice(0, -1); + } + }; + + // resets any erase strokes + const handleReset = () => { + if (!canvasRef.current || !currImg.current) return; + const ctx = ImageUtility.getCanvasContext(canvasRef); + if (!ctx) return; + ctx.clearRect(0, 0, canvasSize, canvasSize); + undoStack.current = []; + redoStack.current = []; + ImageUtility.drawImgToCanvas(currImg.current, canvasRef, canvasDims.width, canvasDims.height); + }; + + // initiate brushing + const handlePointerDown = (e: React.PointerEvent) => { + const canvas = canvasRef.current; + if (!canvas) return; + const ctx = ImageUtility.getCanvasContext(canvasRef); + if (!ctx) return; + + undoStack.current = [...undoStack.current, canvasRef.current.toDataURL()]; + redoStack.current = []; + + setIsBrushing(true); + const { x, y } = PointerHandler.getPointRelativeToElement(canvas, e, canvasScale); + BrushHandler.brushCircleOverlay(x, y, cursorData.width / 2 / canvasScale, ctx, eraserColor, brushStyle === BrushStyle.SUBTRACT); + }; + + // stop brushing, push to undo stack + const handlePointerUp = (e: React.PointerEvent) => { + const ctx = ImageUtility.getCanvasContext(canvasBackgroundRef); + if (!ctx) return; + if (!isBrushing) return; + setIsBrushing(false); + }; + + // handles brushing on pointer movement + useEffect(() => { + if (!isBrushing) return; + const canvas = canvasRef.current; + if (!canvas) return; + const ctx = ImageUtility.getCanvasContext(canvasRef); + if (!ctx) return; + + const handlePointerMove = (e: PointerEvent) => { + const currPoint = PointerHandler.getPointRelativeToElement(canvas, e, canvasScale); + const lastPoint: Point = { + x: currPoint.x - e.movementX / canvasScale, + y: currPoint.y - e.movementY / canvasScale, + }; + BrushHandler.createBrushPathOverlay(lastPoint, currPoint, cursorData.width / 2 / canvasScale, ctx, eraserColor, brushStyle === BrushStyle.SUBTRACT); + }; + + drawingAreaRef.current?.addEventListener('pointermove', handlePointerMove); + return () => { + drawingAreaRef.current?.removeEventListener('pointermove', handlePointerMove); + }; + }, [isBrushing]); + + // first load + useEffect(() => { + const loadInitial = async () => { + if (!imageEditorSource || imageEditorSource === '') return; + const img = new Image(); + const res = await ImageUtility.urlToBase64(imageEditorSource); + if (!res) return; + img.src = `data:image/png;base64,${res}`; + + img.onload = () => { + currImg.current = img; + originalImg.current = img; + const imgWidth = img.naturalWidth; + const imgHeight = img.naturalHeight; + const scale = Math.min(canvasSize / imgWidth, canvasSize / imgHeight); + const width = imgWidth * scale; + const height = imgHeight * scale; + setCanvasDims({ width, height }); + }; + }; + + loadInitial(); + + // cleanup + return () => { + setInput(''); + setEdited(false); + newCollectionRef.current = null; + parentDoc.current = null; + childrenDocs.current = []; + currImg.current = null; + originalImg.current = null; + originalDoc.current = null; + undoStack.current = []; + redoStack.current = []; + ImageUtility.clearCanvas(canvasRef); + }; + }, [canvasRef, imageEditorSource]); + + // once the appropriate dimensions are set, draw the image to the canvas + useEffect(() => { + if (!currImg.current) return; + ImageUtility.drawImgToCanvas(currImg.current, canvasRef, canvasDims.width, canvasDims.height); + }, [canvasDims]); + + // handles brush sizing + useEffect(() => { + const handleKeyPress = (e: KeyboardEvent) => { + if (e.key === 'ArrowUp') { + e.preventDefault(); + e.stopPropagation(); + setCursorData(data => ({ ...data, width: data.width + 5 })); + } else if (e.key === 'ArrowDown') { + e.preventDefault(); + e.stopPropagation(); + setCursorData(data => (data.width >= 20 ? { ...data, width: data.width - 5 } : data)); + } + }; + window.addEventListener('keydown', handleKeyPress); + return () => window.removeEventListener('keydown', handleKeyPress); + }, []); + + // handle pinch zoom + useEffect(() => { + const handlePinch = (e: WheelEvent) => { + e.preventDefault(); + e.stopPropagation(); + const delta = e.deltaY; + const scaleFactor = delta > 0 ? 0.98 : 1.02; + setCanvasScale(prevScale => prevScale * scaleFactor); + }; + + drawingAreaRef.current?.addEventListener('wheel', handlePinch, { + passive: false, + }); + return () => drawingAreaRef.current?.removeEventListener('wheel', handlePinch); + }, [drawingAreaRef]); + + // updates the current position of the cursor + const updateCursorData = (e: React.PointerEvent) => { + const drawingArea = drawingAreaRef.current; + if (!drawingArea) return; + const { x, y } = PointerHandler.getPointRelativeToElement(drawingArea, e, 1); + setCursorData(data => ({ + ...data, + x, + y, + })); + }; + + // Get AI Edit + const getEdit = async () => { + const img = currImg.current; + if (!img) return; + const canvas = canvasRef.current; + if (!canvas) return; + const ctx = ImageUtility.getCanvasContext(canvasRef); + if (!ctx) return; + setLoading(true); + setEdited(true); + try { + const canvasOriginalImg = ImageUtility.getCanvasImg(img); + if (!canvasOriginalImg) return; + const canvasMask = ImageUtility.getCanvasMask(canvas, canvasOriginalImg); + if (!canvasMask) return; + const maskBlob = await ImageUtility.canvasToBlob(canvasMask); + const imgBlob = await ImageUtility.canvasToBlob(canvasOriginalImg); + const res = await ImageUtility.getEdit(imgBlob, maskBlob, input !== '' ? input + ' in the same style' : 'Fill in the image in the same style', 2); + // const res = await ImageUtility.mockGetEdit(img.src); + + // create first image + if (!newCollectionRef.current) { + if (!isNewCollection && imageRootDoc) { + // if the parent hasn't been set yet + if (!parentDoc.current) parentDoc.current = imageRootDoc; + } else { + if (!(originalImg.current && imageRootDoc)) return; + // create new collection and add it to the view + newCollectionRef.current = Docs.Create.FreeformDocument([], { + x: NumCast(imageRootDoc.x) + NumCast(imageRootDoc._width) + offsetX, + y: NumCast(imageRootDoc.y), + _width: newCollectionSize, + _height: newCollectionSize, + title: 'Image edit collection', + }); + DocUtils.MakeLink(imageRootDoc, newCollectionRef.current, { link_relationship: 'Image Edit Version History', link_displayLine: false }); + + // opening new tab + CollectionDockingView.AddSplit(newCollectionRef.current, OpenWhereMod.right); + + // add the doc to the main freeform + await createNewImgDoc(originalImg.current, true); + } + } else { + childrenDocs.current = []; + } + + originalImg.current = currImg.current; + originalDoc.current = parentDoc.current; + const { urls } = res as APISuccess; + const imgUrls = await Promise.all(urls.map(url => ImageUtility.convertImgToCanvasUrl(url, canvasDims.width, canvasDims.height))); + const imgRes = await Promise.all( + imgUrls.map(async url => { + const saveRes = await onSave(url); + return [url, saveRes as Doc]; + }) + ); + setEdits(imgRes); + const image = new Image(); + image.src = imgUrls[0]; + ImageUtility.drawImgToCanvas(image, canvasRef, canvasDims.width, canvasDims.height); + currImg.current = image; + parentDoc.current = imgRes[0][1] as Doc; + } catch (err) { + console.log(err); + } + setLoading(false); + }; + + // adjusts all the img positions to be aligned + const adjustImgPositions = () => { + if (!parentDoc.current) return; + const startY = NumCast(parentDoc.current.y); + const children = DocListCast(parentDoc.current.gen_fill_children); + const len = children.length; + let initialYPositions: number[] = []; + for (let i = 0; i < len; i++) { + initialYPositions.push(startY + i * offsetDistanceY); + } + children.forEach((doc, i) => { + if (len % 2 === 1) { + doc.y = initialYPositions[i] - Math.floor(len / 2) * offsetDistanceY; + } else { + doc.y = initialYPositions[i] - (len / 2 - 1 / 2) * offsetDistanceY; + } + }); + }; + + // creates a new image document and returns its reference + const createNewImgDoc = async (img: HTMLImageElement, firstDoc: boolean): Promise<Doc | undefined> => { + if (!imageRootDoc) return; + const src = img.src; + const [result] = await Networking.PostToServer('/uploadRemoteImage', { sources: [src] }); + const source = Utils.prepend(result.accessPaths.agnostic.client); + + if (firstDoc) { + const x = 0; + const initialY = 0; + const newImg = Docs.Create.ImageDocument(source, { + x: x, + y: initialY, + _height: freeformRenderSize, + _width: freeformRenderSize, + data_nativeWidth: result.nativeWidth, + data_nativeHeight: result.nativeHeight, + }); + if (isNewCollection && newCollectionRef.current) { + Doc.AddDocToList(newCollectionRef.current, undefined, newImg); + } else { + addDoc?.(newImg); + } + parentDoc.current = newImg; + return newImg; + } else { + if (!parentDoc.current) return; + const x = NumCast(parentDoc.current.x) + freeformRenderSize + offsetX; + const initialY = 0; + + const newImg = Docs.Create.ImageDocument(source, { + x: x, + y: initialY, + _height: freeformRenderSize, + _width: freeformRenderSize, + data_nativeWidth: result.nativeWidth, + data_nativeHeight: result.nativeHeight, + }); + + const parentList = DocListCast(parentDoc.current.gen_fill_children); + if (parentList.length > 0) { + parentList.push(newImg); + parentDoc.current.gen_fill_children = new List<Doc>(parentList); + } else { + parentDoc.current.gen_fill_children = new List<Doc>([newImg]); + } + + DocUtils.MakeLink(parentDoc.current, newImg, { link_relationship: `Image edit; Prompt: ${input}`, link_displayLine: true }); + adjustImgPositions(); + + if (isNewCollection && newCollectionRef.current) { + Doc.AddDocToList(newCollectionRef.current, undefined, newImg); + } else { + addDoc?.(newImg); + } + return newImg; + } + }; + + // Saves an image to the collection + const onSave = async (src: string) => { + const img = new Image(); + img.src = src; + if (!currImg.current || !originalImg.current || !imageRootDoc) return; + try { + const res = await createNewImgDoc(img, false); + return res; + } catch (err) { + console.log(err); + } + }; + + // Closes the editor view + const handleViewClose = () => { + ImageBox.setImageEditorOpen(false); + ImageBox.setImageEditorSource(''); + if (newCollectionRef.current) { + newCollectionRef.current.fitContentOnce = true; + } + setEdits([]); + }; + + return ( + <div className="generativeFillContainer" style={{ display: imageEditorOpen ? 'flex' : 'none' }}> + <div className="generativeFillControls"> + <h1>Image Editor</h1> + <div style={{ display: 'flex', alignItems: 'center', gap: '1.5rem' }}> + <FormControlLabel + control={ + <Checkbox + // disable once edited has been clicked (doesn't make sense to change after first edit) + disabled={edited} + checked={isNewCollection} + onChange={e => { + setIsNewCollection(prev => !prev); + }} + /> + } + label={'Create New Collection'} + labelPlacement="end" + sx={{ whiteSpace: 'nowrap' }} + /> + <Buttons getEdit={getEdit} loading={loading} onReset={handleReset} /> + <IconButton color={activeColor} tooltip="close" icon={<CgClose size={'16px'} />} onClick={handleViewClose} /> + </div> + </div> + {/* Main canvas for editing */} + <div + className="drawingArea" // this only works if pointerevents: none is set on the custom pointer + ref={drawingAreaRef} + onPointerOver={updateCursorData} + onPointerMove={updateCursorData} + onPointerDown={handlePointerDown} + onPointerUp={handlePointerUp}> + <canvas ref={canvasRef} width={canvasDims.width} height={canvasDims.height} style={{ transform: `scale(${canvasScale})` }} /> + <canvas ref={canvasBackgroundRef} width={canvasDims.width} height={canvasDims.height} style={{ transform: `scale(${canvasScale})` }} /> + <div + className="pointer" + style={{ + left: cursorData.x, + top: cursorData.y, + width: cursorData.width, + height: cursorData.width, + }}> + <div className="innerPointer"></div> + </div> + {/* Icons */} + <div className="iconContainer"> + {/* Undo and Redo */} + <IconButton + style={{ cursor: 'pointer' }} + onPointerDown={e => { + e.stopPropagation(); + handleUndo(); + }} + onPointerUp={e => { + e.stopPropagation(); + }} + color={activeColor} + tooltip="Undo" + icon={<IoMdUndo />} + /> + <IconButton + style={{ cursor: 'pointer' }} + onPointerDown={e => { + e.stopPropagation(); + handleRedo(); + }} + onPointerUp={e => { + e.stopPropagation(); + }} + color={activeColor} + tooltip="Redo" + icon={<IoMdRedo />} + /> + <div onPointerDown={e => e.stopPropagation()} style={{ height: 225, width: '100%', display: 'flex', justifyContent: 'center', cursor: 'pointer' }}> + <Slider + sx={{ + '& input[type="range"]': { + WebkitAppearance: 'slider-vertical', + }, + }} + orientation="vertical" + min={25} + max={500} + defaultValue={150} + size="small" + valueLabelDisplay="auto" + onChange={(e, val) => { + setCursorData(prev => ({ ...prev, width: val as number })); + }} + /> + </div> + </div> + {/* Edits thumbnails*/} + <div className="editsBox"> + {edits.map((edit, i) => ( + <img + key={i} + width={75} + src={edit[0] as string} + style={{ cursor: 'pointer' }} + onClick={async () => { + const img = new Image(); + img.src = edit[0] as string; + ImageUtility.drawImgToCanvas(img, canvasRef, canvasDims.width, canvasDims.height); + currImg.current = img; + parentDoc.current = edit[1] as Doc; + }} + /> + ))} + {/* Original img thumbnail */} + {edits.length > 0 && ( + <div style={{ position: 'relative' }}> + <label + style={{ + position: 'absolute', + bottom: 10, + left: 10, + color: '#ffffff', + fontSize: '0.8rem', + letterSpacing: '1px', + textTransform: 'uppercase', + }}> + Original + </label> + <img + width={75} + src={originalImg.current?.src} + style={{ cursor: 'pointer' }} + onClick={() => { + if (!originalImg.current) return; + const img = new Image(); + img.src = originalImg.current.src; + ImageUtility.drawImgToCanvas(img, canvasRef, canvasDims.width, canvasDims.height); + currImg.current = img; + parentDoc.current = originalDoc.current; + }} + /> + </div> + )} + </div> + </div> + <div> + <TextField + value={input} + onChange={e => setInput(e.target.value)} + disabled={isBrushing} + type="text" + label="Prompt" + placeholder="Prompt..." + InputLabelProps={{ style: { fontSize: '16px' } }} + inputProps={{ style: { fontSize: '16px' } }} + sx={{ + backgroundColor: '#ffffff', + position: 'absolute', + bottom: '16px', + transform: 'translateX(calc(50vw - 50%))', + width: 'calc(100vw - 64px)', + }} + /> + </div> + </div> + ); +}; + +export default GenerativeFill; diff --git a/src/client/views/nodes/generativeFill/GenerativeFillButtons.scss b/src/client/views/nodes/generativeFill/GenerativeFillButtons.scss new file mode 100644 index 000000000..0180ef904 --- /dev/null +++ b/src/client/views/nodes/generativeFill/GenerativeFillButtons.scss @@ -0,0 +1,4 @@ +.generativeFillBtnContainer { + display: flex; + gap: 1rem; +} diff --git a/src/client/views/nodes/generativeFill/GenerativeFillButtons.tsx b/src/client/views/nodes/generativeFill/GenerativeFillButtons.tsx new file mode 100644 index 000000000..10eca358e --- /dev/null +++ b/src/client/views/nodes/generativeFill/GenerativeFillButtons.tsx @@ -0,0 +1,44 @@ +import './GenerativeFillButtons.scss'; +import React = require('react'); +import ReactLoading from 'react-loading'; +import { activeColor } from './generativeFillUtils/generativeFillConstants'; +import { Button, IconButton, Type } from 'browndash-components'; +import { AiOutlineInfo } from 'react-icons/ai'; + +interface ButtonContainerProps { + getEdit: () => Promise<void>; + loading: boolean; + onReset: () => void; +} + +const Buttons = ({ loading, getEdit, onReset }: ButtonContainerProps) => { + return ( + <div className="generativeFillBtnContainer"> + <Button text="RESET" type={Type.PRIM} color={activeColor} onClick={onReset} /> + {loading ? ( + <Button + text="GET EDITS" + type={Type.TERT} + color={activeColor} + icon={<ReactLoading type="spin" color={'#ffffff'} width={20} height={20} />} + iconPlacement="right" + onClick={() => { + if (!loading) getEdit(); + }} + /> + ) : ( + <Button + text="GET EDITS" + type={Type.TERT} + color={activeColor} + onClick={() => { + if (!loading) getEdit(); + }} + /> + )} + <IconButton type={Type.SEC} color={activeColor} tooltip="Open Documentation" icon={<AiOutlineInfo size={'16px'} />} onClick={() => window.open('https://brown-dash.github.io/Dash-Documentation/features/generativeai/#editing', '_blank')} /> + </div> + ); +}; + +export default Buttons; diff --git a/src/client/views/nodes/generativeFill/generativeFillUtils/BrushHandler.ts b/src/client/views/nodes/generativeFill/generativeFillUtils/BrushHandler.ts new file mode 100644 index 000000000..f4ec70fbc --- /dev/null +++ b/src/client/views/nodes/generativeFill/generativeFillUtils/BrushHandler.ts @@ -0,0 +1,25 @@ +import { GenerativeFillMathHelpers } from './GenerativeFillMathHelpers'; +import { eraserColor } from './generativeFillConstants'; +import { Point } from './generativeFillInterfaces'; + +export class BrushHandler { + static brushCircleOverlay = (x: number, y: number, brushRadius: number, ctx: CanvasRenderingContext2D, fillColor: string, erase: boolean) => { + ctx.globalCompositeOperation = 'destination-out'; + ctx.fillStyle = fillColor; + ctx.shadowColor = eraserColor; + ctx.shadowBlur = 5; + ctx.beginPath(); + ctx.arc(x, y, brushRadius, 0, 2 * Math.PI); + ctx.fill(); + ctx.closePath(); + }; + + static createBrushPathOverlay = (startPoint: Point, endPoint: Point, brushRadius: number, ctx: CanvasRenderingContext2D, fillColor: string, erase: boolean) => { + const dist = GenerativeFillMathHelpers.distanceBetween(startPoint, endPoint); + + for (let i = 0; i < dist; i += 5) { + const s = i / dist; + BrushHandler.brushCircleOverlay(startPoint.x * (1 - s) + endPoint.x * s, startPoint.y * (1 - s) + endPoint.y * s, brushRadius, ctx, fillColor, erase); + } + }; +} diff --git a/src/client/views/nodes/generativeFill/generativeFillUtils/GenerativeFillMathHelpers.ts b/src/client/views/nodes/generativeFill/generativeFillUtils/GenerativeFillMathHelpers.ts new file mode 100644 index 000000000..97e03ff20 --- /dev/null +++ b/src/client/views/nodes/generativeFill/generativeFillUtils/GenerativeFillMathHelpers.ts @@ -0,0 +1,10 @@ +import { Point } from './generativeFillInterfaces'; + +export class GenerativeFillMathHelpers { + static distanceBetween = (p1: Point, p2: Point) => { + return Math.sqrt(Math.pow(p2.x - p1.x, 2) + Math.pow(p2.y - p1.y, 2)); + }; + static angleBetween = (p1: Point, p2: Point) => { + return Math.atan2(p2.x - p1.x, p2.y - p1.y); + }; +} diff --git a/src/client/views/nodes/generativeFill/generativeFillUtils/ImageHandler.ts b/src/client/views/nodes/generativeFill/generativeFillUtils/ImageHandler.ts new file mode 100644 index 000000000..47a14135f --- /dev/null +++ b/src/client/views/nodes/generativeFill/generativeFillUtils/ImageHandler.ts @@ -0,0 +1,314 @@ +import { RefObject } from 'react'; +import { bgColor, canvasSize } from './generativeFillConstants'; + +export interface APISuccess { + status: 'success'; + urls: string[]; +} + +export interface APIError { + status: 'error'; + message: string; +} + +export class ImageUtility { + /** + * + * @param canvas Canvas to convert + * @returns Blob of canvas + */ + static canvasToBlob = (canvas: HTMLCanvasElement): Promise<Blob> => { + return new Promise(resolve => { + canvas.toBlob(blob => { + if (blob) { + resolve(blob); + } + }, 'image/png'); + }); + }; + + // given a square api image, get the cropped img + static getCroppedImg = (img: HTMLImageElement, width: number, height: number): HTMLCanvasElement | undefined => { + // Create a new canvas element + const canvas = document.createElement('canvas'); + canvas.width = width; + canvas.height = height; + const ctx = canvas.getContext('2d'); + if (ctx) { + // Clear the canvas + ctx.clearRect(0, 0, canvas.width, canvas.height); + if (width < height) { + // horizontal padding, x offset + const xOffset = (canvasSize - width) / 2; + ctx.drawImage(img, xOffset, 0, canvas.width, canvas.height, 0, 0, canvas.width, canvas.height); + } else { + // vertical padding, y offset + const yOffset = (canvasSize - height) / 2; + ctx.drawImage(img, 0, yOffset, canvas.width, canvas.height, 0, 0, canvas.width, canvas.height); + } + return canvas; + } + }; + + // converts an image to a canvas data url + static convertImgToCanvasUrl = async (imageSrc: string, width: number, height: number): Promise<string> => { + return new Promise<string>((resolve, reject) => { + const img = new Image(); + img.onload = () => { + const canvas = this.getCroppedImg(img, width, height); + if (canvas) { + const dataUrl = canvas.toDataURL(); + resolve(dataUrl); + } + }; + img.onerror = error => { + reject(error); + }; + img.src = imageSrc; + }); + }; + + // calls the openai api to get image edits + static getEdit = async (imgBlob: Blob, maskBlob: Blob, prompt: string, n?: number): Promise<APISuccess | APIError> => { + const apiUrl = 'https://api.openai.com/v1/images/edits'; + const fd = new FormData(); + fd.append('image', imgBlob, 'image.png'); + fd.append('mask', maskBlob, 'mask.png'); + fd.append('prompt', prompt); + fd.append('size', '1024x1024'); + fd.append('n', n ? JSON.stringify(n) : '1'); + fd.append('response_format', 'b64_json'); + + try { + const res = await fetch(apiUrl, { + method: 'POST', + headers: { + Authorization: `Bearer ${process.env.OPENAI_KEY}`, + }, + body: fd, + }); + const data = await res.json(); + console.log(data.data); + return { + status: 'success', + urls: (data.data as { b64_json: string }[]).map(data => `data:image/png;base64,${data.b64_json}`), + }; + } catch (err) { + console.log(err); + return { status: 'error', message: 'API error.' }; + } + }; + + // mock api call + static mockGetEdit = async (mockSrc: string): Promise<APISuccess | APIError> => { + return { + status: 'success', + urls: [mockSrc, mockSrc, mockSrc], + }; + }; + + // Gets the canvas rendering context of a canvas + static getCanvasContext = (canvasRef: RefObject<HTMLCanvasElement>): CanvasRenderingContext2D | null => { + if (!canvasRef.current) return null; + const ctx = canvasRef.current.getContext('2d'); + if (!ctx) return null; + return ctx; + }; + + // Helper for downloading the canvas (for debugging) + static downloadCanvas = (canvas: HTMLCanvasElement) => { + const url = canvas.toDataURL(); + const downloadLink = document.createElement('a'); + downloadLink.href = url; + downloadLink.download = 'canvas'; + + downloadLink.click(); + downloadLink.remove(); + }; + + // Download the canvas (for debugging) + static downloadImageCanvas = (imgUrl: string) => { + const img = new Image(); + img.src = imgUrl; + img.onload = () => { + const canvas = document.createElement('canvas'); + canvas.width = canvasSize; + canvas.height = canvasSize; + const ctx = canvas.getContext('2d'); + ctx?.drawImage(img, 0, 0, canvasSize, canvasSize); + + this.downloadCanvas(canvas); + }; + }; + + // Clears the canvas + static clearCanvas = (canvasRef: React.RefObject<HTMLCanvasElement>) => { + const ctx = this.getCanvasContext(canvasRef); + if (!ctx || !canvasRef.current) return; + ctx.clearRect(0, 0, canvasRef.current.width, canvasRef.current.height); + }; + + // Draws the image to the current canvas + static drawImgToCanvas = (img: HTMLImageElement, canvasRef: React.RefObject<HTMLCanvasElement>, width: number, height: number) => { + const drawImg = (img: HTMLImageElement) => { + const ctx = this.getCanvasContext(canvasRef); + if (!ctx) return; + ctx.globalCompositeOperation = 'source-over'; + ctx.clearRect(0, 0, width, height); + ctx.drawImage(img, 0, 0, width, height); + }; + + if (img.complete) { + drawImg(img); + } else { + img.onload = () => { + drawImg(img); + }; + } + }; + + // Gets the image mask for the openai endpoint + static getCanvasMask = (srcCanvas: HTMLCanvasElement, paddedCanvas: HTMLCanvasElement): HTMLCanvasElement | undefined => { + const canvas = document.createElement('canvas'); + canvas.width = canvasSize; + canvas.height = canvasSize; + const ctx = canvas.getContext('2d'); + if (!ctx) return; + ctx?.clearRect(0, 0, canvasSize, canvasSize); + ctx.drawImage(paddedCanvas, 0, 0); + + // extract and set padding data + if (srcCanvas.height > srcCanvas.width) { + // horizontal padding, x offset + const xOffset = (canvasSize - srcCanvas.width) / 2; + ctx?.clearRect(xOffset, 0, srcCanvas.width, srcCanvas.height); + ctx.drawImage(srcCanvas, xOffset, 0, srcCanvas.width, srcCanvas.height); + } else { + // vertical padding, y offset + const yOffset = (canvasSize - srcCanvas.height) / 2; + ctx?.clearRect(0, yOffset, srcCanvas.width, srcCanvas.height); + ctx.drawImage(srcCanvas, 0, yOffset, srcCanvas.width, srcCanvas.height); + } + return canvas; + }; + + // Fills in the blank areas of the image with an image reflection (to fill in a square-shaped canvas) + static drawHorizontalReflection = (ctx: CanvasRenderingContext2D, canvas: HTMLCanvasElement, xOffset: number) => { + const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height); + const data = imageData.data; + for (let i = 0; i < canvas.height; i++) { + for (let j = 0; j < xOffset; j++) { + const targetIdx = 4 * (i * canvas.width + j); + const sourceI = i; + const sourceJ = xOffset + (xOffset - j); + const sourceIdx = 4 * (sourceI * canvas.width + sourceJ); + data[targetIdx] = data[sourceIdx]; + data[targetIdx + 1] = data[sourceIdx + 1]; + data[targetIdx + 2] = data[sourceIdx + 2]; + } + } + for (let i = 0; i < canvas.height; i++) { + for (let j = canvas.width - 1; j >= canvas.width - 1 - xOffset; j--) { + const targetIdx = 4 * (i * canvas.width + j); + const sourceI = i; + const sourceJ = canvas.width - 1 - xOffset - (xOffset - (canvas.width - j)); + const sourceIdx = 4 * (sourceI * canvas.width + sourceJ); + data[targetIdx] = data[sourceIdx]; + data[targetIdx + 1] = data[sourceIdx + 1]; + data[targetIdx + 2] = data[sourceIdx + 2]; + } + } + ctx.putImageData(imageData, 0, 0); + }; + + // Fills in the blank areas of the image with an image reflection (to fill in a square-shaped canvas) + static drawVerticalReflection = (ctx: CanvasRenderingContext2D, canvas: HTMLCanvasElement, yOffset: number) => { + const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height); + const data = imageData.data; + for (let j = 0; j < canvas.width; j++) { + for (let i = 0; i < yOffset; i++) { + const targetIdx = 4 * (i * canvas.width + j); + const sourceJ = j; + const sourceI = yOffset + (yOffset - i); + const sourceIdx = 4 * (sourceI * canvas.width + sourceJ); + data[targetIdx] = data[sourceIdx]; + data[targetIdx + 1] = data[sourceIdx + 1]; + data[targetIdx + 2] = data[sourceIdx + 2]; + } + } + for (let j = 0; j < canvas.width; j++) { + for (let i = canvas.height - 1; i >= canvas.height - 1 - yOffset; i--) { + const targetIdx = 4 * (i * canvas.width + j); + const sourceJ = j; + const sourceI = canvas.height - 1 - yOffset - (yOffset - (canvas.height - i)); + const sourceIdx = 4 * (sourceI * canvas.width + sourceJ); + data[targetIdx] = data[sourceIdx]; + data[targetIdx + 1] = data[sourceIdx + 1]; + data[targetIdx + 2] = data[sourceIdx + 2]; + } + } + ctx.putImageData(imageData, 0, 0); + }; + + // Gets the unaltered (besides filling in padding) version of the image for the api call + static getCanvasImg = (img: HTMLImageElement): HTMLCanvasElement | undefined => { + const canvas = document.createElement('canvas'); + canvas.width = canvasSize; + canvas.height = canvasSize; + const ctx = canvas.getContext('2d'); + if (!ctx) return; + // fix scaling + const scale = Math.min(canvasSize / img.width, canvasSize / img.height); + const width = Math.floor(img.width * scale); + const height = Math.floor(img.height * scale); + ctx?.clearRect(0, 0, canvasSize, canvasSize); + ctx.fillStyle = bgColor; + ctx.fillRect(0, 0, canvasSize, canvasSize); + + // extract and set padding data + if (img.naturalHeight > img.naturalWidth) { + // horizontal padding, x offset + const xOffset = Math.floor((canvasSize - width) / 2); + ctx.drawImage(img, xOffset, 0, width, height); + + // draw reflected image padding + this.drawHorizontalReflection(ctx, canvas, xOffset); + } else { + // vertical padding, y offset + const yOffset = Math.floor((canvasSize - height) / 2); + ctx.drawImage(img, 0, yOffset, width, height); + + // draw reflected image padding + this.drawVerticalReflection(ctx, canvas, yOffset); + } + return canvas; + }; + + /** + * Converts a url to base64 (tainted canvas workaround) + */ + static urlToBase64 = async (imageUrl: string): Promise<string | undefined> => { + try { + const res = await fetch(imageUrl); + const blob = await res.blob(); + + return new Promise<string>((resolve, reject) => { + const reader = new FileReader(); + reader.onload = () => { + const base64Data = reader.result?.toString().split(',')[1]; + if (base64Data) { + resolve(base64Data); + } else { + reject(new Error('Failed to convert.')); + } + }; + reader.onerror = () => { + reject(new Error('Error reading image data')); + }; + reader.readAsDataURL(blob); + }); + } catch (err) { + console.error(err); + } + }; +} diff --git a/src/client/views/nodes/generativeFill/generativeFillUtils/PointerHandler.ts b/src/client/views/nodes/generativeFill/generativeFillUtils/PointerHandler.ts new file mode 100644 index 000000000..9e620ad11 --- /dev/null +++ b/src/client/views/nodes/generativeFill/generativeFillUtils/PointerHandler.ts @@ -0,0 +1,15 @@ +import { Point } from "./generativeFillInterfaces"; + +export class PointerHandler { + static getPointRelativeToElement = ( + element: HTMLElement, + e: React.PointerEvent | PointerEvent, + scale: number + ): Point => { + const boundingBox = element.getBoundingClientRect(); + return { + x: (e.clientX - boundingBox.x) / scale, + y: (e.clientY - boundingBox.y) / scale, + }; + }; +} diff --git a/src/client/views/nodes/generativeFill/generativeFillUtils/generativeFillConstants.ts b/src/client/views/nodes/generativeFill/generativeFillUtils/generativeFillConstants.ts new file mode 100644 index 000000000..4772304bc --- /dev/null +++ b/src/client/views/nodes/generativeFill/generativeFillUtils/generativeFillConstants.ts @@ -0,0 +1,9 @@ +export const canvasSize = 1024; +export const freeformRenderSize = 300; +export const offsetDistanceY = freeformRenderSize + 400; +export const offsetX = 200; +export const newCollectionSize = 500; + +export const activeColor = '#1976d2'; +export const eraserColor = '#e1e9ec'; +export const bgColor = '#f0f4f6'; diff --git a/src/client/views/nodes/generativeFill/generativeFillUtils/generativeFillInterfaces.ts b/src/client/views/nodes/generativeFill/generativeFillUtils/generativeFillInterfaces.ts new file mode 100644 index 000000000..1e7801056 --- /dev/null +++ b/src/client/views/nodes/generativeFill/generativeFillUtils/generativeFillInterfaces.ts @@ -0,0 +1,20 @@ +export interface CursorData { + x: number; + y: number; + width: number; +} + +export interface Point { + x: number; + y: number; +} + +export enum BrushMode { + ADD, + SUBTRACT, +} + +export interface ImageDimensions { + width: number; + height: number; +} diff --git a/src/client/views/nodes/trails/PresBox.scss b/src/client/views/nodes/trails/PresBox.scss index bf56b4d9e..0b51813a5 100644 --- a/src/client/views/nodes/trails/PresBox.scss +++ b/src/client/views/nodes/trails/PresBox.scss @@ -121,7 +121,7 @@ .dropdown.active { transform: rotate(180deg); color: $light-blue; - opacity: 0.8; + opacity: 0.7; } .presBox-radioButtons { @@ -187,9 +187,6 @@ font-size: 11; font-weight: 200; height: 20; - background-color: $white; - color: $black; - border: solid 1px $black; display: flex; margin-left: 5px; margin-top: 5px; @@ -210,13 +207,11 @@ .ribbon-propertyUpDownItem { cursor: pointer; - color: white; display: flex; justify-content: center; align-items: center; height: 100%; width: 100%; - background: $black; } .ribbon-propertyUpDownItem:hover { @@ -609,7 +604,6 @@ font-weight: 200; height: 20; background-color: $white; - border: solid 1px rgba(0, 0, 0, 0.5); display: flex; color: $black; margin-top: 5px; @@ -691,16 +685,19 @@ padding-right: 5px; padding-top: 3; padding-bottom: 3; + opacity: 0.8; } .presBox-dropdownOption:hover { position: relative; - background-color: lightgrey; + opacity: 1; + font-weight: bold; } .presBox-dropdownOption.active { position: relative; - background-color: $light-blue; + opacity: 1; + font-weight: bold; } .presBox-dropdownOptions { diff --git a/src/client/views/nodes/trails/PresBox.tsx b/src/client/views/nodes/trails/PresBox.tsx index 56af67802..bcec2d2bd 100644 --- a/src/client/views/nodes/trails/PresBox.tsx +++ b/src/client/views/nodes/trails/PresBox.tsx @@ -3,7 +3,7 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { Tooltip } from '@material-ui/core'; import { action, computed, IReactionDisposer, observable, ObservableSet, reaction, runInAction } from 'mobx'; import { observer } from 'mobx-react'; -import { Doc, DocListCast, Field, FieldResult, Opt, StrListCast } from '../../../../fields/Doc'; +import { Doc, DocListCast, FieldResult, Opt, StrListCast } from '../../../../fields/Doc'; import { Animation } from '../../../../fields/DocSymbols'; import { Copy, Id } from '../../../../fields/FieldSymbols'; import { InkField } from '../../../../fields/InkField'; @@ -13,7 +13,7 @@ import { listSpec } from '../../../../fields/Schema'; import { ComputedField, ScriptField } from '../../../../fields/ScriptField'; import { BoolCast, Cast, DocCast, NumCast, StrCast } from '../../../../fields/Types'; import { AudioField } from '../../../../fields/URLField'; -import { emptyFunction, emptyPath, returnFalse, returnOne, setupMoveUpEvents, StopEvent } from '../../../../Utils'; +import { emptyFunction, emptyPath, lightOrDark, returnFalse, returnOne, setupMoveUpEvents, StopEvent } from '../../../../Utils'; import { DocServer } from '../../../DocServer'; import { Docs } from '../../../documents/Documents'; import { CollectionViewType, DocumentType } from '../../../documents/DocumentTypes'; @@ -36,6 +36,9 @@ import { FieldView, FieldViewProps } from '../FieldView'; import { ScriptingBox } from '../ScriptingBox'; import './PresBox.scss'; import { PresEffect, PresEffectDirection, PresMovement, PresStatus } from './PresEnums'; +import { BranchingTrailManager } from '../../../util/BranchingTrailManager'; +import { TreeView } from '../../collections/TreeView'; +import { OverlayView } from '../../OverlayView'; const { Howl } = require('howler'); export interface pinDataTypes { @@ -52,6 +55,7 @@ export interface pinDataTypes { dataview?: boolean; poslayoutview?: boolean; dataannos?: boolean; + map?: boolean; } export interface PinProps { audioRange?: boolean; @@ -74,7 +78,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { constructor(props: any) { super(props); if (!PresBox.navigateToDocScript) { - PresBox.navigateToDocScript = ScriptField.MakeFunction('navigateToDoc(self.presentationTargetDoc, self)')!; + PresBox.navigateToDocScript = ScriptField.MakeFunction('navigateToDoc(self.presentation_targetDoc, self)')!; } } @@ -116,7 +120,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { return DocListCast(this.rootDoc[this.presFieldKey]); } @computed get tagDocs() { - return this.childDocs.map(doc => Cast(doc.presentationTargetDoc, Doc, null)); + return this.childDocs.map(doc => Cast(doc.presentation_targetDoc, Doc, null)); } @computed get itemIndex() { return NumCast(this.rootDoc._itemIndex); @@ -125,10 +129,10 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { return DocCast(this.childDocs[NumCast(this.rootDoc._itemIndex)]); } @computed get targetDoc() { - return Cast(this.activeItem?.presentationTargetDoc, Doc, null); + return Cast(this.activeItem?.presentation_targetDoc, Doc, null); } public static targetRenderedDoc = (doc: Doc) => { - const targetDoc = Cast(doc?.presentationTargetDoc, Doc, null); + const targetDoc = Cast(doc?.presentation_targetDoc, Doc, null); return targetDoc?.layout_unrendered ? DocCast(targetDoc.annotationOn) : targetDoc; }; @computed get scrollable() { @@ -149,7 +153,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { @computed get selectedDoc() { return this.selectedDocumentView?.rootDoc; } - isActiveItemTarget = (layoutDoc: Doc) => this.activeItem?.presentationTargetDoc === layoutDoc; + 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)); @@ -196,18 +200,18 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { } this.turnOffEdit(true); this._disposers.selection = reaction( - () => SelectionManager.Views(), + () => SelectionManager.Views().slice(), views => (!PresBox.Instance || views.some(view => view.props.Document === this.rootDoc)) && this.updateCurrentPresentation(), { fireImmediately: true } ); this._disposers.editing = reaction( - () => this.layoutDoc.presStatus === PresStatus.Edit, + () => this.layoutDoc.presentation_status === PresStatus.Edit, editing => { if (editing) { this.childDocs.forEach(doc => { - if (doc.presIndexed !== undefined) { + if (doc.presentation_indexed !== undefined) { this.progressivizedItems(doc)?.forEach(indexedDoc => (indexedDoc.opacity = undefined)); - doc.presIndexed = Math.min(this.progressivizedItems(doc)?.length ?? 0, 1); + doc.presentation_indexed = Math.min(this.progressivizedItems(doc)?.length ?? 0, 1); } }); } @@ -224,10 +228,10 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { _mediaTimer!: [NodeJS.Timeout, Doc]; // 'Play on next' for audio or video therefore first navigate to the audio/video before it should be played startTempMedia = (targetDoc: Doc, activeItem: Doc) => { - const duration: number = NumCast(activeItem.presEndTime) - NumCast(activeItem.presStartTime); + const duration: number = NumCast(activeItem.config_clipEnd) - NumCast(activeItem.config_clipStart); if ([DocumentType.VID, DocumentType.AUDIO].includes(targetDoc.type as any)) { const targMedia = DocumentManager.Instance.getDocumentView(targetDoc); - targMedia?.ComponentView?.playFrom?.(NumCast(activeItem.presStartTime), NumCast(activeItem.presStartTime) + duration); + targMedia?.ComponentView?.playFrom?.(NumCast(activeItem.config_clipStart), NumCast(activeItem.config_clipStart) + duration); } }; @@ -251,12 +255,12 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { (nextSelected: number, force = false) => () => { if (nextSelected < this.childDocs.length) { - if (force || this.childDocs[nextSelected].groupWithUp) { + if (force || this.childDocs[nextSelected].presentation_groupWithUp) { this.addToSelectedArray(this.childDocs[nextSelected]); - const serial = nextSelected + 1 < this.childDocs.length && NumCast(this.childDocs[nextSelected + 1].groupWithUp) > 1; + const serial = nextSelected + 1 < this.childDocs.length && NumCast(this.childDocs[nextSelected + 1].presentation_groupWithUp) > 1; if (serial) { this.gotoDocument(nextSelected, this.activeItem, true, async () => { - const waitTime = NumCast(this.activeItem.presDuration) - NumCast(this.activeItem.presTransition); + const waitTime = NumCast(this.activeItem.presentation_duration); await new Promise<void>(res => setTimeout(() => res(), Math.max(0, waitTime))); doGroupWithUp(nextSelected + 1)(); }); @@ -274,16 +278,33 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { // docs within a slide target that will be progressively revealed progressivizedItems = (doc: Doc) => { const targetList = PresBox.targetRenderedDoc(doc); - if (doc.presIndexed !== undefined && targetList) { + if (doc.presentation_indexed !== undefined && targetList) { const listItems = (Cast(targetList[Doc.LayoutFieldKey(targetList)], listSpec(Doc), null)?.filter(d => d instanceof Doc) as Doc[]) ?? DocListCast(targetList[Doc.LayoutFieldKey(targetList) + '_annotations']); return listItems.filter(doc => !doc.layout_unrendered); } }; + + // 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; + } + + childrenToRun.forEach(child => { + DocumentManager.Instance.showDocument(child, {}); + }); + }; + // Called when the user activates 'next' - to move to the next part of the pres. trail @action next = () => { const progressiveReveal = (first: boolean) => { - const presIndexed = Cast(this.activeItem?.presIndexed, 'number', null); + const presIndexed = Cast(this.activeItem?.presentation_indexed, 'number', null); if (presIndexed !== undefined) { const targetRenderedDoc = PresBox.targetRenderedDoc(this.activeItem); targetRenderedDoc._dataTransition = 'all 1s'; @@ -296,8 +317,8 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { const targetView = listItems && DocumentManager.Instance.getFirstDocumentView(listItemDoc); Doc.linkFollowUnhighlight(); Doc.HighlightDoc(listItemDoc); - listItemDoc.presEffect = this.activeItem.presBulletEffect; - listItemDoc.presTransition = 500; + 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; @@ -308,7 +329,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { }); } listItemDoc.opacity = undefined; - this.activeItem.presIndexed = presIndexed + 1; + this.activeItem.presentation_indexed = presIndexed + 1; } return true; } @@ -319,10 +340,16 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { // 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 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 :) + const currentDoc = this.childDocs[this.itemIndex]; + //could i do this.childDocs[this.itemIndex] for first arg? + this.runSubroutines(TreeView.GetRunningChildren.get(currentDoc)?.(), this.childDocs[this.itemIndex + 1]); + this.nextSlide(curLast + 1 === this.childDocs.length ? (this.layoutDoc.presLoop ? 0 : curLast) : curLast + 1); progressiveReveal(true); // shows first progressive document, but without a transition effect } else { - if (this.childDocs[this.itemIndex + 1] === undefined && (this.layoutDoc.presLoop || this.layoutDoc.presStatus === PresStatus.Edit)) { + if (this.childDocs[this.itemIndex + 1] === undefined && (this.layoutDoc.presLoop || this.layoutDoc.presentation_status === PresStatus.Edit)) { // Case 2: Last slide and presLoop is toggled ON or it is in Edit mode this.nextSlide(0); progressiveReveal(true); // shows first progressive document, but without a transition effect @@ -338,9 +365,9 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { const activeItem: Doc = this.activeItem; let prevSelected = this.itemIndex; // Functionality for group with up - let didZoom = activeItem.presMovement; - for (; prevSelected > 0 && this.childDocs[Math.max(0, prevSelected - 1)].groupWithUp; prevSelected--) { - didZoom = didZoom === 'none' ? this.childDocs[prevSelected].presMovement : didZoom; + let didZoom = activeItem.presentation_movement; + for (; prevSelected > 0 && this.childDocs[Math.max(0, prevSelected - 1)].presentation_groupWithUp; prevSelected--) { + didZoom = didZoom === 'none' ? this.childDocs[prevSelected].presentation_movement : didZoom; } if (activeItem && this.childDocs[this.itemIndex - 1] !== undefined) { // Case 2: There are no other frames so it should go to the previous slide @@ -361,20 +388,20 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { Doc.UnBrushAllDocs(); if (index >= 0 && index < this.childDocs.length) { this.rootDoc._itemIndex = index; - if (from?.mediaStopTriggerList && this.layoutDoc.presStatus !== PresStatus.Edit) { + if (from?.mediaStopTriggerList && this.layoutDoc.presentation_status !== PresStatus.Edit) { DocListCast(from.mediaStopTriggerList).forEach(this.stopTempMedia); } - if (from?.mediaStop === 'auto' && this.layoutDoc.presStatus !== PresStatus.Edit) { - this.stopTempMedia(from.presentationTargetDoc); + if (from?.presentation_mediaStop === 'auto' && this.layoutDoc.presentation_status !== PresStatus.Edit) { + this.stopTempMedia(from.presentation_targetDoc); } // If next slide is audio / video 'Play automatically' then the next slide should be played - if (this.layoutDoc.presStatus !== PresStatus.Edit && (this.targetDoc.type === DocumentType.AUDIO || this.targetDoc.type === DocumentType.VID) && this.activeItem.mediaStart === 'auto') { + if (this.layoutDoc.presentation_status !== PresStatus.Edit && (this.targetDoc.type === DocumentType.AUDIO || this.targetDoc.type === DocumentType.VID) && this.activeItem.presentation_mediaStart === 'auto') { this.startTempMedia(this.targetDoc, this.activeItem); } if (!group) this.clearSelectedArray(); this.childDocs[index] && this.addToSelectedArray(this.childDocs[index]); //Update selected array this.turnOffEdit(); - this.navigateToActiveItem(finished); //Handles movement to element only when presTrail is list + this.navigateToActiveItem(finished); //Handles movement to element only when presentationTrail is list this.doHideBeforeAfter(); //Handles hide after/before } }); @@ -383,6 +410,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { const inkable = [DocumentType.INK].includes(targetType); const scrollable = [DocumentType.PDF, DocumentType.RTF, DocumentType.WEB].includes(targetType) || target?._type_collection === CollectionViewType.Stacking; const pannable = [DocumentType.IMG, DocumentType.PDF].includes(targetType) || (targetType === DocumentType.COL && target?._type_collection === CollectionViewType.Freeform); + const map = [DocumentType.MAP].includes(targetType); const temporal = [DocumentType.AUDIO, DocumentType.VID].includes(targetType); const clippable = [DocumentType.COMPARISON].includes(targetType); const datarange = [DocumentType.FUNCPLOT].includes(targetType); @@ -392,39 +420,39 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { const filters = true; const pivot = true; const dataannos = false; - return { scrollable, pannable, inkable, type_collection, pivot, filters, temporal, clippable, dataview, datarange, poslayoutview, dataannos }; + return { scrollable, pannable, inkable, type_collection, pivot, map, filters, temporal, clippable, dataview, datarange, poslayoutview, dataannos }; } @action playAnnotation = (anno: AudioField) => {}; @action - static restoreTargetDocView(bestTargetView: Opt<DocumentView>, activeItem: Doc, transTime: number, pinDocLayout: boolean = BoolCast(activeItem.presPinLayout), pinDataTypes?: pinDataTypes, targetDoc?: Doc) { + 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); - if (!bestTarget || activeItem === bestTarget) return; + if (!bestTarget) return; let changed = false; if (pinDocLayout) { if ( - bestTarget.x !== NumCast(activeItem.presX, NumCast(bestTarget.x)) || - bestTarget.y !== NumCast(activeItem.presY, NumCast(bestTarget.y)) || - bestTarget.rotation !== NumCast(activeItem.presRotation, NumCast(bestTarget.rotation)) || - bestTarget.width !== NumCast(activeItem.presWidth, NumCast(bestTarget.width)) || - bestTarget.height !== NumCast(activeItem.presHeight, NumCast(bestTarget.height)) + bestTarget.x !== NumCast(activeItem.config_x, NumCast(bestTarget.x)) || + bestTarget.y !== NumCast(activeItem.config_y, NumCast(bestTarget.y)) || + bestTarget.rotation !== NumCast(activeItem.config_rotation, NumCast(bestTarget.rotation)) || + bestTarget.width !== NumCast(activeItem.config_width, NumCast(bestTarget.width)) || + bestTarget.height !== NumCast(activeItem.config_height, NumCast(bestTarget.height)) ) { bestTarget._dataTransition = `all ${transTime}ms`; - bestTarget.x = NumCast(activeItem.presX, NumCast(bestTarget.x)); - bestTarget.y = NumCast(activeItem.presY, NumCast(bestTarget.y)); - bestTarget.rotation = NumCast(activeItem.presRotation, NumCast(bestTarget.rotation)); - bestTarget.width = NumCast(activeItem.presWidth, NumCast(bestTarget.width)); - bestTarget.height = NumCast(activeItem.presHeight, NumCast(bestTarget.height)); + bestTarget.x = NumCast(activeItem.config_x, NumCast(bestTarget.x)); + bestTarget.y = NumCast(activeItem.config_y, NumCast(bestTarget.y)); + bestTarget.rotation = NumCast(activeItem.config_rotation, NumCast(bestTarget.rotation)); + bestTarget.width = NumCast(activeItem.config_width, NumCast(bestTarget.width)); + bestTarget.height = NumCast(activeItem.config_height, NumCast(bestTarget.height)); setTimeout(() => (bestTarget._dataTransition = undefined), transTime + 10); changed = true; } } - const activeFrame = activeItem.presActiveFrame ?? activeItem.presCurrentFrame; + const activeFrame = activeItem.config_activeFrame ?? activeItem.config_currentFrame; if (activeFrame !== undefined) { - const transTime = NumCast(activeItem.presTransition, 500); - const acontext = activeItem.presActiveFrame !== undefined ? DocCast(DocCast(activeItem.presentationTargetDoc).embedContainer) : DocCast(activeItem.presentationTargetDoc); + const transTime = NumCast(activeItem.presentation_transition, 500); + const acontext = activeItem.config_activeFrame !== undefined ? DocCast(DocCast(activeItem.presentation_targetDoc).embedContainer) : DocCast(activeItem.presentation_targetDoc); const context = DocCast(acontext)?.annotationOn ? DocCast(DocCast(acontext).annotationOn) : acontext; if (context) { const ffview = DocumentManager.Instance.getFirstDocumentView(context)?.props.CollectionFreeFormDocumentView?.().props.CollectionFreeFormView; @@ -434,73 +462,95 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { } } } - if ((pinDataTypes?.dataview && activeItem.presData !== undefined) || (!pinDataTypes && activeItem.presData !== undefined)) { + if ((pinDataTypes?.dataview && activeItem.config_data !== undefined) || (!pinDataTypes && activeItem.config_data !== undefined)) { bestTarget._dataTransition = `all ${transTime}ms`; const fkey = Doc.LayoutFieldKey(bestTarget); const setData = bestTargetView?.ComponentView?.setData; - if (setData) setData(activeItem.presData); - else Doc.GetProto(bestTarget)[fkey] = activeItem.presData instanceof ObjectField ? activeItem.presData[Copy]() : activeItem.presData; - bestTarget[fkey + '_usePath'] = activeItem.presUsePath; + if (setData) setData(activeItem.config_data); + else Doc.GetProto(bestTarget)[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); } - if (pinDataTypes?.datarange || (!pinDataTypes && activeItem.presXRange !== undefined)) { - if (bestTarget.xRange !== activeItem.presXRange) { - bestTarget.xRange = (activeItem.presXRange as ObjectField)?.[Copy](); + if (pinDataTypes?.datarange || (!pinDataTypes && activeItem.config_xRange !== undefined)) { + if (bestTarget.xRange !== activeItem.config_xRange) { + bestTarget.xRange = (activeItem.config_xRange as ObjectField)?.[Copy](); changed = true; } - if (bestTarget.yRange !== activeItem.presYRange) { - bestTarget.yRange = (activeItem.presYRange as ObjectField)?.[Copy](); + if (bestTarget.yRange !== activeItem.config_yRange) { + bestTarget.yRange = (activeItem.config_yRange as ObjectField)?.[Copy](); changed = true; } } - if (pinDataTypes?.clippable || (!pinDataTypes && activeItem.presClipWidth !== undefined)) { + if (pinDataTypes?.clippable || (!pinDataTypes && activeItem.config_clipWidth !== undefined)) { const fkey = '_' + Doc.LayoutFieldKey(bestTarget); - if (bestTarget[fkey + '_clipWidth'] !== activeItem.presClipWidth) { - bestTarget[fkey + '_clipWidth'] = activeItem.presClipWidth; + if (bestTarget[fkey + '_clipWidth'] !== activeItem.config_clipWidth) { + bestTarget[fkey + '_clipWidth'] = activeItem.config_clipWidth; changed = true; } } - if (pinDataTypes?.temporal || (!pinDataTypes && activeItem.presStartTime !== undefined)) { - if (bestTarget._layout_currentTimecode !== activeItem.presStartTime) { - bestTarget._layout_currentTimecode = activeItem.presStartTime; + if (pinDataTypes?.map || (!pinDataTypes && activeItem.config_latitude !== undefined)) { + if (bestTarget.latitude !== activeItem.config_latitude) { + Doc.SetInPlace(bestTarget, 'latitude', NumCast(activeItem.config_latitude), true); + changed = true; + } + if (bestTarget.longitude !== activeItem.config_longitude) { + Doc.SetInPlace(bestTarget, 'longitude', NumCast(activeItem.config_longitude), true); + changed = true; + } + if (bestTarget.zoom !== activeItem.config_map_zoom) { + Doc.SetInPlace(bestTarget, 'map_zoom', NumCast(activeItem.config_map_zoom), true); + changed = true; + } + if (bestTarget.map_type !== activeItem.config_map_type) { + Doc.SetInPlace(bestTarget, 'map_type', StrCast(activeItem.config_map_type), true); + changed = true; + } + if (bestTarget.map !== activeItem.config_map) { + Doc.SetInPlace(bestTarget, 'map', StrCast(activeItem.config_map), true); + changed = true; + } + } + if (pinDataTypes?.temporal || (!pinDataTypes && activeItem.config_clipStart !== undefined)) { + if (bestTarget._layout_currentTimecode !== activeItem.config_clipStart) { + bestTarget._layout_currentTimecode = activeItem.config_clipStart; changed = true; } } - if (pinDataTypes?.inkable || (!pinDataTypes && (activeItem.presFillColor !== undefined || activeItem.color !== undefined))) { - if (bestTarget.fillColor !== activeItem.presFillColor) { - Doc.GetProto(bestTarget).fillColor = activeItem.presFillColor; + if (pinDataTypes?.inkable || (!pinDataTypes && (activeItem.config_fillColor !== undefined || activeItem.color !== undefined))) { + if (bestTarget.fillColor !== activeItem.config_fillColor) { + Doc.GetProto(bestTarget).fillColor = activeItem.config_fillColor; changed = true; } - if (bestTarget.color !== activeItem.presColor) { - Doc.GetProto(bestTarget).color = activeItem.presColor; + if (bestTarget.color !== activeItem.config_color) { + Doc.GetProto(bestTarget).color = activeItem.config_color; changed = true; } if (bestTarget.width !== activeItem.width) { - bestTarget._width = NumCast(activeItem.presWidth, NumCast(bestTarget.width)); + bestTarget._width = NumCast(activeItem.config_width, NumCast(bestTarget.width)); changed = true; } if (bestTarget.height !== activeItem.height) { - bestTarget._height = NumCast(activeItem.presHeight, NumCast(bestTarget.height)); + bestTarget._height = NumCast(activeItem.config_height, NumCast(bestTarget.height)); changed = true; } } - if ((pinDataTypes?.type_collection && activeItem.presViewType !== undefined) || (!pinDataTypes && activeItem.presViewType !== undefined)) { - if (bestTarget._type_collection !== activeItem.presViewType) { - bestTarget._type_collection = activeItem.presViewType; + if ((pinDataTypes?.type_collection && activeItem.config_viewType !== undefined) || (!pinDataTypes && activeItem.config_viewType !== undefined)) { + if (bestTarget._type_collection !== activeItem.config_viewType) { + bestTarget._type_collection = activeItem.config_viewType; changed = true; } } - if ((pinDataTypes?.filters && activeItem.presDocFilters !== undefined) || (!pinDataTypes && activeItem.presDocFilters !== undefined)) { - if (bestTarget.childFilters !== activeItem.presDocFilters) { - bestTarget.childFilters = ObjectField.MakeCopy(activeItem.presDocFilters as ObjectField) || new List<string>([]); + if ((pinDataTypes?.filters && activeItem.config_docFilters !== undefined) || (!pinDataTypes && activeItem.config_docFilters !== undefined)) { + if (bestTarget.childFilters !== activeItem.config_docFilters) { + bestTarget.childFilters = ObjectField.MakeCopy(activeItem.config_docFilters as ObjectField) || new List<string>([]); changed = true; } } - if ((pinDataTypes?.pivot && activeItem.presPivotField !== undefined) || (!pinDataTypes && activeItem.presPivotField !== undefined)) { - if (bestTarget.pivotField !== activeItem.presPivotField) { - bestTarget.pivotField = activeItem.presPivotField; + if ((pinDataTypes?.pivot && activeItem.config_pivotField !== undefined) || (!pinDataTypes && activeItem.config_pivotField !== undefined)) { + if (bestTarget.pivotField !== activeItem.config_pivotField) { + bestTarget.pivotField = activeItem.config_pivotField; bestTarget._prevFilterIndex = 1; // need to revisit this...see CollectionTimeView changed = true; } @@ -509,21 +559,21 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { changed = true; } - if (pinDataTypes?.scrollable || (!pinDataTypes && activeItem.presViewScroll !== undefined)) { - if (bestTarget._layout_scrollTop !== activeItem.presViewScroll) { - bestTarget._layout_scrollTop = activeItem.presViewScroll; + if (pinDataTypes?.scrollable || (!pinDataTypes && activeItem.config_scrollTop !== undefined)) { + if (bestTarget._layout_scrollTop !== activeItem.config_scrollTop) { + bestTarget._layout_scrollTop = activeItem.config_scrollTop; changed = true; - const contentBounds = Cast(activeItem.presPinViewBounds, listSpec('number')); + 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] }); + 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.presAnnotations !== undefined)) { + if (pinDataTypes?.dataannos || (!pinDataTypes && activeItem.config_annotations !== undefined)) { const fkey = Doc.LayoutFieldKey(bestTarget); const oldItems = DocListCast(bestTarget[fkey + '_annotations']).filter(doc => doc.layout_unrendered); - const newItems = DocListCast(activeItem.presAnnotations).map(doc => { + const newItems = DocListCast(activeItem.config_annotations).map(doc => { doc.hidden = false; return doc; }); @@ -536,11 +586,11 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { const newList = new List<Doc>([...oldItems, ...hiddenItems, ...newItems]); Doc.GetProto(bestTarget)[fkey + '_annotations'] = newList; } - if (pinDataTypes?.poslayoutview || (!pinDataTypes && activeItem.presPinLayoutData !== undefined)) { + if (pinDataTypes?.poslayoutview || (!pinDataTypes && activeItem.config_pinLayoutData !== undefined)) { changed = true; const layoutField = Doc.LayoutFieldKey(bestTarget); const transitioned = new Set<Doc>(); - StrListCast(activeItem.presPinLayoutData) + StrListCast(activeItem.config_pinLayoutData) .map(str => JSON.parse(str) as { id: string; x: number; y: number; back: string; fill: string; w: number; h: number; data: string; text: string }) .forEach(async data => { const doc = DocCast(DocServer.GetCachedRefField(data.id)); @@ -562,8 +612,8 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { }); setTimeout(() => Array.from(transitioned).forEach(action(doc => (doc._dataTransition = undefined))), transTime + 10); } - if ((pinDataTypes?.pannable || (!pinDataTypes && (activeItem.presPinViewBounds !== undefined || activeItem.presPanX !== undefined || activeItem.presViewScale !== undefined))) && !bestTarget._isGroup) { - const contentBounds = Cast(activeItem.presPinViewBounds, listSpec('number')); + 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] }; bestTarget._freeform_panX = viewport.panX; @@ -571,15 +621,15 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { const dv = DocumentManager.Instance.getDocumentView(bestTarget); if (dv) { changed = true; - const computedScale = NumCast(activeItem.presZoom, 1) * Math.min(dv.props.PanelWidth() / viewport.width, dv.props.PanelHeight() / viewport.height); - activeItem.presMovement === PresMovement.Zoom && (bestTarget._freeform_scale = computedScale); - dv.ComponentView?.brushView?.(viewport); + 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); } } else { - if (bestTarget._freeform_panX !== activeItem.presPanX || bestTarget._freeform_panY !== activeItem.presPanY || bestTarget._freeform_scale !== activeItem.presViewScale) { - bestTarget._freeform_panX = activeItem.presPanX ?? bestTarget._freeform_panX; - bestTarget._freeform_panY = activeItem.presPanY ?? bestTarget._freeform_panY; - bestTarget._freeform_scale = activeItem.presViewScale ?? bestTarget._freeform_scale; + if (bestTarget._freeform_panX !== activeItem.config_panX || bestTarget._freeform_panY !== activeItem.config_panY || bestTarget._freeform_scale !== activeItem.config_viewScale) { + bestTarget._freeform_panX = activeItem.config_panX ?? bestTarget._freeform_panX; + bestTarget._freeform_panY = activeItem.config_panY ?? bestTarget._freeform_panY; + bestTarget._freeform_scale = activeItem.config_viewScale ?? bestTarget._freeform_scale; changed = true; } } @@ -594,17 +644,19 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { /// target doc when navigating to it. @action static pinDocView(pinDoc: Doc, pinProps: PinProps, targetDoc: Doc) { + pinDoc.presentation = true; + pinDoc.config = ''; if (pinProps.pinDocLayout) { - pinDoc.presPinLayout = true; - pinDoc.presX = NumCast(targetDoc.x); - pinDoc.presY = NumCast(targetDoc.y); - pinDoc.presRotation = NumCast(targetDoc.rotation); - pinDoc.presWidth = NumCast(targetDoc.width); - pinDoc.presHeight = NumCast(targetDoc.height); + pinDoc.config_pinLayout = true; + pinDoc.config_x = NumCast(targetDoc.x); + pinDoc.config_y = NumCast(targetDoc.y); + pinDoc.config_rotation = NumCast(targetDoc.rotation); + pinDoc.config_width = NumCast(targetDoc.width); + pinDoc.config_height = NumCast(targetDoc.height); } if (pinProps.pinAudioPlay) pinDoc.presPlayAudio = true; if (pinProps.pinData) { - pinDoc.presPinData = + pinDoc.config_pinData = pinProps.pinData.scrollable || pinProps.pinData.temporal || pinProps.pinData.pannable || @@ -616,30 +668,37 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { pinProps?.activeFrame !== undefined; const fkey = Doc.LayoutFieldKey(targetDoc); if (pinProps.pinData.dataview) { - pinDoc.presUsePath = targetDoc[fkey + '_usePath']; - pinDoc.presData = targetDoc[fkey] instanceof ObjectField ? (targetDoc[fkey] as ObjectField)[Copy]() : targetDoc.data; + pinDoc.config_usePath = targetDoc[fkey + '_usePath']; + pinDoc.config_data = targetDoc[fkey] instanceof ObjectField ? (targetDoc[fkey] as ObjectField)[Copy]() : targetDoc.data; } if (pinProps.pinData.dataannos) { const fkey = Doc.LayoutFieldKey(targetDoc); - pinDoc.presAnnotations = new List<Doc>(DocListCast(Doc.GetProto(targetDoc)[fkey + '_annotations']).filter(doc => !doc.layout_unrendered)); + pinDoc.config_annotations = new List<Doc>(DocListCast(Doc.GetProto(targetDoc)[fkey + '_annotations']).filter(doc => !doc.layout_unrendered)); } if (pinProps.pinData.inkable) { - pinDoc.presFillColor = targetDoc.fillColor; - pinDoc.presColor = targetDoc.color; - pinDoc.presWidth = targetDoc._width; - pinDoc.presHeight = targetDoc._height; + pinDoc.config_fillColor = targetDoc.fillColor; + pinDoc.config_color = targetDoc.color; + pinDoc.config_width = targetDoc._width; + pinDoc.config_height = targetDoc._height; } - if (pinProps.pinData.scrollable) pinDoc.presViewScroll = targetDoc._layout_scrollTop; + if (pinProps.pinData.scrollable) pinDoc.config_scrollTop = targetDoc._layout_scrollTop; if (pinProps.pinData.clippable) { const fkey = Doc.LayoutFieldKey(targetDoc); - pinDoc.presClipWidth = targetDoc[fkey + '_clipWidth']; + pinDoc.config_clipWidth = targetDoc[fkey + '_clipWidth']; } if (pinProps.pinData.datarange) { - pinDoc.presXRange = undefined; //targetDoc?.xrange; - pinDoc.presYRange = undefined; //targetDoc?.yrange; + pinDoc.config_xRange = undefined; //targetDoc?.xrange; + pinDoc.config_yRange = undefined; //targetDoc?.yrange; + } + if (pinProps.pinData.map) { + // pinDoc.config_latitude = targetDoc?.latitude; + // pinDoc.config_longitude = targetDoc?.longitude; + pinDoc.config_map_zoom = targetDoc?.map_zoom; + pinDoc.config_map_type = targetDoc?.map_type; + //... } if (pinProps.pinData.poslayoutview) - pinDoc.presPinLayoutData = new List<string>( + pinDoc.config_pinLayoutData = new List<string>( DocListCast(targetDoc[fkey] as ObjectField).map(d => JSON.stringify({ id: d[Id], @@ -654,30 +713,39 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { }) ) ); - if (pinProps.pinData.type_collection) pinDoc.presViewType = targetDoc._type_collection; - if (pinProps.pinData.filters) pinDoc.presDocFilters = ObjectField.MakeCopy(targetDoc.childFilters as ObjectField); - if (pinProps.pinData.pivot) pinDoc.presPivotField = targetDoc._pivotField; + if (pinProps.pinData.type_collection) pinDoc.config_viewType = targetDoc._type_collection; + if (pinProps.pinData.filters) pinDoc.config_docFilters = ObjectField.MakeCopy(targetDoc.childFilters as ObjectField); + if (pinProps.pinData.pivot) pinDoc.config_pivotField = targetDoc._pivotField; if (pinProps.pinData.pannable) { - pinDoc.presPanX = NumCast(targetDoc._freeform_panX); - pinDoc.presPanY = NumCast(targetDoc._freeform_panY); - pinDoc.presViewScale = NumCast(targetDoc._freeform_scale, 1); + pinDoc.config_panX = NumCast(targetDoc._freeform_panX); + pinDoc.config_panY = NumCast(targetDoc._freeform_panY); + pinDoc.config_viewScale = NumCast(targetDoc._freeform_scale, 1); } if (pinProps.pinData.temporal) { - pinDoc.presStartTime = targetDoc._layout_currentTimecode; - const duration = NumCast(pinDoc[`${Doc.LayoutFieldKey(pinDoc)}_duration`], NumCast(targetDoc.presStartTime) + 0.1); - pinDoc.presEndTime = NumCast(targetDoc.clipEnd, duration); + pinDoc.config_clipStart = targetDoc._layout_currentTimecode; + const duration = NumCast(pinDoc[`${Doc.LayoutFieldKey(pinDoc)}_duration`], NumCast(targetDoc.config_clipStart) + 0.1); + pinDoc.config_clipEnd = NumCast(targetDoc.clipEnd, duration); } } if (pinProps?.pinViewport) { // If pinWithView option set then update scale and x / y props of slide const bounds = pinProps.pinViewport; - pinDoc.presPinView = true; - pinDoc.presViewScale = NumCast(targetDoc._freeform_scale, 1); - pinDoc.presPanX = bounds.left + bounds.width / 2; - pinDoc.presPanY = bounds.top + bounds.height / 2; - pinDoc.presPinViewBounds = new List<number>([bounds.left, bounds.top, bounds.left + bounds.width, bounds.top + bounds.height]); + pinDoc.config_pinView = true; + pinDoc.config_viewScale = NumCast(targetDoc._freeform_scale, 1); + pinDoc.config_panX = bounds.left + bounds.width / 2; + pinDoc.config_panY = bounds.top + bounds.height / 2; + pinDoc.config_viewBounds = new List<number>([bounds.left, bounds.top, bounds.left + bounds.width, bounds.top + bounds.height]); } } + + @action + static reversePin(pinDoc: Doc, targetDoc: Doc) { + // const fkey = Doc.LayoutFieldKey(targetDoc); + pinDoc.config_data = targetDoc.data; + + console.log(pinDoc.presData); + } + /** * This method makes sure that cursor navigates to the element that * has the option open and last in the group. @@ -712,33 +780,34 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { }; static NavigateToTarget(targetDoc: Doc, activeItem: Doc, finished?: () => void) { - if (activeItem.presMovement === PresMovement.None && targetDoc.type === DocumentType.SCRIPTING) { + if (activeItem.presentation_movement === PresMovement.None && targetDoc.type === DocumentType.SCRIPTING) { (DocumentManager.Instance.getFirstDocumentView(targetDoc)?.ComponentView as ScriptingBox)?.onRun?.(); return; } - const effect = activeItem.presEffect && activeItem.presEffect !== PresEffect.None ? activeItem.presEffect : undefined; - const presTime = NumCast(activeItem.presTransition, effect ? 750 : 500); + 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 = { - willPan: activeItem.presMovement !== PresMovement.None, - willZoomCentered: activeItem.presMovement === PresMovement.Zoom || activeItem.presMovement === PresMovement.Jump || activeItem.presMovement === PresMovement.Center, - zoomScale: activeItem.presMovement === PresMovement.Center ? 0 : NumCast(activeItem.presZoom, 1), - zoomTime: activeItem.presMovement === PresMovement.Jump ? 0 : Math.min(Math.max(effect ? 750 : 500, (effect ? 0.2 : 1) * presTime), presTime), + 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, anchorDoc: activeItem, easeFunc: StrCast(activeItem.presEaseFunc, 'ease') as any, - zoomTextSelections: BoolCast(activeItem.presZoomText), + zoomTextSelections: BoolCast(activeItem.presentation_zoomText), playAudio: BoolCast(activeItem.presPlayAudio), + playMedia: activeItem.presentation_mediaStart === 'auto', }; - if (activeItem.presOpenInLightbox) { + if (activeItem.presentation_openInLightbox) { const context = DocCast(targetDoc.annotationOn) ?? targetDoc; if (!DocumentManager.Instance.getLightboxDocumentView(context)) { LightboxView.SetLightboxDoc(context); } } if (targetDoc) { - if (activeItem.presentationTargetDoc instanceof Doc) activeItem.presentationTargetDoc[Animation] = undefined; + if (activeItem.presentation_targetDoc instanceof Doc) activeItem.presentation_targetDoc[Animation] = undefined; DocumentManager.Instance.AddViewRenderedCb(LightboxView.LightboxDoc, dv => { // if target or the doc it annotates is not in the lightbox, then close the lightbox @@ -761,16 +830,16 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { const tagDoc = PresBox.targetRenderedDoc(curDoc); const itemIndexes: number[] = this.getAllIndexes(this.tagDocs, tagDoc); let opacity: Opt<number> = index === this.itemIndex ? 1 : undefined; - if (curDoc.presHide) { + if (curDoc.presentation_hide) { if (index !== this.itemIndex) { opacity = 1; } } const hidingIndBef = itemIndexes.find(item => item >= this.itemIndex) ?? itemIndexes.slice().reverse().lastElement(); - if (curDoc.presHideBefore && index === hidingIndBef) { + if (curDoc.presentation_hideBefore && index === hidingIndBef) { if (index > this.itemIndex) { opacity = 0; - } else if (index === this.itemIndex || !curDoc.presHideAfter) { + } else if (index === this.itemIndex || !curDoc.presentation_hideAfter) { opacity = 1; setTimeout(() => (tagDoc._dataTransition = undefined), 1000); } @@ -780,15 +849,15 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { .slice() .reverse() .find(item => item <= this.itemIndex) ?? itemIndexes.lastElement(); - if (curDoc.presHideAfter && index === hidingIndAft) { + if (curDoc.presentation_hideAfter && index === hidingIndAft) { if (index < this.itemIndex) { opacity = 0; - } else if (index === this.itemIndex || !curDoc.presHideBefore) { + } else if (index === this.itemIndex || !curDoc.presentation_hideBefore) { opacity = 1; } } const hidingInd = itemIndexes.find(item => item === this.itemIndex); - if (curDoc.presHide && index === hidingInd) { + if (curDoc.presentation_hide && index === hidingInd) { if (index === this.itemIndex) { opacity = 0; } @@ -846,9 +915,9 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { // The function pauses the auto presentation @action pauseAutoPres = () => { - if (this.layoutDoc.presStatus === PresStatus.Autoplay) { + if (this.layoutDoc.presentation_status === PresStatus.Autoplay) { if (this._presTimer) clearTimeout(this._presTimer); - this.layoutDoc.presStatus = PresStatus.Manual; + this.layoutDoc.presentation_status = PresStatus.Manual; this.childDocs.forEach(this.stopTempMedia); } }; @@ -879,23 +948,23 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { runInAction(() => (this._expandBoolean = !this._expandBoolean)); this.rootDoc.expandBoolean = this._expandBoolean; this.childDocs.forEach(doc => { - doc.presExpandInlineButton = this._expandBoolean; + doc.presentation_expandInlineButton = this._expandBoolean; }); }; initializePresState = (startIndex: number) => { this.childDocs.forEach((doc, index) => { const tagDoc = PresBox.targetRenderedDoc(doc); - if (doc.presHideBefore && index > startIndex) tagDoc.opacity = 0; - if (doc.presHideAfter && index < startIndex) tagDoc.opacity = 0; - if (doc.presIndexed !== undefined && index >= startIndex) { - const startInd = NumCast(doc.presIndexedStart); + if (doc.presentation_hideBefore && index > startIndex) tagDoc.opacity = 0; + if (doc.presentation_hideAfter && index < startIndex) tagDoc.opacity = 0; + if (doc.presentation_indexed !== undefined && index >= startIndex) { + const startInd = NumCast(doc.presentation_indexedStart); this.progressivizedItems(doc) ?.slice(startInd) .forEach(indexedDoc => (indexedDoc.opacity = 0)); - doc.presIndexed = Math.min(this.progressivizedItems(doc)?.length ?? 0, startInd); + doc.presentation_indexed = Math.min(this.progressivizedItems(doc)?.length ?? 0, startInd); } - // if (doc.presHide && this.childDocs.indexOf(doc) === startIndex) tagDoc.opacity = 0; + // if (doc.presentation_hide && this.childDocs.indexOf(doc) === startIndex) tagDoc.opacity = 0; }); }; @@ -909,13 +978,13 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { PresBox.Instance = this; clearTimeout(this._presTimer); if (this.childDocs.length) { - this.layoutDoc.presStatus = PresStatus.Autoplay; + this.layoutDoc.presentation_status = PresStatus.Autoplay; this.initializePresState(startIndex); const func = () => { - const delay = NumCast(this.activeItem.presDuration, this.activeItem.type === DocumentType.SCRIPTING ? 0 : 2500) + NumCast(this.activeItem.presTransition); + const delay = NumCast(this.activeItem.presentation_duration, this.activeItem.type === DocumentType.SCRIPTING ? 0 : 2500) + NumCast(this.activeItem.presentation_transition); this._presTimer = setTimeout(() => { - if (!this.next()) this.layoutDoc.presStatus = this._exitTrail?.() ?? PresStatus.Manual; - this.layoutDoc.presStatus === PresStatus.Autoplay && func(); + if (!this.next()) this.layoutDoc.presentation_status = this._exitTrail?.() ?? PresStatus.Manual; + this.layoutDoc.presentation_status === PresStatus.Autoplay && func(); }, delay); }; this.gotoDocument(startIndex, this.activeItem, undefined, func); @@ -949,7 +1018,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { doc._width = PresBox.minimizedWidth; Doc.AddToMyOverlay(doc); PresBox.Instance?.initializePresState(PresBox.Instance.itemIndex); - return (doc.presStatus = PresStatus.Manual); + return (doc.presentation_status = PresStatus.Manual); } /** @@ -993,39 +1062,37 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { }); movementName = action((activeItem: Doc) => { - if (![PresMovement.Zoom, PresMovement.Pan, PresMovement.Center, PresMovement.Jump, PresMovement.None].includes(StrCast(activeItem.presMovement) as any)) { + if (![PresMovement.Zoom, PresMovement.Pan, PresMovement.Center, PresMovement.Jump, PresMovement.None].includes(StrCast(activeItem.presentation_movement) as any)) { return PresMovement.Zoom; } - return StrCast(activeItem.presMovement); + return StrCast(activeItem.presentation_movement); }); whenChildContentsActiveChanged = action((isActive: boolean) => this.props.whenChildContentsActiveChanged((this._isChildActive = isActive))); // For dragging documents into the presentation trail addDocumentFilter = (docs: Doc[]) => { docs.forEach((doc, i) => { - if (doc.presentationTargetDoc) return true; + if (doc.presentation_targetDoc) return true; if (doc.type === DocumentType.LABEL) { const audio = Cast(doc.annotationOn, Doc, null); if (audio) { - audio.mediaStart = 'manual'; - audio.mediaStop = 'manual'; - audio.presStartTime = NumCast(doc._timecodeToShow /* audioStart */, NumCast(doc._timecodeToShow /* videoStart */)); - audio.presEndTime = NumCast(doc._timecodeToHide /* audioEnd */, NumCast(doc._timecodeToHide /* videoEnd */)); - audio.presDuration = audio.presStartTime - audio.presEndTime; + 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 }); setTimeout(() => this.removeDocument(doc), 0); return false; } } else { - if (!doc.presentationTargetDoc) doc.title = doc.title + ' - Slide'; - doc.presentationTargetDoc = doc.createdFrom; // dropped document will be a new embedding of an embedded document somewhere else. - doc.presMovement = PresMovement.Zoom; - if (this._expandBoolean) doc.presExpandInlineButton = true; + 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; + if (this._expandBoolean) doc.presentation_expandInlineButton = true; } }); return true; }; - childLayoutTemplate = () => (!this.isTreeOrStack ? DocCast(Doc.UserDoc().presElement) : DocCast(Doc.UserDoc().presElement)); + 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; @@ -1040,7 +1107,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { @computed get listOfSelected() { return Array.from(this.selectedArray).map((doc: Doc, index: any) => { const curDoc = Cast(doc, Doc, null); - const tagDoc = Cast(curDoc.presentationTargetDoc, Doc, null); + const tagDoc = Cast(curDoc.presentation_targetDoc, Doc, null); if (curDoc && curDoc === this.activeItem) return ( <div key={index} className="selectedList-items"> @@ -1084,7 +1151,9 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { if (index >= 0 && index < this.childDocs.length) { this.rootDoc._itemIndex = index; } - } else this.gotoDocument(this.childDocs.indexOf(doc), this.activeItem); + } else { + this.gotoDocument(this.childDocs.indexOf(doc), this.activeItem); + } this.updateCurrentPresentation(DocCast(doc.embedContainer)); }; @@ -1146,7 +1215,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { if (anchorNode && anchorNode.className?.includes('lm_title')) return; switch (e.key) { case 'Backspace': - if (this.layoutDoc.presStatus === 'edit') { + if (this.layoutDoc.presentation_status === 'edit') { undoBatch( action(() => { for (const doc of this.selectedArray) { @@ -1163,11 +1232,11 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { case 'Escape': if (Doc.IsInMyOverlay(this.layoutDoc)) { this.exitClicked(); - } else if (this.layoutDoc.presStatus === PresStatus.Edit) { + } else if (this.layoutDoc.presentation_status === PresStatus.Edit) { this.clearSelectedArray(); this._eleArray.length = this._dragArray.length = 0; } else { - this.layoutDoc.presStatus = PresStatus.Edit; + this.layoutDoc.presentation_status = PresStatus.Edit; } if (this._presTimer) clearTimeout(this._presTimer); handled = true; @@ -1184,7 +1253,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { this.next(); if (this._presTimer) { clearTimeout(this._presTimer); - this.layoutDoc.presStatus = PresStatus.Manual; + this.layoutDoc.presentation_status = PresStatus.Manual; } } handled = true; @@ -1201,19 +1270,19 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { this.back(); if (this._presTimer) { clearTimeout(this._presTimer); - this.layoutDoc.presStatus = PresStatus.Manual; + this.layoutDoc.presentation_status = PresStatus.Manual; } } handled = true; break; case 'Spacebar': case ' ': - if (this.layoutDoc.presStatus === PresStatus.Manual) this.startOrPause(true); - else if (this.layoutDoc.presStatus === PresStatus.Autoplay) if (this._presTimer) clearTimeout(this._presTimer); + if (this.layoutDoc.presentation_status === PresStatus.Manual) this.startOrPause(true); + else if (this.layoutDoc.presentation_status === PresStatus.Autoplay) if (this._presTimer) clearTimeout(this._presTimer); handled = true; break; case 'a': - if ((e.metaKey || e.altKey) && this.layoutDoc.presStatus === 'edit') { + if ((e.metaKey || e.altKey) && this.layoutDoc.presentation_status === 'edit') { this.clearSelectedArray(); this.childDocs.forEach(doc => this.addToSelectedArray(doc)); handled = true; @@ -1236,9 +1305,9 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { const presCollection = DocumentManager.GetContextPath(this.activeItem).reverse().lastElement(); const dv = DocumentManager.Instance.getDocumentView(presCollection); this.childDocs - .filter(doc => Cast(doc.presentationTargetDoc, Doc, null)) + .filter(doc => Cast(doc.presentation_targetDoc, Doc, null)) .forEach((doc, index) => { - const tagDoc = Cast(doc.presentationTargetDoc, Doc, null); + const tagDoc = Cast(doc.presentation_targetDoc, Doc, null); const srcContext = Cast(tagDoc.embedContainer, Doc, null); const width = NumCast(tagDoc._width) / 10; const height = Math.max(NumCast(tagDoc._height) / 10, 15); @@ -1271,17 +1340,17 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { </div> ); } - } else if (doc.presPinView && presCollection === tagDoc && dv) { + } else if (doc.config_pinView && presCollection === tagDoc && dv) { // Case B: Document is presPinView and is presCollection - const scale: number = 1 / NumCast(doc.presViewScale); + const scale: number = 1 / NumCast(doc.config_viewScale); const height: number = dv.props.PanelHeight() * scale; const width: number = dv.props.PanelWidth() * scale; const indWidth = width / 10; const indHeight = Math.max(height / 10, 15); const indEdge = Math.max(indWidth, indHeight); const indFontSize = indEdge * 0.8; - const xLoc: number = NumCast(doc.presPanX) - width / 2; - const yLoc: number = NumCast(doc.presPanY) - height / 2; + const xLoc: number = NumCast(doc.config_panX) - width / 2; + const yLoc: number = NumCast(doc.config_panY) - height / 2; docs.push(tagDoc); order.push( <> @@ -1307,15 +1376,15 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { @computed get paths() { let pathPoints = ''; this.childDocs.forEach((doc, index) => { - const tagDoc = Cast(doc.presentationTargetDoc, Doc, null); + const tagDoc = Cast(doc.presentation_targetDoc, Doc, null); if (tagDoc) { const n1x = NumCast(tagDoc.x) + NumCast(tagDoc._width) / 2; const n1y = NumCast(tagDoc.y) + NumCast(tagDoc._height) / 2; if ((index = 0)) pathPoints = n1x + ',' + n1y; else pathPoints = pathPoints + ' ' + n1x + ',' + n1y; - } else if (doc.presPinView) { - const n1x = NumCast(doc.presPanX); - const n1y = NumCast(doc.presPanY); + } else if (doc.config_pinView) { + const n1x = NumCast(doc.config_panX); + const n1y = NumCast(doc.config_panY); if ((index = 0)) pathPoints = n1x + ',' + n1y; else pathPoints = pathPoints + ' ' + n1x + ',' + n1y; } @@ -1339,7 +1408,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { } getPaths = (collection: Doc) => this.paths; // needs to be smarter and figure out the paths to draw for this specific collection. or better yet, draw everything in an overlay layer instad of within a collection - // Converts seconds to ms and updates presTransition + // Converts seconds to ms and updates presentation_transition public static SetTransitionTime = (number: String, setter: (timeInMS: number) => void, change?: number) => { let timeInMS = Number(number) * 1000; if (change) timeInMS += change; @@ -1350,21 +1419,21 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { @undoBatch updateTransitionTime = (number: String, change?: number) => { - PresBox.SetTransitionTime(number, (timeInMS: number) => this.selectedArray.forEach(doc => (doc.presTransition = timeInMS)), change); + PresBox.SetTransitionTime(number, (timeInMS: number) => this.selectedArray.forEach(doc => (doc.presentation_transition = timeInMS)), change); }; - // Converts seconds to ms and updates presTransition + // Converts seconds to ms and updates presentation_transition @undoBatch updateZoom = (number: String, change?: number) => { let scale = Number(number) / 100; if (change) scale += change; if (scale < 0.01) scale = 0.01; if (scale > 1) scale = 1; - this.selectedArray.forEach(doc => (doc.presZoom = scale)); + this.selectedArray.forEach(doc => (doc.config_zoom = scale)); }; /* - * Converts seconds to ms and updates presDuration + * Converts seconds to ms and updates presentation_duration */ @undoBatch updateDurationTime = (number: String, change?: number) => { @@ -1372,38 +1441,38 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { if (change) timeInMS += change; if (timeInMS < 100) timeInMS = 100; if (timeInMS > 20000) timeInMS = 20000; - this.selectedArray.forEach(doc => (doc.presDuration = timeInMS)); + this.selectedArray.forEach(doc => (doc.presentation_duration = timeInMS)); }; @undoBatch - updateMovement = action((movement: PresMovement, all?: boolean) => (all ? this.childDocs : this.selectedArray).forEach(doc => (doc.presMovement = movement))); + updateMovement = action((movement: PresMovement, all?: boolean) => (all ? this.childDocs : this.selectedArray).forEach(doc => (doc.presentation_movement = movement))); @undoBatch @action updateHideBefore = (activeItem: Doc) => { - activeItem.presHideBefore = !activeItem.presHideBefore; - this.selectedArray.forEach(doc => (doc.presHideBefore = activeItem.presHideBefore)); + activeItem.presentation_hideBefore = !activeItem.presentation_hideBefore; + this.selectedArray.forEach(doc => (doc.presentation_hideBefore = activeItem.presentation_hideBefore)); }; @undoBatch @action updateHide = (activeItem: Doc) => { - activeItem.presHide = !activeItem.presHide; - this.selectedArray.forEach(doc => (doc.presHide = activeItem.presHide)); + activeItem.presentation_hide = !activeItem.presentation_hide; + this.selectedArray.forEach(doc => (doc.presentation_hide = activeItem.presentation_hide)); }; @undoBatch @action updateHideAfter = (activeItem: Doc) => { - activeItem.presHideAfter = !activeItem.presHideAfter; - this.selectedArray.forEach(doc => (doc.presHideAfter = activeItem.presHideAfter)); + activeItem.presentation_hideAfter = !activeItem.presentation_hideAfter; + this.selectedArray.forEach(doc => (doc.presentation_hideAfter = activeItem.presentation_hideAfter)); }; @undoBatch @action updateOpenDoc = (activeItem: Doc) => { - activeItem.presOpenInLightbox = !activeItem.presOpenInLightbox; - this.selectedArray.forEach(doc => (doc.presOpenInLightbox = activeItem.presOpenInLightbox)); + activeItem.presentation_openInLightbox = !activeItem.presentation_openInLightbox; + this.selectedArray.forEach(doc => (doc.presentation_openInLightbox = activeItem.presentation_openInLightbox)); }; @undoBatch @@ -1415,11 +1484,11 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { @undoBatch @action - updateEffectDirection = (effect: PresEffectDirection, all?: boolean) => (all ? this.childDocs : this.selectedArray).forEach(doc => (doc.presEffectDirection = effect)); + 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.presEffect = effect))); + updateEffect = (effect: PresEffect, bullet: boolean, all?: boolean) => (all ? this.childDocs : this.selectedArray).forEach(doc => (bullet ? (doc.presBulletEffect = effect) : (doc.presentation_effect = effect))); static _sliderBatch: any; public static inputter = (min: string, step: string, max: string, value: number, active: boolean, change: (val: string) => void, hmargin?: number) => { @@ -1431,7 +1500,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { max={max} value={value} readOnly={true} - style={{ marginLeft: hmargin, marginRight: hmargin, width: `calc(100% - ${2 * (hmargin ?? 0)}px)` }} + style={{ marginLeft: hmargin, marginRight: hmargin, width: `calc(100% - ${2 * (hmargin ?? 0)}px)`, background: SettingsManager.userColor, color: SettingsManager.userVariantColor }} className={`toolbar-slider ${active ? '' : 'none'}`} onPointerDown={e => { PresBox._sliderBatch = UndoManager.StartBatch('pres slider'); @@ -1449,52 +1518,71 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { @undoBatch @action applyTo = (array: Doc[]) => { - this.updateMovement(this.activeItem.presMovement as PresMovement, true); - this.updateEffect(this.activeItem.presEffect as PresEffect, false, true); + this.updateMovement(this.activeItem.presentation_movement as PresMovement, true); + this.updateEffect(this.activeItem.presentation_effect as PresEffect, false, true); this.updateEffect(this.activeItem.presBulletEffect as PresEffect, true, true); - this.updateEffectDirection(this.activeItem.presEffectDirection as PresEffectDirection, true); - const { presTransition, presDuration, presHideBefore, presHideAfter } = this.activeItem; + this.updateEffectDirection(this.activeItem.presentation_effectDirection as PresEffectDirection, true); + const { presentation_transition, presentation_duration, presentation_hideBefore, presentation_hideAfter } = this.activeItem; array.forEach(curDoc => { - curDoc.presTransition = presTransition; - curDoc.presDuration = presDuration; - curDoc.presHideBefore = presHideBefore; - curDoc.presHideAfter = presHideAfter; + curDoc.presentation_transition = presentation_transition; + curDoc.presentation_duration = presentation_duration; + curDoc.presentation_hideBefore = presentation_hideBefore; + curDoc.presentation_hideAfter = presentation_hideAfter; }); }; - @computed get visibiltyDurationDropdown() { + @computed get visibilityDurationDropdown() { const activeItem = this.activeItem; if (activeItem && this.targetDoc) { const targetType = this.targetDoc.type; - let duration = activeItem.presDuration ? NumCast(activeItem.presDuration) / 1000 : 0; + let duration = activeItem.presentation_duration ? NumCast(activeItem.presentation_duration) / 1000 : 0; if (activeItem.type === DocumentType.AUDIO) duration = NumCast(activeItem.duration); return ( <div className="presBox-ribbon"> <div className="ribbon-doubleButton"> - <Tooltip title={<div className="dash-tooltip">{'Hide before presented'}</div>}> - <div className={`ribbon-toggle ${activeItem.presHideBefore ? 'active' : ''}`} onClick={() => this.updateHideBefore(activeItem)}> + <Tooltip title={<div className="dash-tooltip">Hide before presented</div>}> + <div + className={`ribbon-toggle ${activeItem.presentation_hideBefore ? 'active' : ''}`} + style={{ border: `solid 1px ${SettingsManager.userColor}`, color: SettingsManager.userColor, background: activeItem.presentation_hideBefore ? SettingsManager.userVariantColor : SettingsManager.userBackgroundColor }} + onClick={() => this.updateHideBefore(activeItem)}> Hide before </div> </Tooltip> <Tooltip title={<div className="dash-tooltip">{'Hide while presented'}</div>}> - <div className={`ribbon-toggle ${activeItem.presHide ? 'active' : ''}`} onClick={() => this.updateHide(activeItem)}> + <div + className={`ribbon-toggle ${activeItem.presentation_hide ? 'active' : ''}`} + style={{ border: `solid 1px ${SettingsManager.userColor}`, color: SettingsManager.userColor, background: activeItem.presentation_hide ? SettingsManager.userVariantColor : SettingsManager.userBackgroundColor }} + onClick={() => this.updateHide(activeItem)}> Hide </div> </Tooltip> <Tooltip title={<div className="dash-tooltip">{'Hide after presented'}</div>}> - <div className={`ribbon-toggle ${activeItem.presHideAfter ? 'active' : ''}`} onClick={() => this.updateHideAfter(activeItem)}> + <div + className={`ribbon-toggle ${activeItem.presentation_hideAfter ? 'active' : ''}`} + style={{ border: `solid 1px ${SettingsManager.userColor}`, color: SettingsManager.userColor, background: activeItem.presentation_hideAfter ? SettingsManager.userVariantColor : SettingsManager.userBackgroundColor }} + onClick={() => this.updateHideAfter(activeItem)}> Hide after </div> </Tooltip> <Tooltip title={<div className="dash-tooltip">{'Open in lightbox view'}</div>}> - <div className="ribbon-toggle" style={{ backgroundColor: activeItem.presOpenInLightbox ? Colors.LIGHT_BLUE : '' }} onClick={() => this.updateOpenDoc(activeItem)}> + <div + className="ribbon-toggle" + style={{ + border: `solid 1px ${SettingsManager.userColor}`, + color: SettingsManager.userColor, + background: activeItem.presentation_openInLightbox ? SettingsManager.userVariantColor : SettingsManager.userBackgroundColor, + }} + onClick={() => this.updateOpenDoc(activeItem)}> Lightbox </div> </Tooltip> - <Tooltip title={<div className="dash-tooltip">{'Transition movement style'}</div>}> - <div className="ribbon-toggle" onClick={() => this.updateEaseFunc(activeItem)}> + <Tooltip title={<div className="dash-tooltip">Transition movement style</div>}> + <div + className="ribbon-toggle" + style={{ border: `solid 1px ${SettingsManager.userColor}`, color: SettingsManager.userColor, background: activeItem.presEaseFunc === 'ease' ? SettingsManager.userVariantColor : SettingsManager.userBackgroundColor }} + onClick={() => this.updateEaseFunc(activeItem)}> {`${StrCast(activeItem.presEaseFunc, 'ease')}`} </div> </Tooltip> @@ -1503,10 +1591,10 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { <> <div className="ribbon-doubleButton"> <div className="presBox-subheading">Slide Duration</div> - <div className="ribbon-property"> + <div className="ribbon-property" style={{ border: `solid 1px ${SettingsManager.userColor}` }}> <input className="presBox-input" type="number" readOnly={true} value={duration} onKeyDown={e => e.stopPropagation()} onChange={e => this.updateDurationTime(e.target.value)} /> s </div> - <div className="ribbon-propertyUpDown"> + <div className="ribbon-propertyUpDown" style={{ color: SettingsManager.userBackgroundColor, background: SettingsManager.userColor }}> <div className="ribbon-propertyUpDownItem" onClick={() => this.updateDurationTime(String(duration), 1000)}> <FontAwesomeIcon icon={'caret-up'} /> </div> @@ -1532,7 +1620,10 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { if (activeItem && this.targetDoc) { const effect = activeItem.presBulletEffect ? activeItem.presBulletEffect : PresMovement.None; const bulletEffect = (effect: PresEffect) => ( - <div className={`presBox-dropdownOption ${activeItem.presEffect === effect || (effect === PresEffect.None && !activeItem.presEffect) ? 'active' : ''}`} onPointerDown={StopEvent} onClick={() => this.updateEffect(effect, true)}> + <div + className={`presBox-dropdownOption ${activeItem.presentation_effect === effect || (effect === PresEffect.None && !activeItem.presentation_effect) ? 'active' : ''}`} + onPointerDown={StopEvent} + onClick={() => this.updateEffect(effect, true)}> {effect} </div> ); @@ -1542,32 +1633,44 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { <div className="presBox-subheading">Progressivize Collection</div> <input className="presBox-checkbox" - style={{ margin: 10 }} + style={{ margin: 10, border: `solid 1px ${SettingsManager.userColor}` }} type="checkbox" onChange={() => { - activeItem.presIndexed = activeItem.presIndexed === undefined ? 0 : undefined; - activeItem.presHideBefore = activeItem.presIndexed !== undefined; + activeItem.presentation_indexed = activeItem.presentation_indexed === undefined ? 0 : undefined; + activeItem.presentation_hideBefore = activeItem.presentation_indexed !== undefined; const tagDoc = PresBox.targetRenderedDoc(this.activeItem); const type = DocCast(tagDoc?.annotationOn)?.type ?? tagDoc.type; - activeItem.presIndexedStart = type === DocumentType.COL ? 1 : 0; + activeItem.presentation_indexedStart = type === DocumentType.COL ? 1 : 0; // a progressivized slide doesn't have sub-slides, but rather iterates over the data list of the target being progressivized. // to avoid creating a new slide to correspond to each of the target's data list, we create a computedField to refernce the target's data list. let dataField = Doc.LayoutFieldKey(tagDoc); if (Cast(tagDoc[dataField], listSpec(Doc), null)?.filter(d => d instanceof Doc) === undefined) dataField = dataField + '_annotations'; - if (DocCast(activeItem.presentationTargetDoc).annotationOn) activeItem.data = ComputedField.MakeFunction(`self.presentationTargetDoc.annotationOn["${dataField}"]`); - else activeItem.data = ComputedField.MakeFunction(`self.presentationTargetDoc["${dataField}"]`); + if (DocCast(activeItem.presentation_targetDoc).annotationOn) activeItem.data = ComputedField.MakeFunction(`self.presentation_targetDoc.annotationOn["${dataField}"]`); + else activeItem.data = ComputedField.MakeFunction(`self.presentation_targetDoc["${dataField}"]`); }} - checked={Cast(activeItem.presIndexed, 'number', null) !== undefined ? true : false} + checked={Cast(activeItem.presentation_indexed, 'number', null) !== undefined ? true : false} /> </div> <div className="ribbon-doubleButton" style={{ display: 'inline-flex' }}> <div className="presBox-subheading">Progressivize First Bullet</div> - <input className="presBox-checkbox" style={{ margin: 10 }} type="checkbox" onChange={() => (activeItem.presIndexedStart = activeItem.presIndexedStart ? 0 : 1)} checked={!NumCast(activeItem.presIndexedStart)} /> + <input + className="presBox-checkbox" + style={{ margin: 10, border: `solid 1px ${SettingsManager.userColor}` }} + type="checkbox" + onChange={() => (activeItem.presentation_indexedStart = activeItem.presentation_indexedStart ? 0 : 1)} + checked={!NumCast(activeItem.presentation_indexedStart)} + /> </div> <div className="ribbon-doubleButton" style={{ display: 'inline-flex' }}> <div className="presBox-subheading">Expand Current Bullet</div> - <input className="presBox-checkbox" style={{ margin: 10 }} type="checkbox" onChange={() => (activeItem.presBulletExpand = !activeItem.presBulletExpand)} checked={BoolCast(activeItem.presBulletExpand)} /> + <input + className="presBox-checkbox" + style={{ margin: 10, border: `solid 1px ${SettingsManager.userColor}` }} + type="checkbox" + onChange={() => (activeItem.presBulletExpand = !activeItem.presBulletExpand)} + checked={BoolCast(activeItem.presBulletExpand)} + /> </div> <div className="ribbon-box"> @@ -1578,10 +1681,18 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { e.stopPropagation(); this._openBulletEffectDropdown = !this._openBulletEffectDropdown; })} - style={{ borderBottomLeftRadius: this._openBulletEffectDropdown ? 0 : 5, border: this._openBulletEffectDropdown ? `solid 2px ${Colors.MEDIUM_BLUE}` : 'solid 1px black' }}> + style={{ + color: SettingsManager.userColor, + background: SettingsManager.userVariantColor, + borderBottomLeftRadius: this._openBulletEffectDropdown ? 0 : 5, + border: this._openBulletEffectDropdown ? `solid 2px ${SettingsManager.userVariantColor}` : `solid 1px ${SettingsManager.userColor}`, + }}> {effect?.toString()} <FontAwesomeIcon className="presBox-dropdownIcon" style={{ gridColumn: 2, color: this._openBulletEffectDropdown ? Colors.MEDIUM_BLUE : 'black' }} icon={'angle-down'} /> - <div className={'presBox-dropdownOptions'} style={{ display: this._openBulletEffectDropdown ? 'grid' : 'none' }} onPointerDown={e => e.stopPropagation()}> + <div + 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)} @@ -1598,18 +1709,21 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { } @computed get transitionDropdown() { const activeItem = this.activeItem; - const presEffect = (effect: PresEffect) => ( - <div className={`presBox-dropdownOption ${activeItem.presEffect === effect || (effect === PresEffect.None && !activeItem.presEffect) ? 'active' : ''}`} onPointerDown={StopEvent} onClick={() => this.updateEffect(effect, false)}> + const preseEffect = (effect: PresEffect) => ( + <div + className={`presBox-dropdownOption ${activeItem.presentation_effect === effect || (effect === PresEffect.None && !activeItem.presentation_effect) ? 'active' : ''}`} + onPointerDown={StopEvent} + onClick={() => this.updateEffect(effect, false)}> {effect} </div> ); const presMovement = (movement: PresMovement) => ( - <div className={`presBox-dropdownOption ${activeItem.presMovement === movement ? 'active' : ''}`} onPointerDown={StopEvent} onClick={() => this.updateMovement(movement)}> + <div className={`presBox-dropdownOption ${activeItem.presentation_movement === movement ? 'active' : ''}`} onPointerDown={StopEvent} onClick={() => this.updateMovement(movement)}> {movement} </div> ); const presDirection = (direction: PresEffectDirection, icon: string, gridColumn: number, gridRow: number, opts: object) => { - const color = activeItem.presEffectDirection === direction || (direction === PresEffectDirection.Center && !activeItem.presEffectDirection) ? Colors.LIGHT_BLUE : 'black'; + const color = activeItem.presentation_effectDirection === direction || (direction === PresEffectDirection.Center && !activeItem.presentation_effectDirection) ? SettingsManager.userVariantColor : SettingsManager.userColor; return ( <Tooltip title={<div className="dash-tooltip">{direction}</div>}> <div @@ -1621,12 +1735,12 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { ); }; if (activeItem && this.targetDoc) { - const transitionSpeed = activeItem.presTransition ? NumCast(activeItem.presTransition) / 1000 : 0.5; - const zoom = NumCast(activeItem.presZoom, 1) * 100; - const effect = activeItem.presEffect ? activeItem.presEffect : PresMovement.None; + const transitionSpeed = activeItem.presentation_transition ? NumCast(activeItem.presentation_transition) / 1000 : 0.5; + const zoom = NumCast(activeItem.config_zoom, 1) * 100; + const effect = activeItem.presentation_effect ? activeItem.presentation_effect : PresMovement.None; return ( <div - className={`presBox-ribbon ${this._transitionTools && this.layoutDoc.presStatus === PresStatus.Edit ? 'active' : ''}`} + className={`presBox-ribbon ${this._transitionTools && this.layoutDoc.presentation_status === PresStatus.Edit ? 'active' : ''}`} onPointerDown={StopEvent} onPointerUp={StopEvent} onClick={action(e => { @@ -1643,10 +1757,23 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { e.stopPropagation(); this._openMovementDropdown = !this._openMovementDropdown; })} - style={{ borderBottomLeftRadius: this._openMovementDropdown ? 0 : 5, border: this._openMovementDropdown ? `solid 2px ${Colors.MEDIUM_BLUE}` : 'solid 1px black' }}> + style={{ + color: SettingsManager.userColor, + background: SettingsManager.userVariantColor, + borderBottomLeftRadius: this._openMovementDropdown ? 0 : 5, + border: this._openMovementDropdown ? `solid 2px ${SettingsManager.userVariantColor}` : `solid 1px ${SettingsManager.userColor}`, + }}> {this.movementName(activeItem)} <FontAwesomeIcon className="presBox-dropdownIcon" style={{ gridColumn: 2, color: this._openMovementDropdown ? Colors.MEDIUM_BLUE : 'black' }} icon={'angle-down'} /> - <div className={'presBox-dropdownOptions'} id={'presBoxMovementDropdown'} onPointerDown={StopEvent} style={{ display: this._openMovementDropdown ? 'grid' : 'none' }}> + <div + className="presBox-dropdownOptions" + id={'presBoxMovementDropdown'} + onPointerDown={StopEvent} + style={{ + color: SettingsManager.userColor, + background: SettingsManager.userBackgroundColor, + display: this._openMovementDropdown ? 'grid' : 'none', + }}> {presMovement(PresMovement.None)} {presMovement(PresMovement.Center)} {presMovement(PresMovement.Zoom)} @@ -1654,12 +1781,12 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { {presMovement(PresMovement.Jump)} </div> </div> - <div className="ribbon-doubleButton" style={{ display: activeItem.presMovement === PresMovement.Zoom ? 'inline-flex' : 'none' }}> + <div className="ribbon-doubleButton" style={{ display: activeItem.presentation_movement === PresMovement.Zoom ? 'inline-flex' : 'none' }}> <div className="presBox-subheading">Zoom (% screen filled)</div> - <div className="ribbon-property"> + <div className="ribbon-property" style={{ border: `solid 1px ${SettingsManager.userColor}` }}> <input className="presBox-input" type="number" readOnly={true} value={zoom} onChange={e => this.updateZoom(e.target.value)} />% </div> - <div className="ribbon-propertyUpDown"> + <div className="ribbon-propertyUpDown" style={{ color: SettingsManager.userBackgroundColor, background: SettingsManager.userColor }}> <div className="ribbon-propertyUpDownItem" onClick={() => this.updateZoom(String(zoom), 0.1)}> <FontAwesomeIcon icon={'caret-up'} /> </div> @@ -1668,13 +1795,13 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { </div> </div> </div> - {PresBox.inputter('0', '1', '100', zoom, activeItem.presMovement === PresMovement.Zoom, this.updateZoom)} + {PresBox.inputter('0', '1', '100', zoom, activeItem.presentation_movement === PresMovement.Zoom, this.updateZoom)} <div className="ribbon-doubleButton" style={{ display: 'inline-flex' }}> <div className="presBox-subheading">Transition Time</div> - <div className="ribbon-property"> + <div className="ribbon-property" style={{ border: `solid 1px ${SettingsManager.userColor}` }}> <input className="presBox-input" type="number" readOnly={true} value={transitionSpeed} onKeyDown={e => e.stopPropagation()} onChange={action(e => this.updateTransitionTime(e.target.value))} /> s </div> - <div className="ribbon-propertyUpDown"> + <div className="ribbon-propertyUpDown" style={{ color: SettingsManager.userBackgroundColor, background: SettingsManager.userColor }}> <div className="ribbon-propertyUpDownItem" onClick={() => this.updateTransitionTime(String(transitionSpeed), 1000)}> <FontAwesomeIcon icon={'caret-up'} /> </div> @@ -1694,11 +1821,23 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { Effects <div className="ribbon-doubleButton" style={{ display: 'inline-flex' }}> <div className="presBox-subheading">Play Audio Annotation</div> - <input className="presBox-checkbox" style={{ margin: 10 }} type="checkbox" onChange={() => (activeItem.presPlayAudio = !BoolCast(activeItem.presPlayAudio))} checked={BoolCast(activeItem.presPlayAudio)} /> + <input + className="presBox-checkbox" + style={{ margin: 10, border: `solid 1px ${SettingsManager.userColor}` }} + type="checkbox" + onChange={() => (activeItem.presPlayAudio = !BoolCast(activeItem.presPlayAudio))} + checked={BoolCast(activeItem.presPlayAudio)} + /> </div> <div className="ribbon-doubleButton" style={{ display: 'inline-flex' }}> <div className="presBox-subheading">Zoom Text Selections</div> - <input className="presBox-checkbox" style={{ margin: 10 }} type="checkbox" onChange={() => (activeItem.presZoomText = !BoolCast(activeItem.presZoomText))} checked={BoolCast(activeItem.presZoomText)} /> + <input + className="presBox-checkbox" + style={{ margin: 10, border: `solid 1px ${SettingsManager.userColor}` }} + type="checkbox" + onChange={() => (activeItem.presentation_zoomText = !BoolCast(activeItem.presentation_zoomText))} + checked={BoolCast(activeItem.presentation_zoomText)} + /> </div> <div className="presBox-dropdown" @@ -1706,21 +1845,36 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { e.stopPropagation(); this._openEffectDropdown = !this._openEffectDropdown; })} - style={{ borderBottomLeftRadius: this._openEffectDropdown ? 0 : 5, border: this._openEffectDropdown ? `solid 2px ${Colors.MEDIUM_BLUE}` : 'solid 1px black' }}> + style={{ + color: SettingsManager.userColor, + background: SettingsManager.userVariantColor, + borderBottomLeftRadius: this._openEffectDropdown ? 0 : 5, + border: this._openEffectDropdown ? `solid 2px ${SettingsManager.userVariantColor}` : `solid 1px ${SettingsManager.userColor}`, + }}> {effect?.toString()} <FontAwesomeIcon className="presBox-dropdownIcon" style={{ gridColumn: 2, color: this._openEffectDropdown ? Colors.MEDIUM_BLUE : 'black' }} icon={'angle-down'} /> - <div className={'presBox-dropdownOptions'} id={'presBoxMovementDropdown'} style={{ display: this._openEffectDropdown ? 'grid' : 'none' }} onPointerDown={e => e.stopPropagation()}> - {presEffect(PresEffect.None)} - {presEffect(PresEffect.Fade)} - {presEffect(PresEffect.Flip)} - {presEffect(PresEffect.Rotate)} - {presEffect(PresEffect.Bounce)} - {presEffect(PresEffect.Roll)} + <div + className="presBox-dropdownOptions" + id={'presBoxMovementDropdown'} + style={{ + color: SettingsManager.userColor, + background: SettingsManager.userBackgroundColor, + 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)} </div> </div> <div className="ribbon-doubleButton" style={{ display: effect === PresEffectDirection.None ? 'none' : 'inline-flex' }}> <div className="presBox-subheading">Effect direction</div> - <div className="ribbon-property">{StrCast(this.activeItem.presEffectDirection)}</div> + <div className="ribbon-property" style={{ border: `solid 1px ${SettingsManager.userColor}` }}> + {StrCast(this.activeItem.presentation_effectDirection)} + </div> </div> <div className="effectDirection" style={{ display: effect === PresEffectDirection.None ? 'none' : 'grid', width: 40 }}> {presDirection(PresEffectDirection.Left, 'angle-right', 1, 2, {})} @@ -1742,8 +1896,10 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { @computed get mediaOptionsDropdown() { const activeItem = this.activeItem; if (activeItem && this.targetDoc) { - const clipStart = NumCast(activeItem.clipStart); - const clipEnd = NumCast(activeItem.clipEnd, NumCast(activeItem[Doc.LayoutFieldKey(activeItem) + '_duration'])); + const renderTarget = PresBox.targetRenderedDoc(this.activeItem); + const clipStart = NumCast(renderTarget.clipStart); + const clipEnd = NumCast(renderTarget.clipEnd, clipStart + NumCast(renderTarget[Doc.LayoutFieldKey(renderTarget) + '_duration'])); + const config_clipEnd = NumCast(activeItem.config_clipEnd) < NumCast(activeItem.config_clipStart) ? clipEnd - clipStart : NumCast(activeItem.config_clipEnd); return ( <div className={'presBox-ribbon'} onClick={e => e.stopPropagation()} onPointerUp={e => e.stopPropagation()} onPointerDown={e => e.stopPropagation()}> <div> @@ -1754,17 +1910,15 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { <div className="slider-text" style={{ fontWeight: 500 }}> Start time (s) </div> - <div id={'startTime'} className="slider-number" style={{ backgroundColor: Colors.LIGHT_GRAY }}> + <div id="startTime" className="slider-number" style={{ color: SettingsManager.userColor, backgroundColor: SettingsManager.userBackgroundColor }}> <input className="presBox-input" style={{ textAlign: 'center', width: '100%', height: 15, fontSize: 10 }} type="number" readOnly={true} - value={NumCast(activeItem.presStartTime).toFixed(2)} + value={NumCast(activeItem.config_clipStart).toFixed(2)} onKeyDown={e => e.stopPropagation()} - onChange={action((e: React.ChangeEvent<HTMLInputElement>) => { - activeItem.presStartTime = Number(e.target.value); - })} + onChange={action(e => (activeItem.config_clipStart = Number(e.target.value)))} /> </div> </div> @@ -1772,25 +1926,23 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { <div className="slider-text" style={{ fontWeight: 500 }}> Duration (s) </div> - <div className="slider-number" style={{ backgroundColor: Colors.LIGHT_BLUE }}> - {Math.round((NumCast(activeItem.presEndTime) - NumCast(activeItem.presStartTime)) * 10) / 10} + <div className="slider-number" style={{ color: SettingsManager.userColor, backgroundColor: SettingsManager.userBackgroundColor }}> + {Math.round((config_clipEnd - NumCast(activeItem.config_clipStart)) * 10) / 10} </div> </div> <div className="slider-block"> <div className="slider-text" style={{ fontWeight: 500 }}> End time (s) </div> - <div id={'endTime'} className="slider-number" style={{ backgroundColor: Colors.LIGHT_GRAY }}> + <div id="endTime" className="slider-number" style={{ color: SettingsManager.userColor, backgroundColor: SettingsManager.userBackgroundColor }}> <input className="presBox-input" onKeyDown={e => e.stopPropagation()} style={{ textAlign: 'center', width: '100%', height: 15, fontSize: 10 }} type="number" readOnly={true} - value={NumCast(activeItem.presEndTime).toFixed(2)} - onChange={action((e: React.ChangeEvent<HTMLInputElement>) => { - activeItem.presEndTime = Number(e.target.value); - })} + value={config_clipEnd.toFixed(2)} + onChange={action(e => (activeItem.config_clipEnd = Number(e.target.value)))} /> </div> </div> @@ -1801,16 +1953,15 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { step="0.1" min={clipStart} max={clipEnd} - value={NumCast(activeItem.presEndTime)} - style={{ gridColumn: 1, gridRow: 1 }} + value={config_clipEnd} + style={{ gridColumn: 1, gridRow: 1, background: SettingsManager.userColor, color: SettingsManager.userVariantColor }} className={`toolbar-slider ${'end'}`} id="toolbar-slider" onPointerDown={e => { - this._batch = UndoManager.StartBatch('presEndTime'); + this._batch = UndoManager.StartBatch('config_clipEnd'); const endBlock = document.getElementById('endTime'); if (endBlock) { - endBlock.style.color = Colors.LIGHT_GRAY; - endBlock.style.backgroundColor = Colors.MEDIUM_BLUE; + endBlock.style.backgroundColor = SettingsManager.userVariantColor; } e.stopPropagation(); }} @@ -1818,13 +1969,12 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { this._batch?.end(); const endBlock = document.getElementById('endTime'); if (endBlock) { - endBlock.style.color = Colors.BLACK; - endBlock.style.backgroundColor = Colors.LIGHT_GRAY; + endBlock.style.backgroundColor = SettingsManager.userBackgroundColor; } }} onChange={(e: React.ChangeEvent<HTMLInputElement>) => { e.stopPropagation(); - activeItem.presEndTime = Number(e.target.value); + activeItem.config_clipEnd = Number(e.target.value); }} /> <input @@ -1832,16 +1982,15 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { step="0.1" min={clipStart} max={clipEnd} - value={NumCast(activeItem.presStartTime)} + value={NumCast(activeItem.config_clipStart)} style={{ gridColumn: 1, gridRow: 1 }} className={`toolbar-slider ${'start'}`} id="toolbar-slider" onPointerDown={e => { - this._batch = UndoManager.StartBatch('presStartTime'); + this._batch = UndoManager.StartBatch('config_clipStart'); const startBlock = document.getElementById('startTime'); if (startBlock) { - startBlock.style.color = Colors.LIGHT_GRAY; - startBlock.style.backgroundColor = Colors.MEDIUM_BLUE; + startBlock.style.backgroundColor = SettingsManager.userVariantColor; } e.stopPropagation(); }} @@ -1849,13 +1998,12 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { this._batch?.end(); const startBlock = document.getElementById('startTime'); if (startBlock) { - startBlock.style.color = Colors.BLACK; - startBlock.style.backgroundColor = Colors.LIGHT_GRAY; + startBlock.style.backgroundColor = SettingsManager.userBackgroundColor; } }} onChange={(e: React.ChangeEvent<HTMLInputElement>) => { e.stopPropagation(); - activeItem.presStartTime = Number(e.target.value); + activeItem.config_clipStart = Number(e.target.value); }} /> </div> @@ -1870,22 +2018,46 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { <div className="presBox-subheading">Start playing:</div> <div className="presBox-radioButtons"> <div className="checkbox-container"> - <input className="presBox-checkbox" type="checkbox" onChange={() => (activeItem.mediaStart = 'manual')} checked={activeItem.mediaStart === 'manual'} /> + <input + className="presBox-checkbox" + type="checkbox" + style={{ border: `solid 1px ${SettingsManager.userColor}` }} + onChange={() => (activeItem.presentation_mediaStart = 'manual')} + checked={activeItem.presentation_mediaStart === 'manual'} + /> <div>On click</div> </div> <div className="checkbox-container"> - <input className="presBox-checkbox" type="checkbox" onChange={() => (activeItem.mediaStart = 'auto')} checked={activeItem.mediaStart === 'auto'} /> + <input + className="presBox-checkbox" + style={{ border: `solid 1px ${SettingsManager.userColor}` }} + type="checkbox" + onChange={() => (activeItem.presentation_mediaStart = 'auto')} + checked={activeItem.presentation_mediaStart === 'auto'} + /> <div>Automatically</div> </div> </div> <div className="presBox-subheading">Stop playing:</div> <div className="presBox-radioButtons"> <div className="checkbox-container"> - <input className="presBox-checkbox" type="checkbox" onChange={() => (activeItem.mediaStop = 'manual')} checked={activeItem.mediaStop === 'manual'} /> - <div>At audio end time</div> + <input + className="presBox-checkbox" + type="checkbox" + style={{ border: `solid 1px ${SettingsManager.userColor}` }} + onChange={() => (activeItem.presentation_mediaStop = 'manual')} + checked={activeItem.presentation_mediaStop === 'manual'} + /> + <div>At media end time</div> </div> <div className="checkbox-container"> - <input className="presBox-checkbox" type="checkbox" onChange={() => (activeItem.mediaStop = 'auto')} checked={activeItem.mediaStop === 'auto'} /> + <input + className="presBox-checkbox" + type="checkbox" + style={{ border: `solid 1px ${SettingsManager.userColor}` }} + onChange={() => (activeItem.presentation_mediaStop = 'auto')} + checked={activeItem.presentation_mediaStop === 'auto'} + /> <div>On slide change</div> </div> {/* <div className="checkbox-container"> @@ -1916,7 +2088,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { return ( <div className="presBox-toolbar-dropdown" - style={{ display: this._newDocumentTools && this.layoutDoc.presStatus === 'edit' ? 'inline-flex' : 'none' }} + style={{ display: this._newDocumentTools && this.layoutDoc.presentation_status === 'edit' ? 'inline-flex' : 'none' }} onClick={e => e.stopPropagation()} onPointerUp={e => e.stopPropagation()} onPointerDown={e => e.stopPropagation()}> @@ -2053,10 +2225,10 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { if (doc) { const tabMap = CollectionDockingView.Instance?.tabMap; const tab = tabMap && Array.from(tabMap).find(tab => tab.DashDoc.type === DocumentType.COL)?.DashDoc; - const presCollection = DocumentManager.GetContextPath(this.activeItem).reverse().lastElement().presentationTargetDoc ?? tab; + const presCollection = DocumentManager.GetContextPath(this.activeItem).reverse().lastElement().presentation_targetDoc ?? tab; const data = Cast(presCollection?.data, listSpec(Doc)); - const presData = Cast(this.rootDoc.data, listSpec(Doc)); - if (data && presData) { + const config_data = Cast(this.rootDoc.data, listSpec(Doc)); + if (data && config_data) { data.push(doc); TabDocView.PinDoc(doc, {}); this.gotoDocument(this.childDocs.length, this.activeItem); @@ -2105,7 +2277,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { className="dropdown-play-button" onClick={undoBatch( action(() => { - this.layoutDoc.presStatus = 'manual'; + this.layoutDoc.presentation_status = 'manual'; this.initializePresState(this.itemIndex); this.turnOffEdit(true); this.gotoDocument(this.itemIndex, this.activeItem); @@ -2133,8 +2305,8 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { const propTitle = SettingsManager.propertiesWidth > 0 ? 'Close Presentation Panel' : 'Open Presentation Panel'; const mode = StrCast(this.rootDoc._type_collection) as CollectionViewType; const isMini: boolean = this.toolbarWidth <= 100; - const activeColor = Colors.LIGHT_BLUE; - const inactiveColor = Colors.WHITE; + const activeColor = SettingsManager.userVariantColor; + const inactiveColor = lightOrDark(SettingsManager.userBackgroundColor) === Colors.WHITE ? Colors.WHITE : SettingsManager.userBackgroundColor; return mode === CollectionViewType.Carousel3D || Doc.IsInMyOverlay(this.rootDoc) ? 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)}> @@ -2144,7 +2316,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { <Tooltip title={<div className="dash-tooltip">View paths</div>}> <div style={{ opacity: this.childDocs.length > 1 ? 1 : 0.3, color: this._pathBoolean ? Colors.MEDIUM_BLUE : 'white', width: isMini ? '100%' : undefined }} - className={'toolbar-button'} + className="toolbar-button" onClick={this.childDocs.length > 1 ? () => this.togglePath() : undefined}> <FontAwesomeIcon icon={'exchange-alt'} /> </div> @@ -2181,7 +2353,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { className={`presBox-buttons${Doc.IsInMyOverlay(this.rootDoc) ? ' inOverlay' : ''}`} style={{ background: Doc.ActivePresentation === this.rootDoc ? Colors.LIGHT_BLUE : undefined, display: !this.rootDoc._chromeHidden ? 'none' : undefined }}> {isMini ? null : ( - <select className="presBox-viewPicker" style={{ display: this.layoutDoc.presStatus === 'edit' ? 'block' : 'none' }} onPointerDown={e => e.stopPropagation()} onChange={this.viewChanged} value={mode}> + <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}> List </option> @@ -2196,12 +2368,12 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { </select> )} <div className="presBox-presentPanel" style={{ opacity: this.childDocs.length ? 1 : 0.3 }}> - <span className={`presBox-button ${this.layoutDoc.presStatus === PresStatus.Edit ? 'present' : ''}`}> + <span className={`presBox-button ${this.layoutDoc.presentation_status === PresStatus.Edit ? 'present' : ''}`}> <div className="presBox-button-left" onClick={undoBatch(() => { if (this.childDocs.length) { - this.layoutDoc.presStatus = 'manual'; + this.layoutDoc.presentation_status = 'manual'; this.initializePresState(this.itemIndex); this.gotoDocument(this.itemIndex, this.activeItem); } @@ -2227,12 +2399,15 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { } @computed get playButtons() { - const presEnd = !this.layoutDoc.presLoop && this.itemIndex === this.childDocs.length - 1 && (this.activeItem.presIndexed === undefined || NumCast(this.activeItem.presIndexed) === (this.progressivizedItems(this.activeItem)?.length ?? 0)); + 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: boolean = !this.layoutDoc.presLoop && this.itemIndex === 0; const inOverlay = Doc.IsInMyOverlay(this.rootDoc); // 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.presStatus !== 'edit' ? 'inline-flex' : 'none' }}> + <div className="presPanelOverlay" style={{ display: this.layoutDoc.presentation_status !== 'edit' ? 'inline-flex' : 'none' }}> <Tooltip title={<div className="dash-tooltip">{'Loop'}</div>}> <div className="presPanel-button" @@ -2255,7 +2430,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { this.back(); if (this._presTimer) { clearTimeout(this._presTimer); - this.layoutDoc.presStatus = PresStatus.Manual; + this.layoutDoc.presentation_status = PresStatus.Manual; } e.stopPropagation(); }, @@ -2265,9 +2440,9 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { }> <FontAwesomeIcon icon={'arrow-left'} /> </div> - <Tooltip title={<div className="dash-tooltip">{this.layoutDoc.presStatus === PresStatus.Autoplay ? 'Pause' : 'Autoplay'}</div>}> + <Tooltip title={<div className="dash-tooltip">{this.layoutDoc.presentation_status === PresStatus.Autoplay ? 'Pause' : 'Autoplay'}</div>}> <div className="presPanel-button" onPointerDown={e => setupMoveUpEvents(this, e, returnFalse, emptyFunction, () => this.startOrPause(true), false, false)}> - <FontAwesomeIcon color={this.layoutDoc.presStatus === PresStatus.Autoplay ? 'red' : undefined} icon={this.layoutDoc.presStatus === PresStatus.Autoplay ? 'pause' : 'play'} /> + <FontAwesomeIcon color={this.layoutDoc.presentation_status === PresStatus.Autoplay ? 'red' : undefined} icon={this.layoutDoc.presentation_status === PresStatus.Autoplay ? 'pause' : 'play'} /> </div> </Tooltip> <div @@ -2283,7 +2458,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { this.next(); if (this._presTimer) { clearTimeout(this._presTimer); - this.layoutDoc.presStatus = PresStatus.Manual; + this.layoutDoc.presentation_status = PresStatus.Manual; } e.stopPropagation(); }, @@ -2316,7 +2491,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { </Tooltip> <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?.presIndexed !== undefined ? `(${this.activeItem.presIndexed}/${this.progressivizedItems(this.activeItem)?.length})` : ''} / {this.childDocs.length} + {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 ? ( @@ -2324,7 +2499,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { className="presPanel-button-text" onClick={undoBatch( action(() => { - this.layoutDoc.presStatus = PresStatus.Edit; + this.layoutDoc.presentation_status = PresStatus.Edit; clearTimeout(this._presTimer); }) )}> @@ -2342,7 +2517,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { @action startOrPause = (makeActive = true) => { makeActive && this.updateCurrentPresentation(); - if (!this.layoutDoc.presStatus || this.layoutDoc.presStatus === PresStatus.Manual || this.layoutDoc.presStatus === PresStatus.Edit) this.startPresentation(this.itemIndex); + if (!this.layoutDoc.presentation_status || this.layoutDoc.presentation_status === PresStatus.Manual || this.layoutDoc.presentation_status === PresStatus.Edit) this.startPresentation(this.itemIndex); else this.pauseAutoPres(); }; @@ -2351,7 +2526,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { this.back(); if (this._presTimer) { clearTimeout(this._presTimer); - this.layoutDoc.presStatus = PresStatus.Manual; + this.layoutDoc.presentation_status = PresStatus.Manual; } }; @@ -2360,19 +2535,19 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { this.next(); if (this._presTimer) { clearTimeout(this._presTimer); - this.layoutDoc.presStatus = PresStatus.Manual; + this.layoutDoc.presentation_status = PresStatus.Manual; } }; @undoBatch @action exitClicked = () => { - this.layoutDoc.presStatus = this._exitTrail?.() ?? this.exitMinimize(); + this.layoutDoc.presentation_status = this._exitTrail?.() ?? this.exitMinimize(); clearTimeout(this._presTimer); }; AddToMap = (treeViewDoc: Doc, index: number[]) => { - if (!treeViewDoc.presentationTargetDoc) 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 (!treeViewDoc.presentation_targetDoc) return this.childDocs; // if treeViewDoc is not a pres elements, then it's a sub-bullet of a progressivized slide which isn't added to the linearized list of pres elements since it's not really a pres element. var indexNum = 0; for (let i = 0; i < index.length; i++) { indexNum += index[i] * 10 ** -i; @@ -2388,20 +2563,23 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { }; RemFromMap = (treeViewDoc: Doc, index: number[]) => { - if (!treeViewDoc.presentationTargetDoc) 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 (!treeViewDoc.presentation_targetDoc) return this.childDocs; // if treeViewDoc is not a pres elements, then it's a sub-bullet of a progressivized slide which isn't added to the linearized list of pres elements since it's not really a pres element. if (!this._unmounting && this.isTree) { this._treeViewMap.delete(treeViewDoc); this.dataDoc[this.presFieldKey] = new List<Doc>(this.sort(this._treeViewMap)); } }; - sort = (treeViewMap: Map<Doc, number>) => [...treeViewMap.entries()].sort((a: [Doc, number], b: [Doc, number]) => (a[1] > b[1] ? 1 : a[1] < b[1] ? -1 : 0)).map(kv => kv[0]); + sort = (treeView_Map: Map<Doc, number>) => [...treeView_Map.entries()].sort((a: [Doc, number], b: [Doc, number]) => (a[1] > b[1] ? 1 : a[1] < b[1] ? -1 : 0)).map(kv => kv[0]); 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 presEnd = !this.layoutDoc.presLoop && this.itemIndex === this.childDocs.length - 1 && (this.activeItem.presIndexed === undefined || NumCast(this.activeItem.presIndexed) === (this.progressivizedItems(this.activeItem)?.length ?? 0)); + 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 <div className="miniPres" onClick={e => e.stopPropagation()} onPointerEnter={action(e => (this._forceKeyEvents = true))}> @@ -2420,9 +2598,9 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { <div className="presPanel-button" style={{ opacity: presStart ? 0.4 : 1 }} onPointerDown={e => setupMoveUpEvents(this, e, returnFalse, returnFalse, this.prevClicked, false, false)}> <FontAwesomeIcon icon={'arrow-left'} /> </div> - <Tooltip title={<div className="dash-tooltip">{this.layoutDoc.presStatus === PresStatus.Autoplay ? 'Pause' : 'Autoplay'}</div>}> + <Tooltip title={<div className="dash-tooltip">{this.layoutDoc.presentation_status === PresStatus.Autoplay ? 'Pause' : 'Autoplay'}</div>}> <div className="presPanel-button" onPointerDown={e => setupMoveUpEvents(this, e, returnFalse, returnFalse, () => this.startOrPause(true), false, false)}> - <FontAwesomeIcon icon={this.layoutDoc.presStatus === 'auto' ? 'pause' : 'play'} /> + <FontAwesomeIcon icon={this.layoutDoc.presentation_status === 'auto' ? 'pause' : 'play'} /> </div> </Tooltip> <div className="presPanel-button" style={{ opacity: presEnd ? 0.4 : 1 }} onPointerDown={e => setupMoveUpEvents(this, e, returnFalse, returnFalse, this.nextClicked, false, false)}> @@ -2436,7 +2614,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { </Tooltip> <div className="presPanel-button-text"> Slide {this.itemIndex + 1} - {this.activeItem?.presIndexed !== undefined ? `(${this.activeItem.presIndexed}/${this.progressivizedItems(this.activeItem)?.length})` : ''} / {this.childDocs.length} + {this.activeItem?.presentation_indexed !== undefined ? `(${this.activeItem.presentation_indexed}/${this.progressivizedItems(this.activeItem)?.length})` : ''} / {this.childDocs.length} </div> <div className="presPanel-divider" /> <div className="presPanel-button-text" onPointerDown={e => setupMoveUpEvents(this, e, returnFalse, returnFalse, this.exitClicked, false, false)}> @@ -2460,6 +2638,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { moveDocument={returnFalse} ignoreUnrendered={true} childDragAction="move" + setContentView={emptyFunction} //childLayoutFitWidth={returnTrue} childOpacity={returnOne} childClickScript={PresBox.navigateToDocScript} diff --git a/src/client/views/nodes/trails/PresElementBox.scss b/src/client/views/nodes/trails/PresElementBox.scss index 4f95f0c1f..9ac2b5a94 100644 --- a/src/client/views/nodes/trails/PresElementBox.scss +++ b/src/client/views/nodes/trails/PresElementBox.scss @@ -4,6 +4,10 @@ $light-background: #ececec; $slide-background: #d5dce2; $slide-active: #5b9fdd; +.testingv2 { + background-color: red; +} + .presItem-container { cursor: grab; display: flex; diff --git a/src/client/views/nodes/trails/PresElementBox.tsx b/src/client/views/nodes/trails/PresElementBox.tsx index f31cf6147..82ed9e8d5 100644 --- a/src/client/views/nodes/trails/PresElementBox.tsx +++ b/src/client/views/nodes/trails/PresElementBox.tsx @@ -6,7 +6,7 @@ import { Doc, DocListCast, Opt } from '../../../../fields/Doc'; import { Height, Width } from '../../../../fields/DocSymbols'; import { Id } from '../../../../fields/FieldSymbols'; import { List } from '../../../../fields/List'; -import { Cast, DocCast, NumCast, StrCast } from '../../../../fields/Types'; +import { BoolCast, Cast, DocCast, NumCast, StrCast } from '../../../../fields/Types'; import { emptyFunction, returnEmptyDoclist, returnFalse, returnTrue, setupMoveUpEvents } from '../../../../Utils'; import { Docs } from '../../../documents/Documents'; import { CollectionViewType } from '../../../documents/DocumentTypes'; @@ -25,6 +25,9 @@ import { PresBox } from './PresBox'; import './PresElementBox.scss'; import { PresMovement } from './PresEnums'; import React = require('react'); +import { TreeView } from '../../collections/TreeView'; +import { BranchingTrailManager } from '../../../util/BranchingTrailManager'; +import { MultiToggle, Type } from 'browndash-components'; /** * This class models the view a document added to presentation will have in the presentation. * It involves some functionality for its buttons and options. @@ -58,13 +61,13 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() { return this.props.DocumentView?.().props.docViewPath().lastElement()?.rootDoc; } @computed get targetDoc() { - return Cast(this.rootDoc.presentationTargetDoc, Doc, null) || this.rootDoc; + return Cast(this.rootDoc.presentation_targetDoc, Doc, null) || this.rootDoc; } componentDidMount() { this.layoutDoc.layout_hideLinkButton = true; this._heightDisposer = reaction( - () => ({ expand: this.rootDoc.presExpandInlineButton, height: this.collapsedHeight }), + () => ({ expand: this.rootDoc.presentation_expandInlineButton, height: this.collapsedHeight }), ({ expand, height }) => (this.layoutDoc._height = height + (expand ? this.expandViewHeight : 0)), { fireImmediately: true } ); @@ -79,7 +82,7 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() { ScreenToLocalListTransform = (xCord: number, yCord: number) => [xCord, yCord]; @action - presExpandDocumentClick = () => (this.rootDoc.presExpandInlineButton = !this.rootDoc.presExpandInlineButton); + presExpandDocumentClick = () => (this.rootDoc.presentation_expandInlineButton = !this.rootDoc.presentation_expandInlineButton); embedHeight = (): number => this.collapsedHeight + this.expandViewHeight; // embedWidth = () => this.props.PanelWidth(); @@ -94,7 +97,7 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() { * presentation element. */ @computed get renderEmbeddedInline() { - return !this.rootDoc.presExpandInlineButton || !this.targetDoc ? null : ( + return !this.rootDoc.presentation_expandInlineButton || !this.targetDoc ? null : ( <div className="presItem-embedded" style={{ height: this.embedHeight(), width: '50%' }}> <DocumentView Document={PresBox.targetRenderedDoc(this.rootDoc)} @@ -158,9 +161,9 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() { @computed get transition() { let transitionInS: number; - if (this.rootDoc.presTransition) transitionInS = NumCast(this.rootDoc.presTransition) / 1000; + if (this.rootDoc.presentation_transition) transitionInS = NumCast(this.rootDoc.presentation_transition) / 1000; else transitionInS = 0.5; - return this.rootDoc.presMovement === PresMovement.Jump || this.rootDoc.presMovement === PresMovement.None ? null : 'M: ' + transitionInS + 's'; + return this.rootDoc.presentation_movement === PresMovement.Jump || this.rootDoc.presentation_movement === PresMovement.None ? null : 'M: ' + transitionInS + 's'; } private _itemRef: React.RefObject<HTMLDivElement> = React.createRef(); @@ -238,7 +241,7 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() { onPointerMove = (e: PointerEvent) => { const slide = this._itemRef.current!; - const dragIsPresItem = DragManager.docsBeingDragged.some(d => d.presentationTargetDoc); + const dragIsPresItem = DragManager.docsBeingDragged.some(d => d.presentation_targetDoc); if (slide && dragIsPresItem) { const rect = slide.getBoundingClientRect(); const y = e.clientY - rect.top; //y position within the element. @@ -296,13 +299,24 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() { @action updateCapturedContainerLayout = (presTargetDoc: Doc, activeItem: Doc) => { const targetDoc = DocCast(presTargetDoc.annotationOn) ?? presTargetDoc; - activeItem.presX = NumCast(targetDoc.x); - activeItem.presY = NumCast(targetDoc.y); - activeItem.presRotation = NumCast(targetDoc.rotation); - activeItem.presWidth = NumCast(targetDoc.width); - activeItem.presHeight = NumCast(targetDoc.height); - activeItem.presPinLayout = true; + activeItem.config_x = NumCast(targetDoc.x); + activeItem.config_y = NumCast(targetDoc.y); + activeItem.config_rotation = NumCast(targetDoc.rotation); + activeItem.config_width = NumCast(targetDoc.width); + activeItem.config_height = NumCast(targetDoc.height); + activeItem.config_pinLayout = !activeItem.config_pinLayout; + // 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 * @@ -399,6 +413,18 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() { } }; + @undoBatch + @action + lfg = (e: React.MouseEvent) => { + e.stopPropagation(); + // TODO: fix this bug + const { toggleChildrenRun } = this.rootDoc; + TreeView.ToggleChildrenRun.get(this.rootDoc)?.(); + + // call this.rootDoc.recurChildren() to get all the children + // if (iconClick) PresElementBox.showVideo = false; + }; + @computed get toolbarWidth(): number { const presBoxDocView = DocumentManager.Instance.getDocumentView(this.presBox); @@ -409,19 +435,21 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() { } @computed get presButtons() { - const presBox = this.presBox; //presBox - const presBoxColor: string = StrCast(presBox?._backgroundColor); - const presColorBool: boolean = presBoxColor ? presBoxColor !== Colors.WHITE && presBoxColor !== 'transparent' : false; - const targetDoc: Doc = this.targetDoc; - const activeItem: Doc = this.rootDoc; + const presBox = this.presBox; + 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 items: JSX.Element[] = []; + items.push( <Tooltip key="slide" title={<div className="dash-tooltip">Update captured doc layout</div>}> <div className="slideButton" onPointerDown={e => setupMoveUpEvents(this, e, returnFalse, emptyFunction, () => this.updateCapturedContainerLayout(targetDoc, activeItem), true)} - style={{ opacity: activeItem.presPinLayout ? 1 : 0.5, fontWeight: 700, display: 'flex' }}> + style={{ opacity: activeItem.config_pinLayout ? 1 : 0.5, fontWeight: 700, display: 'flex' }}> L </div> </Tooltip> @@ -431,7 +459,7 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() { <div className="slideButton" onPointerDown={e => setupMoveUpEvents(this, e, returnFalse, emptyFunction, () => this.updateCapturedViewContents(targetDoc, activeItem))} - style={{ opacity: activeItem.presPinData || activeItem.presPinView ? 1 : 0.5, fontWeight: 700, display: 'flex' }}> + style={{ opacity: activeItem.config_pinData || activeItem.config_pinView ? 1 : 0.5, fontWeight: 700, display: 'flex' }}> C </div> </Tooltip> @@ -445,19 +473,29 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() { ); if (this.indexInPres !== 0) { items.push( - <Tooltip key="arrow" title={<div className="dash-tooltip">{activeItem.groupWithUp ? 'Ungroup' : 'Group with up'}</div>}> + <Tooltip + key="arrow" + title={ + <div className="dash-tooltip"> + {!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)'} + </div> + }> <div className="slideButton" - onClick={() => (activeItem.groupWithUp = (NumCast(activeItem.groupWithUp) + 1) % 3)} + onClick={() => (activeItem.presentation_groupWithUp = (NumCast(activeItem.presentation_groupWithUp) + 1) % 3)} style={{ zIndex: 1000 - this.indexInPres, fontWeight: 700, - backgroundColor: activeItem.groupWithUp ? (presColorBool ? presBoxColor : Colors.MEDIUM_BLUE) : undefined, - outline: NumCast(activeItem.groupWithUp) > 1 ? 'solid black 1px' : undefined, - height: activeItem.groupWithUp ? 53 : 18, - transform: activeItem.groupWithUp ? 'translate(0, -17px)' : undefined, + backgroundColor: activeItem.presentation_groupWithUp ? (presColorBool ? presBoxColor : Colors.MEDIUM_BLUE) : undefined, + outline: NumCast(activeItem.presentation_groupWithUp) > 1 ? 'solid black 1px' : undefined, + height: activeItem.presentation_groupWithUp ? 53 : 18, + transform: activeItem.presentation_groupWithUp ? 'translate(0, -17px)' : undefined, }}> - <div style={{ transform: activeItem.groupWithUp ? 'rotate(180deg) translate(0, -17.5px)' : 'rotate(0deg)' }}> + <div style={{ transform: activeItem.presentation_groupWithUp ? 'rotate(180deg) translate(0, -17.5px)' : 'rotate(0deg)' }}> <FontAwesomeIcon icon={'arrow-up'} onPointerDown={e => e.stopPropagation()} /> </div> </div> @@ -465,17 +503,33 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() { ); } items.push( - <Tooltip key="eye" title={<div className="dash-tooltip">{this.rootDoc.presExpandInlineButton ? 'Minimize' : 'Expand'}</div>}> + <Tooltip key="eye" title={<div className="dash-tooltip">{this.rootDoc.presentation_expandInlineButton ? 'Minimize' : 'Expand'}</div>}> <div className="slideButton" onClick={e => { e.stopPropagation(); this.presExpandDocumentClick(); }}> - <FontAwesomeIcon icon={this.rootDoc.presExpandInlineButton ? 'eye-slash' : 'eye'} onPointerDown={e => e.stopPropagation()} /> + <FontAwesomeIcon icon={this.rootDoc.presentation_expandInlineButton ? 'eye-slash' : 'eye'} onPointerDown={e => e.stopPropagation()} /> </div> </Tooltip> ); + if (!Doc.noviceMode && hasChildren) { + // TODO: replace with if treeveiw, has childrenDocs + items.push( + <Tooltip key="children" title={<div className="dash-tooltip">Run child processes (tree only)</div>}> + <div + className="slideButton" + onClick={e => { + e.stopPropagation(); + this.lfg(e); + }} + style={{ fontWeight: 700 }}> + <FontAwesomeIcon icon={'circle-play'} onPointerDown={e => e.stopPropagation()} /> + </div> + </Tooltip> + ); + } items.push( <Tooltip key="trash" title={<div className="dash-tooltip">Remove from presentation</div>}> <div className={'slideButton'} onClick={this.removePresentationItem}> @@ -522,7 +576,7 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() { ) : ( <div ref={this._dragRef} - className={`presItem-slide ${isCurrent ? 'active' : ''}`} + className={`presItem-slide ${isCurrent ? 'active' : ''}${this.rootDoc.runProcess ? ' testingv2' : ''}`} style={{ display: 'infline-block', backgroundColor: this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.BackgroundColor), @@ -534,7 +588,7 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() { style={{ display: 'inline-flex', pointerEvents: isSelected ? undefined : 'none', - width: `calc(100% ${this.rootDoc.presExpandInlineButton ? '- 50%' : ''} - ${this.presButtons.length * 22}px`, + width: `calc(100% ${this.rootDoc.presentation_expandInlineButton ? '- 50%' : ''} - ${this.presButtons.length * 22}px`, cursor: isSelected ? 'text' : 'grab', }}> <div |
