diff options
Diffstat (limited to 'src/client/views/nodes')
39 files changed, 1093 insertions, 661 deletions
diff --git a/src/client/views/nodes/AudioBox.tsx b/src/client/views/nodes/AudioBox.tsx index 7c409c38c..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,7 +68,7 @@ export class AudioBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp _stream: MediaStream | undefined; // passed to MediaRecorder, records device input audio _play: any = null; // timeout for playback - @observable _stackedTimeline: 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; @@ -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; }; @@ -418,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)); @@ -473,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, @@ -525,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 @@ -587,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"> @@ -654,7 +698,7 @@ 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} 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/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 a334e75f1..b09fcd882 100644 --- a/src/client/views/nodes/ComparisonBox.tsx +++ b/src/client/views/nodes/ComparisonBox.tsx @@ -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 a69881b7c..430446c06 100644 --- a/src/client/views/nodes/DataVizBox/DataVizBox.scss +++ b/src/client/views/nodes/DataVizBox/DataVizBox.scss @@ -1,12 +1,15 @@ .dataviz { - overflow: scroll; + overflow: auto; height: 100%; width: 100%; - .datatype-button{ + .datatype-button { display: flex; flex-direction: row; } + .button-container { + pointer-events: unset; + } } .start-message { margin: 10px; diff --git a/src/client/views/nodes/DataVizBox/components/Chart.scss b/src/client/views/nodes/DataVizBox/components/Chart.scss index 50dfe7f05..3072d0907 100644 --- a/src/client/views/nodes/DataVizBox/components/Chart.scss +++ b/src/client/views/nodes/DataVizBox/components/Chart.scss @@ -6,10 +6,10 @@ margin-top: 10px; overflow-y: visible; - .graph{ + .graph { overflow: visible; } - .graph-title{ + .graph-title { align-items: center; font-size: larger; display: flex; @@ -29,7 +29,7 @@ position: relative; margin-bottom: -35px; } - .selected-data{ + .selected-data { align-items: center; text-align: center; display: flex; @@ -44,10 +44,10 @@ stroke-width: 2px; } } - - .histogram-bar{ + + .histogram-bar { outline: thin solid black; - &.hover{ + &.hover { outline: 3px solid black; outline-offset: -3px; } @@ -91,13 +91,22 @@ .tableBox { display: flex; flex-direction: column; + height: calc(100% - 40px); // bcz: hack 40px is the size of the button rows + .table { + height: 100%; + } } -.table-container{ +.table-container { overflow: scroll; margin: 5px; margin-left: 25px; margin-right: 10px; margin-bottom: 0; + tr td { + height: 40px !important; // bcz: hack. you can't set a <tr> height directly, but you can set the height of all of it's <td>s. So this is the height of a tableBox row. + padding: 0 !important; + vertical-align: middle !important; + } } .selectAll-buttons { display: flex; @@ -106,4 +115,4 @@ margin-top: 5px; margin-right: 10px; float: right; -}
\ No newline at end of file +} diff --git a/src/client/views/nodes/DataVizBox/components/TableBox.scss b/src/client/views/nodes/DataVizBox/components/TableBox.scss deleted file mode 100644 index 1264d6a46..000000000 --- a/src/client/views/nodes/DataVizBox/components/TableBox.scss +++ /dev/null @@ -1,22 +0,0 @@ -.table { - margin-top: 10px; - margin-bottom: 10px; - margin-left: 10px; - margin-right: 10px; -} - -.table-row { - display: flex; - flex-direction: row; - justify-content: space-between; - align-items: center; - padding: 5px; - border-bottom: 1px solid #ccc; -} - -.table-container { - display: flex; - flex-direction: column; - justify-content: center; - align-items: center; -}
\ No newline at end of file diff --git a/src/client/views/nodes/DocumentContentsView.tsx b/src/client/views/nodes/DocumentContentsView.tsx index 8ac9d6804..e1de2fa76 100644 --- a/src/client/views/nodes/DocumentContentsView.tsx +++ b/src/client/views/nodes/DocumentContentsView.tsx @@ -41,7 +41,6 @@ import { PhysicsSimulationBox } from './PhysicsBox/PhysicsSimulationBox'; import { RecordingBox } from './RecordingBox'; import { ScreenshotBox } from './ScreenshotBox'; import { ScriptingBox } from './ScriptingBox'; -import { SliderBox } from './SliderBox'; import { PresBox } from './trails/PresBox'; import { VideoBox } from './VideoBox'; import { WebBox } from './WebBox'; @@ -240,7 +239,6 @@ export class DocumentContentsView extends React.Component< FontIconBox, LabelBox, EquationBox, - SliderBox, FieldView, CollectionFreeFormView, CollectionDockingView, 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/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx index 74eee49e0..e4fc6c4a2 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'; @@ -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 @@ -120,6 +121,7 @@ export interface DocComponentView { 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 @@ -392,18 +394,15 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps dragData.treeViewDoc = this.props.treeViewDoc; dragData.removeDocument = this.props.removeDocument; dragData.moveDocument = this.props.moveDocument; + dragData.draggedViews = [this.props.DocumentView()]; dragData.canEmbed = this.rootDoc.dragAction ?? this.props.dragAction ? true : false; - const ffview = this.props.CollectionFreeFormDocumentView?.().props.CollectionFreeFormView; - ffview && runInAction(() => (ffview.ChildDrag = this.props.DocumentView())); DragManager.StartDocumentDrag( selected.map(dv => dv.docView!._mainCont.current!), dragData, x, y, - { hideSource: hideSource || (!dropAction && !this.layoutDoc.onDragStart && !this.props.dontHideOnDrag) }, - () => setTimeout(action(() => ffview && (ffview.ChildDrag = undefined))) + { hideSource: hideSource || (!dropAction && !this.layoutDoc.onDragStart && !this.props.dontHideOnDrag) } ); // this needs to happen after the drop event is processed. - ffview?.setupDragLines(false); } } @@ -493,11 +492,10 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps } const sendToBack = e.altKey; this._singleClickFunc = - clickFunc ?? - (() => - sendToBack - ? this.props.DocumentView().props.CollectionFreeFormDocumentView?.().props.bringToFront(this.rootDoc, true) - : this._componentView?.select?.(e.ctrlKey || e.metaKey, e.shiftKey) ?? this.props.select(e.ctrlKey || e.metaKey || e.shiftKey)); + // prettier-ignore + clickFunc ?? (() => (sendToBack ? this.props.DocumentView().props.bringToFront(this.rootDoc, true) : + this._componentView?.select?.(e.ctrlKey || e.metaKey, e.shiftKey) ?? + this.props.select(e.ctrlKey || e.metaKey || e.shiftKey))); const waitFordblclick = this.props.waitForDoubleClickToClick?.() ?? this.Document.waitForDoubleClickToClick; if ((clickFunc && waitFordblclick !== 'never') || waitFordblclick === 'always') { this._doubleClickTimeout && clearTimeout(this._doubleClickTimeout); @@ -629,7 +627,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); } @@ -700,7 +698,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps } const cm = ContextMenu.Instance; - if (!cm || (e as any)?.nativeEvent?.SchemaHandled) return; + if (!cm || (e as any)?.nativeEvent?.SchemaHandled || DocumentView.ExploreMode) return; if (e && !(e.nativeEvent as any).dash) { const onDisplay = () => { @@ -761,10 +759,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 +796,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) { @@ -965,6 +963,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); }; @@ -1052,7 +1054,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps } } }; - runInAction(() => (dataDoc.audioAnnoState = 'recording')); + //runInAction(() => (dataDoc.audioAnnoState = 'recording')); recorder.start(); const stopFunc = () => { recorder.stop(); @@ -1069,23 +1071,30 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps const audioAnnoState = this.dataDoc.audioAnnoState ?? 'stopped'; const audioAnnos = Cast(this.dataDoc[this.LayoutFieldKey + '_audioAnnotations'], listSpec(AudioField), null); const anno = audioAnnos?.lastElement(); - if (anno instanceof AudioField && 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; + } } }; captionStyleProvider = (doc: Opt<Doc>, props: Opt<DocumentViewProps>, property: string) => this.props?.styleProvider?.(doc, props, property + ':caption'); @computed get innards() { TraceMobx(); - const ffscale = () => this.props.DocumentView().props.CollectionFreeFormDocumentView?.().props.ScreenToLocalTransform().Scale || 1; const showTitle = this.layout_showTitle?.split(':')[0]; const showTitleHover = this.layout_showTitle?.includes(':hover'); const captionView = !this.layout_showCaption ? null : ( @@ -1093,8 +1102,6 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps className="documentView-captionWrapper" style={{ 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')), }}> @@ -1103,7 +1110,6 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps yPadding={10} xPadding={10} fieldKey={this.layout_showCaption} - fontSize={12 * Math.max(1, (2 * ffscale()) / 3)} styleProvider={this.captionStyleProvider} dontRegisterView={true} noSidebar={true} @@ -1114,7 +1120,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, StrCast(Doc.SharingDoc().headingColor, SettingsManager.userVariantColor)); + 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 @@ -1124,7 +1130,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, }}> @@ -1138,14 +1144,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 { diff --git a/src/client/views/nodes/FieldView.tsx b/src/client/views/nodes/FieldView.tsx index 4ebf22ddf..f014f842e 100644 --- a/src/client/views/nodes/FieldView.tsx +++ b/src/client/views/nodes/FieldView.tsx @@ -34,7 +34,6 @@ export interface FieldViewProps extends DocumentViewSharedProps { backgroundColor?: string; treeViewDoc?: Doc; color?: string; - fontSize?: number; height?: number; width?: number; noSidebar?: boolean; diff --git a/src/client/views/nodes/FontIconBox/FontIconBox.tsx b/src/client/views/nodes/FontIconBox/FontIconBox.tsx index 64f289cf7..d2e1293da 100644 --- a/src/client/views/nodes/FontIconBox/FontIconBox.tsx +++ b/src/client/views/nodes/FontIconBox/FontIconBox.tsx @@ -6,8 +6,8 @@ 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 { Utils } from '../../../../Utils'; +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'; @@ -19,7 +19,6 @@ import { SelectedDocView } from '../../selectedDoc'; import { StyleProp } from '../../StyleProvider'; import { OpenWhere } from '../DocumentView'; import { FieldView, FieldViewProps } from '../FieldView'; -import { RichTextMenu } from '../formattedText/RichTextMenu'; import './FontIconBox.scss'; import TrailsIcon from './TrailsIcon'; @@ -176,86 +175,88 @@ 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 color = this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.Color); 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(); - if (selected) { - if (StrCast(selected.type) === DocumentType.COL) { - text = StrCast(selected._type_collection); + 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 { - 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={SettingsManager.userColor} - background={SettingsManager.userVariantColor} - 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={SettingsManager.userColor} background={SettingsManager.userVariantColor} 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.Carousel3D, 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 }); } // 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), // shortcut: '#', })); return ( - <div style={{ color: SettingsManager.userColor, background: SettingsManager.userBackgroundColor }}> - <Dropdown - selectedVal={text} - setSelectedVal={undoable(val => script.script.run({ this: this.layoutDoc, self: this.rootDoc, val }), `dropdown select ${this.label}`)} - color={SettingsManager.userColor} - background={isViewDropdown ? SettingsManager.userVariantColor : SettingsManager.userBackgroundColor} - type={Type.TERT} - dropdownType={DropdownType.SELECT} - items={list} - tooltip={this.label} - fillWidth - /> - </div> + <Dropdown + selectedVal={text} + 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 + /> ); } @@ -343,7 +344,7 @@ export class FontIconBox extends DocComponent<ButtonProps>() { toggleStatus={toggleStatus} text={buttonText} color={color} - background={SettingsManager.userBackgroundColor} + //background={SettingsManager.userBackgroundColor} icon={this.Icon(color)!} label={this.label} onPointerDown={() => script.script.run({ this: this.layoutDoc, self: this.rootDoc, value: !toggleStatus, _readOnly_: false })} diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx index f5c6a9273..1ab120af5 100644 --- a/src/client/views/nodes/ImageBox.tsx +++ b/src/client/views/nodes/ImageBox.tsx @@ -85,8 +85,9 @@ 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, config_panX: NumCast(this.layoutDoc._freeform_panX), @@ -96,8 +97,8 @@ export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp }); 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; @@ -230,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](); @@ -502,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/KeyValuePair.tsx b/src/client/views/nodes/KeyValuePair.tsx index b0d041bdd..f22cb195f 100644 --- a/src/client/views/nodes/KeyValuePair.tsx +++ b/src/client/views/nodes/KeyValuePair.tsx @@ -111,7 +111,7 @@ 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} /> - <Tooltip title={Object.entries(new DocumentOptions()).find((pair: [string, FInfo]) => pair[0].replace(/^_/, '') === props.fieldKey)?.[1].description}> + <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} diff --git a/src/client/views/nodes/LinkDocPreview.tsx b/src/client/views/nodes/LinkDocPreview.tsx index 42c8ea6a4..198cbe851 100644 --- a/src/client/views/nodes/LinkDocPreview.tsx +++ b/src/client/views/nodes/LinkDocPreview.tsx @@ -19,6 +19,7 @@ import { Transform } from '../../util/Transform'; import { DocumentView, DocumentViewSharedProps, OpenWhere } from './DocumentView'; import './LinkDocPreview.scss'; import React = require('react'); +import { DocumentManager } from '../../util/DocumentManager'; interface LinkDocPreviewProps { linkDoc?: Doc; @@ -176,7 +177,12 @@ export class LinkDocPreview extends React.Component<LinkDocPreviewProps> { const webDoc = Array.from(SearchUtil.SearchCollection(Doc.MyFilesystem, this.props.hrefs[0]).keys()).lastElement() ?? Docs.Create.WebDocument(this.props.hrefs[0], { title: this.props.hrefs[0], _nativeWidth: 850, _width: 200, _height: 400, data_useCors: true }); - 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 0b02626e9..bdc074e0c 100644 --- a/src/client/views/nodes/LoadingBox.tsx +++ b/src/client/views/nodes/LoadingBox.tsx @@ -46,7 +46,7 @@ export class LoadingBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { const updateFunc = async () => { 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.tsx b/src/client/views/nodes/MapBox/MapAnchorMenu.tsx index f731763af..7af4d9b59 100644 --- a/src/client/views/nodes/MapBox/MapAnchorMenu.tsx +++ b/src/client/views/nodes/MapBox/MapAnchorMenu.tsx @@ -1,43 +1,32 @@ import React = require('react'); import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { action, computed, IReactionDisposer, observable, ObservableMap, reaction } from 'mobx'; +import { IReactionDisposer, 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 { returnFalse, setupMoveUpEvents, unimplementedFunction } 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 { AntimodeMenu, AntimodeMenuProps } from '../../AntimodeMenu'; // import { GPTPopup, GPTPopupMode } from './../../GPTPopup/GPTPopup'; -import { EditorView } from 'prosemirror-view'; +import { IconButton } from 'browndash-components'; +import { SettingsManager } from '../../../util/SettingsManager'; 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'; @observer export class MapAnchorMenu extends AntimodeMenu<AntimodeMenuProps> { static Instance: MapAnchorMenu; private _disposer: IReactionDisposer | undefined; - private _disposer2: IReactionDisposer | undefined; - private _commentCont = React.createRef<HTMLButtonElement>(); - - - + private _commentRef = React.createRef<HTMLDivElement>(); public onMakeAnchor: () => Opt<Doc> = () => undefined; // Method to get anchor from text search public Center: () => void = unimplementedFunction; - // public OnClick: (e: PointerEvent) => 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 StartDrag: (e: PointerEvent, ele: HTMLElement) => void = unimplementedFunction; public Highlight: (color: string, isTargetToggler: boolean, savedAnnotations?: ObservableMap<number, HTMLDivElement[]>, addAsAnnotation?: boolean) => Opt<Doc> = (color: string, isTargetToggler: boolean) => undefined; public GetAnchor: (savedAnnotations: Opt<ObservableMap<number, HTMLDivElement[]>>, addAsAnnotation: boolean) => Opt<Doc> = (savedAnnotations: Opt<ObservableMap<number, HTMLDivElement[]>>, addAsAnnotation: boolean) => undefined; public Delete: () => void = unimplementedFunction; - public LinkNote: () => void = unimplementedFunction; // public MakeTargetToggle: () => void = unimplementedFunction; // public ShowTargetTrail: () => void = unimplementedFunction; public IsTargetToggler: () => boolean = returnFalse; @@ -54,23 +43,12 @@ export class MapAnchorMenu extends AntimodeMenu<AntimodeMenuProps> { 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); - } + selected => MapAnchorMenu.Instance.fadeOut(true) ); } // audioDown = (e: React.PointerEvent) => { @@ -89,42 +67,54 @@ export class MapAnchorMenu extends AntimodeMenu<AntimodeMenuProps> { // e => this.OnCrop?.(e) // ); // }; - + notePointerDown = (e: React.PointerEvent) => { + setupMoveUpEvents( + this, + e, + (e: PointerEvent) => { + this.StartDrag(e, this._commentRef.current!); + return true; + }, + returnFalse, + e => this.OnClick(e) + ); + }; static top = React.createRef<HTMLDivElement>(); // public get Top(){ // return this.top // } - render() { - const buttons =( - <> - {( - <IconButton - tooltip="Delete Pin" // - onPointerDown={this.Delete} - icon={<FontAwesomeIcon icon="trash-alt" />} - color={StrCast(Doc.UserDoc().userColor)} - /> - )} - {( + const buttons = ( + <> + { + <IconButton + tooltip="Delete Pin" // + onPointerDown={this.Delete} + icon={<FontAwesomeIcon icon="trash-alt" />} + color={SettingsManager.userColor} + /> + } + { + <div ref={this._commentRef}> <IconButton tooltip="Link Note to Pin" // - onPointerDown={this.LinkNote} + onPointerDown={this.notePointerDown} icon={<FontAwesomeIcon icon="sticky-note" />} - color={StrCast(Doc.UserDoc().userColor)} + color={SettingsManager.userColor} /> - )} - {( - <IconButton - tooltip="Center on pin" // - onPointerDown={this.Center} - icon={<FontAwesomeIcon icon="compress-arrows-alt" />} - color={StrCast(Doc.UserDoc().userColor)} - /> - )} - {/* {this.IsTargetToggler !== returnFalse && ( + </div> + } + { + <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} @@ -132,15 +122,16 @@ export class MapAnchorMenu extends AntimodeMenu<AntimodeMenuProps> { toggleStatus={this.IsTargetToggler()} onClick={this.MakeTargetToggle} icon={<FontAwesomeIcon icon="thumbtack" />} - color={StrCast(Doc.UserDoc().userColor)} + color={SettingsManager.userColor} /> )} */} - </> - ); + </> + ); - return this.getElement(<div ref={MapAnchorMenu.top} style={{width:"100%", display:"flex"}}> - {buttons} - </div> + return this.getElement( + <div ref={MapAnchorMenu.top} style={{ width: '100%', display: 'flex' }}> + {buttons} + </div> ); } } diff --git a/src/client/views/nodes/MapBox/MapBox.tsx b/src/client/views/nodes/MapBox/MapBox.tsx index f0106dbbb..65c138975 100644 --- a/src/client/views/nodes/MapBox/MapBox.tsx +++ b/src/client/views/nodes/MapBox/MapBox.tsx @@ -6,9 +6,10 @@ import { observer } from 'mobx-react'; import * as React from 'react'; import { Doc, DocListCast, Field, Opt } from '../../../../fields/Doc'; import { DocCss, Highlight, Width } from '../../../../fields/DocSymbols'; +import { Id } from '../../../../fields/FieldSymbols'; import { InkTool } from '../../../../fields/InkField'; import { DocCast, NumCast, StrCast } from '../../../../fields/Types'; -import { emptyFunction, returnEmptyDoclist, returnEmptyFilter, returnEmptyString, returnFalse, returnOne, returnTrue, setupMoveUpEvents, Utils } from '../../../../Utils'; +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'; @@ -24,6 +25,7 @@ import { MarqueeAnnotator } from '../../MarqueeAnnotator'; import { SidebarAnnos } from '../../SidebarAnnos'; import { DocumentView } from '../DocumentView'; import { FieldView, FieldViewProps } from '../FieldView'; +import { FormattedTextBox } from '../formattedText/FormattedTextBox'; import { PinProps, PresBox } from '../trails'; import { MapAnchorMenu } from './MapAnchorMenu'; import './MapBox.scss'; @@ -70,7 +72,7 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps private _sidebarRef = React.createRef<SidebarAnnos>(); private _ref: React.RefObject<HTMLDivElement> = React.createRef(); private _disposers: { [key: string]: IReactionDisposer } = {}; - private _setPreviewCursor: undefined | ((x: number, y: number, drag: boolean, hide: boolean) => void); + private _setPreviewCursor: undefined | ((x: number, y: number, drag: boolean, hide: boolean, doc: Opt<Doc>) => void); @observable private _marqueeing: number[] | undefined; @observable private _savedAnnotations = new ObservableMap<number, HTMLDivElement[]>(); @@ -106,6 +108,7 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps componentWillUnmount(): void { this._unmounting = true; this.deselectPin(); + this._rerenderTimeout && clearTimeout(this._rerenderTimeout); Object.keys(this._disposers).forEach(key => this._disposers[key]?.()); } @@ -139,13 +142,19 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps return this.addDocument(doc, sidebarKey); // add to sidebar list }; + removeMapDocument = (doc: Doc | Doc[], annotationKey?: string) => { + const docs = doc instanceof Doc ? [doc] : doc; + this.allAnnotations.filter(anno => docs.includes(DocCast(anno.mapPin))).forEach(anno => (anno.mapPin = undefined)); + return this.removeDocument(doc, annotationKey, undefined); + }; + /** * Removing documents from the sidebar * @param doc * @param sidebarKey * @returns */ - sidebarRemoveDocument = (doc: Doc | Doc[], sidebarKey?: string) => this.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 @@ -208,10 +217,41 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps this.layoutDoc._width = this.layoutDoc._layout_showSidebar ? NumCast(this.layoutDoc._width) * 1.2 : Math.max(20, NumCast(this.layoutDoc._width) - prevWidth); }; + startAnchorDrag = (e: PointerEvent, ele: HTMLElement) => { + e.preventDefault(); + e.stopPropagation(); + + const sourceAnchorCreator = action(() => { + const note = this.getAnchor(true); + if (note && this.selectedPin) { + note.latitude = this.selectedPin.latitude; + note.longitude = this.selectedPin.longitude; + note.map = this.selectedPin.map; + } + return note as Doc; + }); + + const targetCreator = (annotationOn: Doc | undefined) => { + const target = DocUtils.GetNewTextDoc('Note linked to ' + this.rootDoc.title, 0, 0, 100, 100, undefined, annotationOn, undefined, 'yellow'); + FormattedTextBox.SelectOnLoad = target[Id]; + return target; + }; + const docView = this.props.DocumentView?.(); + docView && + DragManager.StartAnchorAnnoDrag([ele], new DragManager.AnchorAnnoDragData(docView, sourceAnchorCreator, targetCreator), e.pageX, e.pageY, { + dragComplete: e => { + if (!e.aborted && e.annoDragData && e.annoDragData.linkSourceDoc && e.annoDragData.dropDocument && e.linkDocument) { + e.annoDragData.linkSourceDoc.followLinkToggle = e.annoDragData.dropDocument.annotationOn === this.props.Document; + e.annoDragData.linkSourceDoc.followLinkZoom = false; + } + }, + }); + }; + createNoteAnnotation = () => { const createFunc = undoable( action(() => { - const note = this._sidebarRef.current?.anchorMenuClick(this.getAnchor(false), ['latitude', 'longitude', '-linkedTo']); + 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; @@ -236,7 +276,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) => { @@ -257,7 +297,7 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps }; @action finishMarquee = (x?: number, y?: number) => { this._marqueeing = undefined; - 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) => this.addDocument(doc, annotationKey); @@ -326,7 +366,7 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps NumCast(longitude), false, [], - { map: map } + { title: map ?? `lat=${latitude},lng=${longitude}`, map: map } // ,'pushpinIDamongus'+ this.incrementer++ ); this.addDocument(pushpin, this.annotationKey); @@ -343,7 +383,7 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps // 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', Field.toString(DocCast(this.selectedPin.mapPin)), 'removeAll'); + Doc.setDocFilter(this.rootDoc, '-linkedTo', `mapPin=${Field.toScriptString(DocCast(this.selectedPin))}`, 'remove'); const temp = this.selectedPin; if (!this._unmounting) { @@ -375,13 +415,14 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps // Doc.setDocFilter(this.rootDoc, 'latitude', this.selectedPin.latitude, 'match'); // Doc.setDocFilter(this.rootDoc, 'longitude', this.selectedPin.longitude, 'match'); - Doc.setDocFilter(this.rootDoc, '-linkedTo', Field.toScriptString(this.selectedPin), 'mapPin' as any); + 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; + MapAnchorMenu.Instance.OnClick = this.createNoteAnnotation; + MapAnchorMenu.Instance.StartDrag = this.startAnchorDrag; const point = this._bingMap.current.tryLocationToPixel(new this.MicrosoftMaps.Location(this.selectedPin.latitude, this.selectedPin.longitude)); const x = point.x + (this.props.PanelWidth() - this.sidebarWidth()) / 2; @@ -451,11 +492,12 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps 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) { - anchor.mapPin = existingPin ?? this.selectedPin; if (!addAsAnnotation) anchor.backgroundColor = 'transparent'; - /* addAsAnnotation &&*/ this.addDocument(anchor); + addAsAnnotation && this.addDocument(anchor); PresBox.pinDocView(anchor, { pinDocLayout: pinProps?.pinDocLayout, pinData: { ...(pinProps?.pinData ?? {}), map: true } }, this.rootDoc); return anchor; } @@ -489,7 +531,7 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps * Removes pin from annotations */ @action - removePushpin = (pinDoc: Doc) => this.removeDocument(pinDoc, this.annotationKey); + removePushpin = (pinDoc: Doc) => this.removeMapDocument(pinDoc, this.annotationKey); /* * Removes pushpin from map render @@ -508,7 +550,7 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps // 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', Field.toString(DocCast(this.selectedPin.mapPin)), 'removeAll'); + Doc.setDocFilter(this.rootDoc, '-linkedTo', `mapPin=${Field.toScriptString(DocCast(this.selectedPin))}`, 'remove'); this.removePushpin(this.selectedPin); } @@ -683,20 +725,20 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps searchbarKeyDown = (e: any) => e.key === 'Enter' && this.bingSearch(); static _firstRender = true; - static _rerenderDelay = 0; + 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._firstRender) { - MapBox._firstRender = false; - MapBox._rerenderDelay = 500; - } else if (MapBox._rerenderDelay) { + if (MapBox._rerenderDelay) { // prettier-ignore this._rerenderTimeout = this._rerenderTimeout ?? setTimeout(action(() => { - MapBox._rerenderDelay = 0; + 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; @@ -753,8 +795,9 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps ? null : this.allAnnotations .filter(anno => !anno.layout_unrendered) - .map(pushpin => ( + .map((pushpin, i) => ( <DocumentView + key={i} {...this.props} renderDepth={this.props.renderDepth + 1} Document={pushpin} diff --git a/src/client/views/nodes/MapBox/MapBox2.tsx b/src/client/views/nodes/MapBox/MapBox2.tsx index a54bdcd5e..407a91dd0 100644 --- a/src/client/views/nodes/MapBox/MapBox2.tsx +++ b/src/client/views/nodes/MapBox/MapBox2.tsx @@ -98,7 +98,7 @@ 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.text_inlineAnnotations); } @@ -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/PDFBox.tsx b/src/client/views/nodes/PDFBox.tsx index 758b49655..cf44649a2 100644 --- a/src/client/views/nodes/PDFBox.tsx +++ b/src/client/views/nodes/PDFBox.tsx @@ -24,7 +24,6 @@ import { ContextMenu } from '../ContextMenu'; import { ContextMenuProps } from '../ContextMenuItem'; import { ViewBoxAnnotatableComponent, ViewBoxAnnotatableProps } from '../DocComponent'; import { Colors } from '../global/globalEnums'; -import { LightboxView } from '../LightboxView'; import { CreateImage } from '../nodes/WebBoxRenderer'; import { PDFViewer } from '../pdf/PDFViewer'; import { SidebarAnnos } from '../SidebarAnnos'; @@ -243,14 +242,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.text_html = ele?.innerHTML; - if (addAsAnnotation || annoAnchor) { - this.addDocument(anchor); - } + addAsAnnotation && this.addDocument(anchor); + return anchor; }; @@ -439,12 +437,12 @@ 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...'); const optionItems: ContextMenuProps[] = options && 'subitems' in options ? options.subitems : []; - optionItems.push({ description: 'Toggle Sidebar Type', event: this.toggleSidebarType, icon: 'expand-arrows-alt' }); + !Doc.noviceMode && optionItems.push({ description: 'Toggle Sidebar Type', event: this.toggleSidebarType, icon: 'expand-arrows-alt' }); !Doc.noviceMode && optionItems.push({ description: 'update icon', event: () => this.pdfUrl && this.updateIcon(), icon: 'expand-arrows-alt' }); //optionItems.push({ description: "Toggle Sidebar ", event: () => this.toggleSidebar(), icon: "expand-arrows-alt" }); !options && ContextMenu.Instance.addItem({ description: 'Options...', subitems: optionItems, icon: 'asterisk' }); @@ -555,7 +553,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 8fa2861b6..481e43feb 100644 --- a/src/client/views/nodes/RecordingBox/RecordingBox.tsx +++ b/src/client/views/nodes/RecordingBox/RecordingBox.tsx @@ -1,20 +1,27 @@ 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 { 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 { BoolCast, DocCast } from '../../../../fields/Types'; -import { ScriptingGlobals } from '../../../util/ScriptingGlobals'; -import { DocumentManager } from '../../../util/DocumentManager'; -import { Docs } from '../../../documents/Documents'; @observer export class RecordingBox extends ViewBoxBaseComponent<FieldViewProps>() { @@ -34,9 +41,7 @@ export class RecordingBox extends ViewBoxBaseComponent<FieldViewProps>() { @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) => { @@ -54,6 +59,128 @@ export class RecordingBox extends ViewBoxBaseComponent<FieldViewProps>() { 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); @@ -81,28 +208,52 @@ export class RecordingBox extends ViewBoxBaseComponent<FieldViewProps>() { } static screengrabber: RecordingBox | undefined; } -ScriptingGlobals.add(function toggleRecording(_readOnly_: boolean) { - if (_readOnly_) return RecordingBox.screengrabber ? true : false; - if (RecordingBox.screengrabber) { - RecordingBox.screengrabber.Pause?.(); - setTimeout(() => { - RecordingBox.screengrabber?.Finish?.(); - RecordingBox.screengrabber!.rootDoc.overlayX = 100; - RecordingBox.screengrabber!.rootDoc.overlayY = 100; - RecordingBox.screengrabber = undefined; - }, 100); - } else { - const screengrabber = Docs.Create.WebCamDocument('', { - _width: 384, - _height: 216, - }); - screengrabber.overlayX = -400; - screengrabber.overlayY = 0; - screengrabber[Doc.LayoutFieldKey(screengrabber) + '_trackScreen'] = true; - Doc.AddToMyOverlay(screengrabber); - DocumentManager.Instance.AddViewRenderedCb(screengrabber, docView => { - RecordingBox.screengrabber = docView.ComponentView as RecordingBox; - RecordingBox.screengrabber.Record?.(); - }); + +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; } -}, 'toggle recording'); + return false; +}); diff --git a/src/client/views/nodes/RecordingBox/RecordingView.tsx b/src/client/views/nodes/RecordingBox/RecordingView.tsx index 0e386b093..f7ed82643 100644 --- a/src/client/views/nodes/RecordingBox/RecordingView.tsx +++ b/src/client/views/nodes/RecordingBox/RecordingView.tsx @@ -164,7 +164,6 @@ export function RecordingView(props: IRecordingViewProps) { 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()); diff --git a/src/client/views/nodes/ScreenshotBox.tsx b/src/client/views/nodes/ScreenshotBox.tsx index fa19caae1..ebb8a3374 100644 --- a/src/client/views/nodes/ScreenshotBox.tsx +++ b/src/client/views/nodes/ScreenshotBox.tsx @@ -26,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 @@ -180,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); @@ -219,6 +221,9 @@ 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 })); @@ -233,9 +238,19 @@ export class ScreenshotBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatabl 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) => { + 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 }); this.dataDoc[this.fieldKey + '_duration'] = (new Date().getTime() - this.recordingStart!) / 1000; diff --git a/src/client/views/nodes/SliderBox.scss b/src/client/views/nodes/SliderBox.scss deleted file mode 100644 index 4206a368d..000000000 --- a/src/client/views/nodes/SliderBox.scss +++ /dev/null @@ -1,19 +0,0 @@ -.sliderBox-outerDiv { - width: calc(100% - 14px); // 14px accounts for handles that are at the max value of the slider that would extend outside the box - height: 100%; - border-radius: inherit; - display: flex; - flex-direction: column; - position: relative; - .slider-tracks { - top: 7px; - position: relative; - } - .slider-ticks { - position: relative; - } - .slider-handles { - top: 7px; - position: relative; - } -}
\ No newline at end of file diff --git a/src/client/views/nodes/SliderBox.tsx b/src/client/views/nodes/SliderBox.tsx deleted file mode 100644 index 430b20eb5..000000000 --- a/src/client/views/nodes/SliderBox.tsx +++ /dev/null @@ -1,103 +0,0 @@ -import { runInAction } from 'mobx'; -import { observer } from 'mobx-react'; -import * as React from 'react'; -import { Handles, Rail, Slider, Ticks, Tracks } from 'react-compound-slider'; -import { NumCast, ScriptCast, StrCast } from '../../../fields/Types'; -import { ContextMenu } from '../ContextMenu'; -import { ContextMenuProps } from '../ContextMenuItem'; -import { ViewBoxBaseComponent } from '../DocComponent'; -import { ScriptBox } from '../ScriptBox'; -import { StyleProp } from '../StyleProvider'; -import { FieldView, FieldViewProps } from './FieldView'; -import { Handle, Tick, TooltipRail, Track } from './SliderBox-components'; -import './SliderBox.scss'; - -@observer -export class SliderBox extends ViewBoxBaseComponent<FieldViewProps>() { - public static LayoutString(fieldKey: string) { - return FieldView.LayoutString(SliderBox, fieldKey); - } - - get minThumbKey() { - return this.fieldKey + '-minThumb'; - } - get maxThumbKey() { - return this.fieldKey + '-maxThumb'; - } - get minKey() { - return this.fieldKey + '-min'; - } - get maxKey() { - return this.fieldKey + '-max'; - } - specificContextMenu = (e: React.MouseEvent): void => { - const funcs: ContextMenuProps[] = []; - funcs.push({ description: 'Edit Thumb Change Script', icon: 'edit', event: (obj: any) => ScriptBox.EditButtonScript('On Thumb Change ...', this.props.Document, 'onThumbChange', obj.x, obj.y) }); - ContextMenu.Instance.addItem({ description: 'Options...', subitems: funcs, icon: 'asterisk' }); - }; - onChange = (values: readonly number[]) => - runInAction(() => { - this.dataDoc[this.minThumbKey] = values[0]; - this.dataDoc[this.maxThumbKey] = values[1]; - ScriptCast(this.layoutDoc.onThumbChanged, null)?.script.run({ - self: this.rootDoc, - scriptContext: this.props.scriptContext, - range: values, - this: this.layoutDoc, - }); - }); - - render() { - const domain = [NumCast(this.layoutDoc[this.minKey]), NumCast(this.layoutDoc[this.maxKey])]; - const defaultValues = [NumCast(this.dataDoc[this.minThumbKey]), NumCast(this.dataDoc[this.maxThumbKey])]; - return domain[1] <= domain[0] ? null : ( - <div className="sliderBox-outerDiv" onContextMenu={this.specificContextMenu} onPointerDown={e => e.stopPropagation()} style={{ boxShadow: this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.BoxShadow) }}> - <div - className="sliderBox-mainButton" - onContextMenu={this.specificContextMenu} - style={{ - background: StrCast(this.layoutDoc.backgroundColor), - color: StrCast(this.layoutDoc.color, 'black'), - fontSize: StrCast(this.layoutDoc._text_fontSize), - letterSpacing: StrCast(this.layoutDoc.letterSpacing), - }}> - <Slider mode={2} step={Math.min(1, 0.1 * (domain[1] - domain[0]))} domain={domain} rootStyle={{ position: 'relative', width: '100%' }} onChange={this.onChange} values={defaultValues}> - <Rail>{railProps => <TooltipRail {...railProps} />}</Rail> - <Handles> - {({ handles, activeHandleID, getHandleProps }) => ( - <div className="slider-handles"> - {handles.map((handle, i) => { - const value = i === 0 ? defaultValues[0] : defaultValues[1]; - return ( - <div title={String(value)}> - <Handle key={handle.id} handle={handle} domain={domain} isActive={handle.id === activeHandleID} getHandleProps={getHandleProps} /> - </div> - ); - })} - </div> - )} - </Handles> - <Tracks left={false} right={false}> - {({ tracks, getTrackProps }) => ( - <div className="slider-tracks"> - {tracks.map(({ id, source, target }) => ( - <Track key={id} source={source} target={target} disabled={false} getTrackProps={getTrackProps} /> - ))} - </div> - )} - </Tracks> - <Ticks count={5}> - {({ ticks }) => ( - <div className="slider-ticks"> - {ticks.map(tick => ( - <Tick key={tick.id} tick={tick} count={ticks.length} format={(val: number) => val.toString()} /> - ))} - </div> - )} - </Ticks> - </Slider> - </div> - </div> - ); - } -} 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 48716b867..d7f7c9b73 100644 --- a/src/client/views/nodes/VideoBox.tsx +++ b/src/client/views/nodes/VideoBox.tsx @@ -29,7 +29,7 @@ import { ViewBoxAnnotatableComponent, ViewBoxAnnotatableProps } from '../DocComp import { MarqueeAnnotator } from '../MarqueeAnnotator'; import { AnchorMenu } from '../pdf/AnchorMenu'; import { StyleProp } from '../StyleProvider'; -import { DocumentView, OpenWhere } from './DocumentView'; +import { DocFocusOptions, DocumentView, OpenWhere } from './DocumentView'; import { FieldView, FieldViewProps } from './FieldView'; import { RecordingBox } from './RecordingBox'; import { PinProps, PresBox } from './trails'; @@ -57,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 @@ -124,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; @@ -135,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) { @@ -144,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]?.()); @@ -191,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); @@ -386,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; }; @@ -408,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) { @@ -847,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); } }; @@ -1039,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); @@ -1077,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} @@ -1119,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}> @@ -1128,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.scss b/src/client/views/nodes/WebBox.scss index 75847c100..511c91da0 100644 --- a/src/client/views/nodes/WebBox.scss +++ b/src/client/views/nodes/WebBox.scss @@ -176,7 +176,6 @@ width: 100%; height: 100%; transform-origin: top left; - overflow: auto; .webBox-iframe { width: 100%; diff --git a/src/client/views/nodes/WebBox.tsx b/src/client/views/nodes/WebBox.tsx index febf8341e..af20ff061 100644 --- a/src/client/views/nodes/WebBox.tsx +++ b/src/client/views/nodes/WebBox.tsx @@ -13,7 +13,7 @@ import { listSpec } from '../../../fields/Schema'; import { Cast, ImageCast, NumCast, StrCast, WebCast } from '../../../fields/Types'; import { ImageField, WebField } from '../../../fields/URLField'; import { TraceMobx } from '../../../fields/util'; -import { addStyleSheet, addStyleSheetRule, clearStyleSheetRules, emptyFunction, getWordAtPoint, returnFalse, returnOne, returnZero, setupMoveUpEvents, smoothScroll, Utils } from '../../../Utils'; +import { addStyleSheet, addStyleSheetRule, clearStyleSheetRules, emptyFunction, getWordAtPoint, lightOrDark, returnFalse, returnOne, returnZero, setupMoveUpEvents, smoothScroll, Utils } from '../../../Utils'; import { Docs, DocUtils } from '../../documents/Documents'; import { DocumentManager } from '../../util/DocumentManager'; import { DragManager } from '../../util/DragManager'; @@ -50,7 +50,7 @@ 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 _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(); @@ -62,10 +62,8 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps private _searchRef = React.createRef<HTMLInputElement>(); private _searchString = ''; private _scrollTimer: any; + private _getAnchor: (savedAnnotations: Opt<ObservableMap<number, HTMLDivElement[]>>, addAsAnnotation: boolean) => Opt<Doc> = () => undefined; - private get _getAnchor() { - return AnchorMenu.Instance?.GetAnchor; - } @observable private _webUrl = ''; // url of the src parameter of the embedded iframe but not necessarily the rendered page - eg, when following a link, the rendered page changes but we don't want the src parameter to also change as that would cause an unnecessary re-render. @observable private _hackHide = false; // apparently changing the value of the 'sandbox' prop doesn't necessarily apply it to the active iframe. so thisforces the ifrmae to be rebuilt when allowScripts is toggled @observable private _searching: boolean = false; @@ -186,18 +184,25 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps this.props.setContentView?.(this); // this tells the DocumentView that this WebBox is the "content" of the document. this allows the DocumentView to call WebBox relevant methods to configure the UI (eg, show back/forward buttons) runInAction(() => { - this._annotationKeySuffix = () => this._urlHash + '_annotations'; - const reqdFuncs: { [key: string]: string } = {}; + this._annotationKeySuffix = () => (this._urlHash ? this._urlHash + '_' : '') + 'annotations'; // bcz: need to make sure that doc.data_annotations points to the currently active web page's annotations (this could/should be when the doc is created) - reqdFuncs[this.fieldKey + '_annotations'] = `copyField(this["${this.fieldKey}_"+urlHash(this["${this.fieldKey}"]?.url?.toString())+"_annotations"])`; - reqdFuncs[this.fieldKey + '_annotations-setter'] = `this["${this.fieldKey}_"+urlHash(this["${this.fieldKey}"]?.url?.toString())+"_annotations"] = value`; - reqdFuncs[this.fieldKey + '_sidebar'] = `copyField(this["${this.fieldKey}_"+urlHash(this["${this.fieldKey}"]?.url?.toString())+"_sidebar"])`; - DocUtils.AssignScripts(this.dataDoc, {}, reqdFuncs); + if (this._url) { + const reqdFuncs: { [key: string]: string } = {}; + reqdFuncs[this.fieldKey + '_annotations'] = `copyField(this["${this.fieldKey}_"+urlHash(this["${this.fieldKey}"]?.url?.toString())+"annotations"])`; + reqdFuncs[this.fieldKey + '_annotations-setter'] = `this["${this.fieldKey}_"+urlHash(this["${this.fieldKey}"]?.url?.toString())+"annotations"] = value`; + reqdFuncs[this.fieldKey + '_sidebar'] = `copyField(this["${this.fieldKey}_"+urlHash(this["${this.fieldKey}"]?.url?.toString())+"sidebar"])`; + DocUtils.AssignScripts(this.dataDoc, {}, reqdFuncs); + } }); 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); } ); @@ -259,14 +264,16 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps const clientRects = selRange.getClientRects(); for (let i = 0; i < clientRects.length; i++) { const rect = clientRects.item(i); + const mainrect = this._url ? { translateX: 0, translateY: 0, scale: 1 } : Utils.GetScreenTransform(this._mainCont.current); if (rect && rect.width !== this._mainCont.current.clientWidth) { const annoBox = document.createElement('div'); annoBox.className = 'marqueeAnnotator-annotationBox'; + const scale = this._url ? 1 : this.props.ScreenToLocalTransform().Scale; // transforms the positions from screen onto the pdf div - annoBox.style.top = (rect.top + this._mainCont.current.scrollTop).toString(); - annoBox.style.left = rect.left.toString(); - annoBox.style.width = rect.width.toString(); - annoBox.style.height = rect.height.toString(); + annoBox.style.top = ((rect.top - mainrect.translateY) * scale + (this._url ? this._mainCont.current.scrollTop : NumCast(this.layoutDoc.layout_scrollTop))).toString(); + annoBox.style.left = ((rect.left - mainrect.translateX) * scale).toString(); + annoBox.style.width = (rect.width * scale).toString(); + annoBox.style.height = (rect.height * scale).toString(); this._annotationLayer.current && MarqueeAnnotator.previewNewAnnotation(this._savedAnnotations, this._annotationLayer.current, annoBox, 1); } } @@ -325,8 +332,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), @@ -334,9 +342,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.text_html = ele?.innerHTML; - //addAsAnnotation && - this.addDocumentWrapper(anchor); + anchor.text_html = ele?.innerHTML ?? this._selectionText; + addAsAnnotation && this.addDocumentWrapper(anchor); return anchor; }; @@ -357,21 +364,57 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps this._textAnnotationCreator = () => this.createTextAnnotation(sel, !sel.isCollapsed ? sel.getRangeAt(0) : undefined); AnchorMenu.Instance.jumpTo(e.clientX * scale + mainContBounds.translateX, e.clientY * scale + mainContBounds.translateY - NumCast(this.layoutDoc._layout_scrollTop) * scale); // Changing which document to add the annotation to (the currently selected WebBox) - GPTPopup.Instance.setSidebarId(`${this.props.fieldKey}_${this._urlHash}_sidebar`); + GPTPopup.Instance.setSidebarId(`${this.props.fieldKey}_${this._urlHash ? this._urlHash + '_' : ''}sidebar`); GPTPopup.Instance.addDoc = this.sidebarAddDocument; } } }; @action + webClipDown = (e: React.PointerEvent) => { + const mainContBounds = Utils.GetScreenTransform(this._mainCont.current!); + const scale = (this.props.NativeDimScaling?.() || 1) * mainContBounds.scale; + const word = getWordAtPoint(e.target, e.clientX, e.clientY); + this._setPreviewCursor?.(e.clientX, e.clientY, false, true, this.rootDoc); + MarqueeAnnotator.clearAnnotations(this._savedAnnotations); + e.button !== 2 && (this._marqueeing = [e.clientX, e.clientY]); + if (word || (e.target as any)?.className?.includes('rangeslider') || (e.target as any)?.onclick || (e.target as any)?.parentNode?.onclick) { + e.stopPropagation(); + setTimeout( + action(() => (this._marqueeing = undefined)), + 100 + ); // bcz: hack .. anchor menu is setup within MarqueeAnnotator so we need to at least create the marqueeAnnotator even though we aren't using it. + } else { + this._isAnnotating = true; + this.props.select(false); + e.stopPropagation(); + e.preventDefault(); + } + document.addEventListener('pointerup', this.webClipUp); + }; + webClipUp = (e: PointerEvent) => { + document.removeEventListener('pointerup', this.webClipUp); + this._getAnchor = AnchorMenu.Instance?.GetAnchor; // need to save AnchorMenu's getAnchor since a subsequent selection on another doc will overwrite this value + const sel = window.getSelection(); + if (sel && !sel.isCollapsed) { + const selRange = sel.getRangeAt(0); + this._selectionText = sel.toString(); + AnchorMenu.Instance.setSelectedText(sel.toString()); + this._textAnnotationCreator = () => this.createTextAnnotation(sel, selRange); + AnchorMenu.Instance.jumpTo(e.clientX, e.clientY); + // Changing which document to add the annotation to (the currently selected WebBox) + GPTPopup.Instance.setSidebarId(`${this.props.fieldKey}_${this._urlHash ? this._urlHash + '_' : ''}sidebar`); + GPTPopup.Instance.addDoc = this.sidebarAddDocument; + } + }; + @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 @@ -392,9 +435,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps ContextMenu.Instance.setIgnoreEvents(true); } }; - isFirefox = () => { - return 'InstallTrigger' in window; // navigator.userAgent.indexOf("Chrome") !== -1; - }; + isFirefox = () => 'InstallTrigger' in window; // navigator.userAgent.indexOf("Chrome") !== -1; iframeClick = () => this._iframeClick; iframeScaling = () => 1 / this.props.ScreenToLocalTransform().Scale; @@ -417,6 +458,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps } _iframetimeout: any = undefined; + @observable _warning = 0; @action iframeLoaded = (e: any) => { const iframe = this._iframe; @@ -430,6 +472,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()); @@ -462,12 +505,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( @@ -545,7 +595,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); @@ -610,9 +659,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) { @@ -668,9 +715,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))); @@ -712,14 +759,15 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps } }; @action finishMarquee = (x?: number, y?: number, e?: PointerEvent) => { + this._getAnchor = AnchorMenu.Instance?.GetAnchor; this._marqueeing = undefined; this._isAnnotating = false; this._iframeClick = undefined; - const sel = this._iframe?.contentDocument?.getSelection(); + const sel = this._url ? this._iframe?.contentDocument?.getSelection() : window.document.getSelection(); 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) { @@ -729,6 +777,8 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps } }; + @observable lighttext = false; + @computed get urlContent() { setTimeout( action(() => { @@ -740,20 +790,37 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps ); const field = this.rootDoc[this.props.fieldKey]; if (field instanceof HtmlField) { - return <span className="webBox-htmlSpan" contentEditable onPointerDown={e => e.stopPropagation()} dangerouslySetInnerHTML={{ __html: field.html }} />; + return ( + <span + className="webBox-htmlSpan" + ref={action((r: any) => { + if (r) { + this._scrollHeight = Number(getComputedStyle(r).height.replace('px', '')); + this.lighttext = Array.from(r.children).some((c: any) => c instanceof HTMLElement && lightOrDark(getComputedStyle(c).color) !== Colors.WHITE); + } + })} + contentEditable + onPointerDown={this.webClipDown} + dangerouslySetInnerHTML={{ __html: field.html }} + /> + ); } 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`} /> ); } @@ -761,7 +828,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps } addDocumentWrapper = (doc: Doc | Doc[], annotationKey?: string) => { - (doc instanceof Doc ? [doc] : doc).forEach(doc => (doc.config_data = new WebField(this._url))); + this._url && (doc instanceof Doc ? [doc] : doc).forEach(doc => (doc.config_data = new WebField(this._url))); return this.addDocument(doc, annotationKey); }; @@ -886,6 +953,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps style={{ transform: `scale(${this.zoomScaling()}) translate(${-NumCast(this.layoutDoc.freeform_panX)}px, ${-NumCast(this.layoutDoc.freeform_panY)}px)`, height: Doc.NativeHeight(this.Document) || undefined, + mixBlendMode: this._url || !this.lighttext ? 'multiply' : 'hard-light', }} ref={this._annotationLayer}> {this.inlineTextAnnotations @@ -993,7 +1061,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/formattedText/DashDocView.tsx b/src/client/views/nodes/formattedText/DashDocView.tsx index 48f4c2afd..c5167461b 100644 --- a/src/client/views/nodes/formattedText/DashDocView.tsx +++ b/src/client/views/nodes/formattedText/DashDocView.tsx @@ -8,7 +8,6 @@ import { NumCast, StrCast } from '../../../../fields/Types'; import { emptyFunction, returnFalse, Utils } from '../../../../Utils'; import { DocServer } from '../../../DocServer'; import { Docs, DocUtils } from '../../../documents/Documents'; -import { ColorScheme } from '../../../util/SettingsManager'; import { Transform } from '../../../util/Transform'; import { DocFocusOptions, DocumentView } from '../DocumentView'; import { FormattedTextBox } from './FormattedTextBox'; @@ -22,7 +21,7 @@ export class DashDocView { this.dom = document.createElement('span'); this.dom.style.position = 'relative'; this.dom.style.textIndent = '0'; - this.dom.style.border = '1px solid ' + StrCast(tbox.layoutDoc.color, Doc.ActiveDashboard?.colorScheme === ColorScheme.Dark ? 'dimgray' : 'lightGray'); + this.dom.style.border = '1px solid ' + StrCast(tbox.layoutDoc.color, 'lightGray'); this.dom.style.width = node.attrs.width; this.dom.style.height = node.attrs.height; this.dom.style.display = node.attrs.hidden ? 'none' : 'inline-block'; diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx index bd969b527..90ebf5206 100644 --- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx +++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx @@ -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'; @@ -71,13 +71,14 @@ 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; -export interface FormattedTextBoxProps { - allowScroll?: boolean; -} +export interface FormattedTextBoxProps {} @observer export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps & FormattedTextBoxProps>() { public static LayoutString(fieldStr: string) { @@ -93,6 +94,7 @@ 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>; @@ -153,11 +155,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[`${this.fieldKey}_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); @@ -265,19 +267,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; /** @@ -372,7 +391,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; @@ -439,7 +458,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); } @@ -541,7 +560,6 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps drop = (e: Event, de: DragManager.DropEvent) => { if (de.complete.annoDragData) { de.complete.annoDragData.dropDocCreator = () => this.getAnchor(true); - e.stopPropagation(); return true; } const dragData = de.complete.docDragData; @@ -648,7 +666,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' }); @@ -688,7 +706,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); }; @@ -844,7 +865,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', @@ -945,14 +966,14 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps }; 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(); } } @@ -1242,13 +1263,14 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps if (!this.props.dontRegisterView) { this._disposers.record = reaction( - () => this._recording, + () => this._recordingDictation, () => { this.stopDictation(true); - 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( @@ -1546,8 +1568,8 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps @action componentWillUnmount() { - if (this._recording) { - this._recording = !this._recording; + if (this._recordingDictation) { + this._recordingDictation = !this._recordingDictation; } Object.values(this._disposers).forEach(disposer => disposer?.()); this.endUndoTypingBatch(); @@ -1561,7 +1583,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(); @@ -1585,7 +1607,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps } }); } - if (this._recording && !e.ctrlKey && e.button === 0) { + if (this._recordingDictation && !e.ctrlKey && e.button === 0) { this.breakupDictation(); } this._downX = e.clientX; @@ -1593,10 +1615,8 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps 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); } } @@ -1614,8 +1634,11 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps if (!state.selection.empty && !(state.selection instanceof NodeSelection)) this.setupAnchorMenu(); else if (this.props.isContentActive(true)) { const pcords = editor.posAtCoords({ left: e.clientX, top: e.clientY }); - // !this.props.isSelected(true) && - editor.dispatch(state.tr.setSelection(new TextSelection(state.doc.resolve(pcords?.pos || 0)))); + let xpos = pcords?.pos || 0; + while (xpos > 0 && !state.doc.resolve(xpos).node()?.isTextblock) { + xpos = xpos - 1; + } + editor.dispatch(state.tr.setSelection(new TextSelection(state.doc.resolve(xpos)))); let target = e.target as any; // hrefs are stored on the dataset of the <a> node that wraps the hyerlink <span> while (target && !target.dataset?.targethrefs) target = target.parentElement; FormattedTextBoxComment.update(this, editor, undefined, target?.dataset?.targethrefs, target?.dataset.linkdoc, target?.dataset.nopreview === 'true'); @@ -1758,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 ); @@ -1802,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))) { @@ -1821,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 ' ': @@ -1830,6 +1857,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps } break; } + if (stopPropagation) e.stopPropagation(); this.startUndoTypingBatch(); }; ondrop = (e: React.DragEvent) => { @@ -1887,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 => @@ -1896,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" /> @@ -1948,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} @@ -1970,14 +1999,14 @@ 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> ); } @@ -2025,9 +2054,24 @@ 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(); + if (this.props.isContentActive()) { + // prevent default if selected || child is active but this doc isn't scrollable + if ( + (this._scrollRef.current?.scrollHeight ?? 0) <= Math.ceil(this.props.PanelHeight()) && // + (this.props.isSelected() || this.isAnyChildContentActive()) + ) { + e.preventDefault(); + } e.stopPropagation(); } }; @@ -2084,7 +2128,6 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps 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', }} @@ -2098,10 +2141,10 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps onPointerDown={this.onPointerDown} onDoubleClick={this.onDoubleClick}> <div - className={`formattedTextBox-outer`} + 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} @@ -2119,9 +2162,9 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps /> </div> {this.noSidebar || this.props.dontSelectOnLoad || !this.SidebarShown || this.layout_sidebarWidthPercent === '0%' ? null : this.sidebarCollection} - {this.noSidebar || this.Document._layout_noSidebar || this.props.dontSelectOnLoad || this.Document._createDocOnCR ? 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/RichTextMenu.tsx b/src/client/views/nodes/formattedText/RichTextMenu.tsx index 9c46459b0..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 @@ -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 8bafc2cef..3e2afd2ce 100644 --- a/src/client/views/nodes/formattedText/RichTextRules.ts +++ b/src/client/views/nodes/formattedText/RichTextRules.ts @@ -280,11 +280,8 @@ export class RichTextRules { this.Document[DocData][fieldKey] = value === 'true' ? true : value === 'false' ? false : num ? Number(value) : value; } const target = DocServer.FindDocByTitle(docTitle); - if (target) { - 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); - } - return state.tr; + 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); }), // create a text display of a metadata field on this or another document, or create a hyperlink portal to another document 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/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 f750392f5..383b400c8 100644 --- a/src/client/views/nodes/trails/PresBox.tsx +++ b/src/client/views/nodes/trails/PresBox.tsx @@ -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'; @@ -28,6 +28,7 @@ import { CollectionFreeFormView, computeTimelineLayout, MarqueeViewBounds } from import { CollectionStackedTimeline } from '../../collections/CollectionStackedTimeline'; import { CollectionView } from '../../collections/CollectionView'; import { TabDocView } from '../../collections/TabDocView'; +import { TreeView } from '../../collections/TreeView'; import { ViewBoxBaseComponent } from '../../DocComponent'; import { Colors } from '../../global/globalEnums'; import { LightboxView } from '../../LightboxView'; @@ -280,6 +281,23 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { 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 = () => { @@ -320,6 +338,12 @@ 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 { @@ -365,11 +389,11 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { if (from?.mediaStopTriggerList && this.layoutDoc.presentation_status !== PresStatus.Edit) { DocListCast(from.mediaStopTriggerList).forEach(this.stopTempMedia); } - if (from?.mediaStop === 'auto' && this.layoutDoc.presentation_status !== PresStatus.Edit) { + 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.presentation_status !== 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(); @@ -441,7 +465,11 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { const fkey = Doc.LayoutFieldKey(bestTarget); const setData = bestTargetView?.ComponentView?.setData; if (setData) setData(activeItem.config_data); - else Doc.GetProto(bestTarget)[fkey] = activeItem.config_data instanceof ObjectField ? activeItem.config_data[Copy]() : activeItem.config_data; + else { + const current = Doc.GetProto(bestTarget)[fkey]; + Doc.GetProto(bestTarget)[fkey + '_' + Date.now()] = current instanceof ObjectField ? current[Copy]() : current; + Doc.GetProto(bestTarget)[fkey] = activeItem.config_data instanceof ObjectField ? activeItem.config_data[Copy]() : activeItem.config_data; + } bestTarget[fkey + '_usePath'] = activeItem.config_usePath; setTimeout(() => (bestTarget._dataTransition = undefined), transTime + 10); } @@ -711,6 +739,15 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { 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. @@ -763,8 +800,9 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { easeFunc: StrCast(activeItem.presEaseFunc, 'ease') as any, 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); @@ -1040,8 +1078,6 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { if (doc.type === DocumentType.LABEL) { const audio = Cast(doc.annotationOn, Doc, null); if (audio) { - audio.mediaStart = 'manual'; - audio.mediaStop = 'manual'; 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; @@ -1117,7 +1153,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)); }; @@ -1340,7 +1378,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { @computed get paths() { let pathPoints = ''; this.childDocs.forEach((doc, index) => { - const tagDoc = Cast(doc.presentation_targetDoc, Doc, null); + const tagDoc = PresBox.targetRenderedDoc(doc); if (tagDoc) { const n1x = NumCast(tagDoc.x) + NumCast(tagDoc._width) / 2; const n1y = NumCast(tagDoc.y) + NumCast(tagDoc._height) / 2; @@ -1435,8 +1473,8 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { @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 @@ -1455,6 +1493,10 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { updateEffect = (effect: PresEffect, bullet: boolean, all?: boolean) => (all ? this.childDocs : this.selectedArray).forEach(doc => (bullet ? (doc.presBulletEffect = effect) : (doc.presentation_effect = effect))); static _sliderBatch: any; + static endBatch = () => { + PresBox._sliderBatch.end(); + document.removeEventListener('pointerup', PresBox.endBatch, true); + }; public static inputter = (min: string, step: string, max: string, value: number, active: boolean, change: (val: string) => void, hmargin?: number) => { return ( <input @@ -1464,13 +1506,13 @@ 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'); + document.addEventListener('pointerup', PresBox.endBatch, true); e.stopPropagation(); }} - onPointerUp={() => PresBox._sliderBatch.end()} onChange={e => { e.stopPropagation(); change(e.target.value); @@ -1495,7 +1537,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { }); }; - @computed get visibiltyDurationDropdown() { + @computed get visibilityDurationDropdown() { const activeItem = this.activeItem; if (activeItem && this.targetDoc) { const targetType = this.targetDoc.type; @@ -1504,30 +1546,49 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { return ( <div className="presBox-ribbon"> <div className="ribbon-doubleButton"> - <Tooltip title={<div className="dash-tooltip">{'Hide before presented'}</div>}> - <div className={`ribbon-toggle ${activeItem.presentation_hideBefore ? '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.presentation_hide ? '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.presentation_hideAfter ? '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> @@ -1536,10 +1597,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> @@ -1578,7 +1639,7 @@ 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.presentation_indexed = activeItem.presentation_indexed === undefined ? 0 : undefined; @@ -1601,7 +1662,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { <div className="presBox-subheading">Progressivize First Bullet</div> <input className="presBox-checkbox" - style={{ margin: 10 }} + style={{ margin: 10, border: `solid 1px ${SettingsManager.userColor}` }} type="checkbox" onChange={() => (activeItem.presentation_indexedStart = activeItem.presentation_indexedStart ? 0 : 1)} checked={!NumCast(activeItem.presentation_indexedStart)} @@ -1609,7 +1670,13 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { </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"> @@ -1620,10 +1687,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)} @@ -1654,7 +1729,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { </div> ); const presDirection = (direction: PresEffectDirection, icon: string, gridColumn: number, gridRow: number, opts: object) => { - const color = activeItem.presentation_effectDirection === direction || (direction === PresEffectDirection.Center && !activeItem.presentation_effectDirection) ? 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 @@ -1688,10 +1763,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)} @@ -1701,10 +1789,10 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { </div> <div className="ribbon-doubleButton" style={{ display: activeItem.presentation_movement === PresMovement.Zoom ? 'inline-flex' : 'none' }}> <div className="presBox-subheading">Zoom (% screen filled)</div> - <div className="ribbon-property"> + <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> @@ -1716,10 +1804,10 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { {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> @@ -1739,13 +1827,19 @@ 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 }} + style={{ margin: 10, border: `solid 1px ${SettingsManager.userColor}` }} type="checkbox" onChange={() => (activeItem.presentation_zoomText = !BoolCast(activeItem.presentation_zoomText))} checked={BoolCast(activeItem.presentation_zoomText)} @@ -1757,10 +1851,23 @@ 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()}> + <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)} @@ -1771,7 +1878,9 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { </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.presentation_effectDirection)}</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, {})} @@ -1793,8 +1902,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> @@ -1805,7 +1916,7 @@ 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 }} @@ -1813,9 +1924,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { readOnly={true} value={NumCast(activeItem.config_clipStart).toFixed(2)} onKeyDown={e => e.stopPropagation()} - onChange={action((e: React.ChangeEvent<HTMLInputElement>) => { - activeItem.config_clipStart = Number(e.target.value); - })} + onChange={action(e => (activeItem.config_clipStart = Number(e.target.value)))} /> </div> </div> @@ -1823,25 +1932,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.config_clipEnd) - NumCast(activeItem.config_clipStart)) * 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.config_clipEnd).toFixed(2)} - onChange={action((e: React.ChangeEvent<HTMLInputElement>) => { - activeItem.config_clipEnd = Number(e.target.value); - })} + value={config_clipEnd.toFixed(2)} + onChange={action(e => (activeItem.config_clipEnd = Number(e.target.value)))} /> </div> </div> @@ -1852,16 +1959,15 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { step="0.1" min={clipStart} max={clipEnd} - value={NumCast(activeItem.config_clipEnd)} - 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('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(); }} @@ -1869,8 +1975,7 @@ 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>) => { @@ -1891,8 +1996,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { 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(); }} @@ -1900,8 +2004,7 @@ 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>) => { @@ -1921,22 +2024,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"> @@ -2184,8 +2311,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)}> @@ -2195,7 +2322,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> @@ -2527,7 +2654,6 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { removeDocument={returnFalse} dontRegisterView={true} focus={this.focusElement} - scriptContext={this} ScreenToLocalTransform={this.getTransform} AddToMap={this.AddToMap} RemFromMap={this.RemFromMap} 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 529a5024f..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. @@ -301,8 +304,19 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() { activeItem.config_rotation = NumCast(targetDoc.rotation); activeItem.config_width = NumCast(targetDoc.width); activeItem.config_height = NumCast(targetDoc.height); - activeItem.config_pinLayout = true; + 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,13 +435,15 @@ 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 @@ -486,6 +514,22 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() { </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}> @@ -532,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), |
