diff options
Diffstat (limited to 'src/client/views/nodes')
| -rw-r--r-- | src/client/views/nodes/AudioBox.tsx | 613 | ||||
| -rw-r--r-- | src/client/views/nodes/ColorBox.tsx | 96 | ||||
| -rw-r--r-- | src/client/views/nodes/DocumentView.tsx | 1460 | ||||
| -rw-r--r-- | src/client/views/nodes/FilterBox.tsx | 486 | ||||
| -rw-r--r-- | src/client/views/nodes/ImageBox.tsx | 362 | ||||
| -rw-r--r-- | src/client/views/nodes/LinkAnchorBox.tsx | 167 | ||||
| -rw-r--r-- | src/client/views/nodes/LinkDocPreview.tsx | 3 | ||||
| -rw-r--r-- | src/client/views/nodes/MapBox/MapBox.tsx | 5 | ||||
| -rw-r--r-- | src/client/views/nodes/MapBox/MapBoxInfoWindow.tsx | 111 | ||||
| -rw-r--r-- | src/client/views/nodes/ScreenshotBox.tsx | 489 | ||||
| -rw-r--r-- | src/client/views/nodes/VideoBox.tsx | 958 | ||||
| -rw-r--r-- | src/client/views/nodes/WebBox.tsx | 818 | ||||
| -rw-r--r-- | src/client/views/nodes/button/FontIconBox.tsx | 25 | ||||
| -rw-r--r-- | src/client/views/nodes/formattedText/DashDocView.tsx | 15 | ||||
| -rw-r--r-- | src/client/views/nodes/formattedText/DashFieldView.tsx | 2 | ||||
| -rw-r--r-- | src/client/views/nodes/formattedText/FormattedTextBox.tsx | 11 | ||||
| -rw-r--r-- | src/client/views/nodes/trails/PresBox.tsx | 48 | ||||
| -rw-r--r-- | src/client/views/nodes/trails/PresElementBox.tsx | 440 |
18 files changed, 3413 insertions, 2696 deletions
diff --git a/src/client/views/nodes/AudioBox.tsx b/src/client/views/nodes/AudioBox.tsx index c42c2306a..8437736ae 100644 --- a/src/client/views/nodes/AudioBox.tsx +++ b/src/client/views/nodes/AudioBox.tsx @@ -1,31 +1,29 @@ -import React = require("react"); -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -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 { ComputedField } from "../../../fields/ScriptField"; -import { Cast, DateCast, NumCast } from "../../../fields/Types"; -import { AudioField, nullAudio } from "../../../fields/URLField"; -import { emptyFunction, formatTime, OmitKeys, returnFalse, setupMoveUpEvents } from "../../../Utils"; -import { DocUtils } from "../../documents/Documents"; -import { Networking } from "../../Network"; -import { CurrentUserUtils } from "../../util/CurrentUserUtils"; -import { DragManager } from "../../util/DragManager"; -import { undoBatch } from "../../util/UndoManager"; -import { CollectionStackedTimeline, TrimScope } from "../collections/CollectionStackedTimeline"; -import { ContextMenu } from "../ContextMenu"; -import { ContextMenuProps } from "../ContextMenuItem"; -import { ViewBoxAnnotatableComponent, ViewBoxAnnotatableProps } from "../DocComponent"; -import "./AudioBox.scss"; -import { FieldView, FieldViewProps } from "./FieldView"; - +import React = require('react'); +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +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 { ComputedField } from '../../../fields/ScriptField'; +import { Cast, DateCast, NumCast } from '../../../fields/Types'; +import { AudioField, nullAudio } from '../../../fields/URLField'; +import { emptyFunction, formatTime, OmitKeys, returnFalse, setupMoveUpEvents } from '../../../Utils'; +import { DocUtils } from '../../documents/Documents'; +import { Networking } from '../../Network'; +import { DragManager } from '../../util/DragManager'; +import { undoBatch } from '../../util/UndoManager'; +import { CollectionStackedTimeline, TrimScope } from '../collections/CollectionStackedTimeline'; +import { ContextMenu } from '../ContextMenu'; +import { ContextMenuProps } from '../ContextMenuItem'; +import { ViewBoxAnnotatableComponent, ViewBoxAnnotatableProps } from '../DocComponent'; +import './AudioBox.scss'; +import { FieldView, FieldViewProps } from './FieldView'; /** * AudioBox * Main component: AudioBox.tsx * Supporting Components: CollectionStackedTimeline, AudioWaveform - * + * * AudioBox is a node that supports the recording and playback of audio files in Dash. * When an audio file is importeed into Dash, it is immediately rendered as an AudioBox document. * When a blank AudioBox node is created in Dash, audio recording controls are displayed and the user can start a recording which can be paused or stopped, and can use dictation to create a text transcript. @@ -34,24 +32,23 @@ import { FieldView, FieldViewProps } from "./FieldView"; * User can trim audio: nondestructive, just sets new bounds for playback and rendering timelin */ - // used as a wrapper class for MediaStream from MediaDevices API declare class MediaRecorder { constructor(e: any); // whatever MediaRecorder has } enum media_state { - PendingRecording = "pendingRecording", - Recording = "recording", - Paused = "paused", - Playing = "playing" + PendingRecording = 'pendingRecording', + Recording = 'recording', + Paused = 'paused', + Playing = 'playing', } - @observer export class AudioBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps & FieldViewProps>() { - - public static LayoutString(fieldKey: string) { return FieldView.LayoutString(AudioBox, fieldKey); } + public static LayoutString(fieldKey: string) { + return FieldView.LayoutString(AudioBox, fieldKey); + } public static Enabled = false; static topControlsHeight = 30; // height of upper controls above timeline @@ -73,27 +70,41 @@ export class AudioBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp @observable _muted: boolean = false; @observable _paused: boolean = false; // is recording paused // @observable rawDuration: number = 0; // computed from the length of the audio element when loaded - @computed get recordingStart() { return DateCast(this.dataDoc[this.fieldKey + "-recordingStart"])?.date.getTime(); } - @computed get rawDuration() { return NumCast(this.dataDoc[`${this.fieldKey}-duration`]); } // bcz: shouldn't be needed since it's computed from audio element + @computed get recordingStart() { + return DateCast(this.dataDoc[this.fieldKey + '-recordingStart'])?.date.getTime(); + } + @computed get rawDuration() { + return NumCast(this.dataDoc[`${this.fieldKey}-duration`]); + } // bcz: shouldn't be needed since it's computed from audio element // mehek: not 100% sure but i think due to the order in which things are loaded this is necessary ^^ // if you get rid of it and set the value to 0 the timeline and waveform will set their bounds incorrectly - @computed get miniPlayer() { return this.props.PanelHeight() < 50; } // used to collapse timeline when node is shrunk - @computed get links() { return DocListCast(this.dataDoc.links); } - @computed get mediaState() { return this.dataDoc.mediaState as media_state; } - @computed get path() { // returns the path of the audio file - const path = Cast(this.props.Document[this.fieldKey], AudioField, null)?.url.href || ""; - return path === nullAudio ? "" : path; + @computed get miniPlayer() { + return this.props.PanelHeight() < 50; + } // used to collapse timeline when node is shrunk + @computed get links() { + return DocListCast(this.dataDoc.links); + } + @computed get mediaState() { + return this.dataDoc.mediaState as media_state; + } + @computed get path() { + // returns the path of the audio file + const path = Cast(this.props.Document[this.fieldKey], AudioField, null)?.url.href || ''; + return path === nullAudio ? '' : path; + } + set mediaState(value) { + this.dataDoc.mediaState = value; } - set mediaState(value) { this.dataDoc.mediaState = value; } - - @computed get timeline() { return this._stackedTimeline; } // returns CollectionStackedTimeline ref + @computed get timeline() { + return this._stackedTimeline; + } // returns CollectionStackedTimeline ref componentWillUnmount() { this.removeCurrentlyPlaying(); this._dropDisposer?.(); - Object.values(this._disposers).forEach((disposer) => disposer?.()); + Object.values(this._disposers).forEach(disposer => disposer?.()); this.mediaState === media_state.Recording && this.stopRecording(); } @@ -110,14 +121,10 @@ export class AudioBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp } } - getLinkData(l: Doc) { let la1 = l.anchor1 as Doc; let la2 = l.anchor2 as Doc; - const linkTime = - this.timeline?.anchorStart(la2) || - this.timeline?.anchorStart(la1) || - 0; + const linkTime = this.timeline?.anchorStart(la2) || this.timeline?.anchorStart(la1) || 0; if (Doc.AreProtosEqual(la1, this.dataDoc)) { la1 = l.anchor2 as Doc; la2 = l.anchor1 as Doc; @@ -126,20 +133,17 @@ export class AudioBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp } getAnchor = () => { - return CollectionStackedTimeline.createAnchor( - this.rootDoc, - this.dataDoc, - this.annotationKey, - "_timecodeToShow" /* audioStart */, - "_timecodeToHide" /* audioEnd */, - this._ele?.currentTime || - Cast(this.props.Document._currentTimecode, "number", null) || - (this.mediaState === media_state.Recording - ? (Date.now() - (this.recordingStart || 0)) / 1000 - : undefined) - ) || this.rootDoc; - } - + return ( + CollectionStackedTimeline.createAnchor( + this.rootDoc, + this.dataDoc, + this.annotationKey, + '_timecodeToShow' /* audioStart */, + '_timecodeToHide' /* audioEnd */, + this._ele?.currentTime || Cast(this.props.Document._currentTimecode, 'number', null) || (this.mediaState === media_state.Recording ? (Date.now() - (this.recordingStart || 0)) / 1000 : undefined) + ) || this.rootDoc + ); + }; // updates timecode and shows it in timeline, follows links at time @action @@ -148,24 +152,23 @@ export class AudioBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp this.links .map(l => this.getLinkData(l)) .forEach(({ la1, la2, linkTime }) => { - if (linkTime > NumCast(this.layoutDoc._currentTimecode) && - linkTime < this._ele!.currentTime) { + if (linkTime > NumCast(this.layoutDoc._currentTimecode) && linkTime < this._ele!.currentTime) { Doc.linkFollowHighlight(la1); } }); this.layoutDoc._currentTimecode = this._ele.currentTime; this.timeline?.scrollToTime(NumCast(this.layoutDoc._currentTimecode)); } - } + }; // play back the audio from seekTimeInSeconds, fullPlay tells whether clip is being played to end vs link range @action playFrom = (seekTimeInSeconds: number, endTime?: number, fullPlay: boolean = false) => { clearTimeout(this._play); // abort any previous clip ending - if (Number.isNaN(this._ele?.duration)) { // audio element isn't loaded yet... wait 1/2 second and try again + if (Number.isNaN(this._ele?.duration)) { + // audio element isn't loaded yet... wait 1/2 second and try again setTimeout(() => this.playFrom(seekTimeInSeconds, endTime), 500); - } - else if (this.timeline && this._ele && AudioBox.Enabled) { + } else if (this.timeline && this._ele && AudioBox.Enabled) { // trimBounds override requested playback bounds const end = Math.min(this.timeline.trimEnd, endTime ?? this.timeline.trimEnd); const start = Math.max(this.timeline.trimStart, seekTimeInSeconds); @@ -175,21 +178,18 @@ export class AudioBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp this._ele.play(); this.mediaState = media_state.Playing; this.addCurrentlyPlaying(); - this._play = setTimeout( - () => { - // need to keep track of if end of clip is reached so on next play, clip restarts - if (fullPlay) this._finished = true; - // removes from currently playing if playback has reached end of range marker - else this.removeCurrentlyPlaying(); - this.Pause(); - }, - (end - start) * 1000); + this._play = setTimeout(() => { + // need to keep track of if end of clip is reached so on next play, clip restarts + if (fullPlay) this._finished = true; + // removes from currently playing if playback has reached end of range marker + else this.removeCurrentlyPlaying(); + this.Pause(); + }, (end - start) * 1000); } else { this.Pause(); } } - } - + }; // removes from currently playing display @action @@ -198,7 +198,7 @@ export class AudioBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp const index = CollectionStackedTimeline.CurrentlyPlaying.indexOf(this.layoutDoc); index !== -1 && CollectionStackedTimeline.CurrentlyPlaying.splice(index, 1); } - } + }; // adds doc to currently playing display @action @@ -209,8 +209,7 @@ export class AudioBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp if (CollectionStackedTimeline.CurrentlyPlaying.indexOf(this.layoutDoc) === -1) { CollectionStackedTimeline.CurrentlyPlaying.push(this.layoutDoc); } - } - + }; // update the recording time updateRecordTime = () => { @@ -220,13 +219,13 @@ export class AudioBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp this.layoutDoc._currentTimecode = (new Date().getTime() - this._recordStart - this._pausedTime) / 1000; } } - } + }; // starts recording recordAudioAnnotation = async () => { this._stream = await navigator.mediaDevices.getUserMedia({ audio: true }); this._recorder = new MediaRecorder(this._stream); - this.dataDoc[this.fieldKey + "-recordingStart"] = new DateField(); + this.dataDoc[this.fieldKey + '-recordingStart'] = new DateField(); DocUtils.ActiveRecordings.push(this); this._recorder.ondataavailable = async (e: any) => { const [{ result }] = await Networking.UploadFilesToServer(e.data); @@ -235,11 +234,11 @@ export class AudioBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp } }; this._recordStart = new Date().getTime(); - runInAction(() => this.mediaState = media_state.Recording); + runInAction(() => (this.mediaState = media_state.Recording)); setTimeout(this.updateRecordTime); this._recorder.start(); setTimeout(this.stopRecording, 60 * 60 * 1000); // stop after an hour - } + }; // stops recording @action @@ -249,52 +248,59 @@ export class AudioBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp this._recorder = undefined; const now = new Date().getTime(); this._paused && (this._pausedTime += now - this._pauseStart); - this.dataDoc[this.fieldKey + "-duration"] = (now - this._recordStart - this._pausedTime) / 1000; + this.dataDoc[this.fieldKey + '-duration'] = (now - this._recordStart - this._pausedTime) / 1000; this.mediaState = media_state.Paused; this._stream?.getAudioTracks()[0].stop(); const ind = DocUtils.ActiveRecordings.indexOf(this); ind !== -1 && DocUtils.ActiveRecordings.splice(ind, 1); } - } - + }; // context menu specificContextMenu = (e: React.MouseEvent): void => { const funcs: ContextMenuProps[] = []; funcs.push({ - description: (this.layoutDoc.hideAnchors ? "Don't hide" : "Hide") + " anchors", - event: e => this.layoutDoc.hideAnchors = !this.layoutDoc.hideAnchors, - icon: "expand-arrows-alt", + description: (this.layoutDoc.hideAnchors ? "Don't hide" : 'Hide') + ' anchors', + event: e => (this.layoutDoc.hideAnchors = !this.layoutDoc.hideAnchors), + icon: 'expand-arrows-alt', }); funcs.push({ - description: (this.layoutDoc.dontAutoFollowLinks ? "" : "Don't") + " follow links when encountered", - event: e => this.layoutDoc.dontAutoFollowLinks = !this.layoutDoc.dontAutoFollowLinks, - icon: "expand-arrows-alt", + description: (this.layoutDoc.dontAutoFollowLinks ? '' : "Don't") + ' follow links when encountered', + event: e => (this.layoutDoc.dontAutoFollowLinks = !this.layoutDoc.dontAutoFollowLinks), + icon: 'expand-arrows-alt', }); funcs.push({ - description: (this.layoutDoc.dontAutoPlayFollowedLinks ? "" : "Don't") + " play when link is selected", - event: e => this.layoutDoc.dontAutoPlayFollowedLinks = !this.layoutDoc.dontAutoPlayFollowedLinks, - icon: "expand-arrows-alt", + description: (this.layoutDoc.dontAutoPlayFollowedLinks ? '' : "Don't") + ' play when link is selected', + event: e => (this.layoutDoc.dontAutoPlayFollowedLinks = !this.layoutDoc.dontAutoPlayFollowedLinks), + icon: 'expand-arrows-alt', }); funcs.push({ - description: (this.layoutDoc.autoPlayAnchors ? "Don't auto" : "Auto") + " play anchors onClick", - event: e => this.layoutDoc.autoPlayAnchors = !this.layoutDoc.autoPlayAnchors, - icon: "expand-arrows-alt", + description: (this.layoutDoc.autoPlayAnchors ? "Don't auto" : 'Auto') + ' play anchors onClick', + event: e => (this.layoutDoc.autoPlayAnchors = !this.layoutDoc.autoPlayAnchors), + icon: 'expand-arrows-alt', }); ContextMenu.Instance?.addItem({ - description: "Options...", + description: 'Options...', subitems: funcs, - icon: "asterisk", + icon: 'asterisk', }); - } - + }; // button for starting and stopping the recording Record = (e: React.PointerEvent) => { - e.button === 0 && !e.ctrlKey && setupMoveUpEvents(this, e, returnFalse, returnFalse, action(() => { - this._recorder ? this.stopRecording() : this.recordAudioAnnotation(); - }), false); - } + e.button === 0 && + !e.ctrlKey && + setupMoveUpEvents( + this, + e, + returnFalse, + returnFalse, + action(() => { + this._recorder ? this.stopRecording() : this.recordAudioAnnotation(); + }), + false + ); + }; // for play button Play = (e?: any) => { @@ -314,7 +320,7 @@ export class AudioBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp this.playFrom(start, this.timeline.trimEnd, true); } - } + }; // pause play back @action @@ -327,60 +333,73 @@ export class AudioBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp if (!this._finished) clearTimeout(this._play); this.removeCurrentlyPlaying(); } - } + }; // for dictation button, creates a text document for dictation onFile = (e: any) => { - setupMoveUpEvents(this, e, returnFalse, returnFalse, action(() => { - const newDoc = CurrentUserUtils.GetNewTextDoc( - "", - NumCast(this.rootDoc.x), - NumCast(this.rootDoc.y) + - NumCast(this.layoutDoc._height) + - 10, - NumCast(this.layoutDoc._width), - 2 * NumCast(this.layoutDoc._height) - ); - Doc.GetProto(newDoc).recordingSource = this.dataDoc; - Doc.GetProto(newDoc).recordingStart = ComputedField.MakeFunction(`self.recordingSource["${this.fieldKey}-recordingStart"]`); - Doc.GetProto(newDoc).mediaState = ComputedField.MakeFunction("self.recordingSource.mediaState"); - if (DocListCast(CurrentUserUtils.MyOverlayDocs?.data).includes(this.rootDoc)) { - newDoc.x = this.rootDoc.x; - newDoc.y = NumCast(this.rootDoc.y) + NumCast(this.rootDoc._height); - Doc.AddDocToList(CurrentUserUtils.MyOverlayDocs, undefined, newDoc); - } else { - this.props.addDocument?.(newDoc); - } - }), false); - } - + setupMoveUpEvents( + this, + e, + returnFalse, + returnFalse, + action(() => { + const newDoc = DocUtils.GetNewTextDoc('', NumCast(this.rootDoc.x), NumCast(this.rootDoc.y) + NumCast(this.layoutDoc._height) + 10, NumCast(this.layoutDoc._width), 2 * NumCast(this.layoutDoc._height)); + Doc.GetProto(newDoc).recordingSource = this.dataDoc; + Doc.GetProto(newDoc).recordingStart = ComputedField.MakeFunction(`self.recordingSource["${this.fieldKey}-recordingStart"]`); + Doc.GetProto(newDoc).mediaState = ComputedField.MakeFunction('self.recordingSource.mediaState'); + if (DocListCast(Doc.MyOverlayDocs?.data).includes(this.rootDoc)) { + newDoc.x = this.rootDoc.x; + newDoc.y = NumCast(this.rootDoc.y) + NumCast(this.rootDoc._height); + Doc.AddDocToList(Doc.MyOverlayDocs, undefined, newDoc); + } else { + this.props.addDocument?.(newDoc); + } + }), + false + ); + }; // sets <audio> ref for updating time setRef = (e: HTMLAudioElement | null) => { - e?.addEventListener("timeupdate", this.timecodeChanged); - e?.addEventListener("ended", () => { this._finished = true; this.Pause(); }); + e?.addEventListener('timeupdate', this.timecodeChanged); + e?.addEventListener('ended', () => { + this._finished = true; + this.Pause(); + }); this._ele = e; - } - + }; // pause the time during recording phase recordPause = (e: React.PointerEvent) => { - setupMoveUpEvents(this, e, returnFalse, returnFalse, action(() => { - this._pauseStart = new Date().getTime(); - this._paused = true; - this._recorder.pause(); - }), false); - } + setupMoveUpEvents( + this, + e, + returnFalse, + returnFalse, + action(() => { + this._pauseStart = new Date().getTime(); + this._paused = true; + this._recorder.pause(); + }), + false + ); + }; // continue the recording recordPlay = (e: React.PointerEvent) => { - setupMoveUpEvents(this, e, returnFalse, returnFalse, action(() => { - this._paused = false; - this._pausedTime += new Date().getTime() - this._pauseStart; - this._recorder.resume(); - }), false); - } - + setupMoveUpEvents( + this, + e, + returnFalse, + returnFalse, + action(() => { + this._paused = false; + this._pausedTime += new Date().getTime() - this._pauseStart; + this._recorder.resume(); + }), + false + ); + }; // plays link playLink = (link: Doc) => { @@ -392,8 +411,8 @@ export class AudioBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp } } else { this.links - .filter((l) => l.anchor1 === link || l.anchor2 === link) - .forEach((l) => { + .filter(l => l.anchor1 === link || l.anchor2 === link) + .forEach(l => { const { la1, la2 } = this.getLinkData(l); const startTime = this.timeline?.anchorStart(la1) || this.timeline?.anchorStart(la2); const endTime = this.timeline?.anchorEnd(la1) || this.timeline?.anchorEnd(la2); @@ -406,17 +425,14 @@ export class AudioBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp } }); } - } - + }; @action - timelineWhenChildContentsActiveChanged = (isActive: boolean) => - this.props.whenChildContentsActiveChanged(this._isAnyChildContentActive = isActive) + timelineWhenChildContentsActiveChanged = (isActive: boolean) => this.props.whenChildContentsActiveChanged((this._isAnyChildContentActive = isActive)); - timelineScreenToLocal = () => - this.props.ScreenToLocalTransform().translate(0, -AudioBox.bottomControlsHeight) + timelineScreenToLocal = () => this.props.ScreenToLocalTransform().translate(0, -AudioBox.bottomControlsHeight); - setPlayheadTime = (time: number) => this._ele!.currentTime = this.layoutDoc._currentTimecode = time; + setPlayheadTime = (time: number) => (this._ele!.currentTime = this.layoutDoc._currentTimecode = time); playing = () => this.mediaState === media_state.Playing; @@ -424,7 +440,7 @@ export class AudioBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp // timeline dimensions timelineWidth = () => this.props.PanelWidth(); - timelineHeight = () => (this.props.PanelHeight() - (AudioBox.topControlsHeight + AudioBox.bottomControlsHeight)); + timelineHeight = () => this.props.PanelHeight() - (AudioBox.topControlsHeight + AudioBox.bottomControlsHeight); // ends trim, hides trim controls and displays new clip @undoBatch @@ -432,32 +448,38 @@ export class AudioBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp this.Pause(); this.setPlayheadTime(Math.max(Math.min(this.timeline?.trimEnd || 0, this._ele!.currentTime), this.timeline?.trimStart || 0)); this.timeline?.StopTrimming(); - } + }; // displays trim controls to start trimming clip startTrim = (scope: TrimScope) => { this.Pause(); this.timeline?.StartTrimming(scope); - } + }; // for trim button, double click displays full clip, single displays curr trim bounds onClipPointerDown = (e: React.PointerEvent) => { e.stopPropagation(); - this.timeline && setupMoveUpEvents(this, e, returnFalse, returnFalse, action((e: PointerEvent, doubleTap?: boolean) => { - if (doubleTap) { - this.startTrim(TrimScope.All); - } else if (this.timeline) { - this.Pause(); - this.timeline.IsTrimming !== TrimScope.None ? this.finishTrim() : this.startTrim(TrimScope.Clip); - } - })); - } - + this.timeline && + setupMoveUpEvents( + this, + e, + returnFalse, + returnFalse, + action((e: PointerEvent, doubleTap?: boolean) => { + if (doubleTap) { + this.startTrim(TrimScope.All); + } else if (this.timeline) { + this.Pause(); + this.timeline.IsTrimming !== TrimScope.None ? this.finishTrim() : this.startTrim(TrimScope.Clip); + } + }) + ); + }; // for zoom slider, sets timeline waveform zoom zoom = (zoom: number) => { this.timeline?.setZoom(zoom); - } + }; // for volume slider sets volume @action @@ -469,7 +491,7 @@ export class AudioBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp this.toggleMute(); } } - } + }; // toggles audio muted @action @@ -478,135 +500,156 @@ export class AudioBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp this._muted = !this._muted; this._ele.muted = this._muted; } - } - + }; setupTimelineDrop = (r: HTMLDivElement | null) => { if (r && this.timeline) { this._dropDisposer?.(); - this._dropDisposer = DragManager.MakeDropTarget(r, + this._dropDisposer = DragManager.MakeDropTarget( + 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); }, - this.layoutDoc, undefined); + this.layoutDoc, + undefined + ); } - } - + }; // UI for recording, initially displayed when new audio created in Dash @computed get recordingControls() { - return <div className="audiobox-recorder"> - <div className="audiobox-dictation" onPointerDown={this.onFile}> - <FontAwesomeIcon - size="2x" - icon="file-alt" /> - </div> - {[media_state.Recording, media_state.Playing].includes(this.mediaState) ? - <div className="recording-controls" onClick={e => e.stopPropagation()}> - <div className="record-button" onPointerDown={this.Record}> - <FontAwesomeIcon - size="2x" - icon="stop" /> - </div> - <div className="record-button" onPointerDown={this._paused ? this.recordPlay : this.recordPause}> - <FontAwesomeIcon - size="2x" - icon={this._paused ? "play" : "pause"} /> + return ( + <div className="audiobox-recorder"> + <div className="audiobox-dictation" onPointerDown={this.onFile}> + <FontAwesomeIcon size="2x" icon="file-alt" /> + </div> + {[media_state.Recording, media_state.Playing].includes(this.mediaState) ? ( + <div className="recording-controls" onClick={e => e.stopPropagation()}> + <div className="record-button" onPointerDown={this.Record}> + <FontAwesomeIcon size="2x" icon="stop" /> + </div> + <div className="record-button" onPointerDown={this._paused ? this.recordPlay : this.recordPause}> + <FontAwesomeIcon size="2x" icon={this._paused ? 'play' : 'pause'} /> + </div> + <div className="record-timecode">{formatTime(Math.round(NumCast(this.layoutDoc._currentTimecode)))}</div> </div> - <div className="record-timecode"> - {formatTime(Math.round(NumCast(this.layoutDoc._currentTimecode)))} + ) : ( + <div className="audiobox-start-record" onPointerDown={this.Record}> + <FontAwesomeIcon icon="microphone" /> + RECORD </div> - </div> - : - <div className="audiobox-start-record" onPointerDown={this.Record}> - <FontAwesomeIcon icon="microphone" /> - RECORD - </div>} - </div>; + )} + </div> + ); } // UI for playback, displayed for imported or recorded clips, hides timeline and collapses controls when node is shrunk vertically @computed get playbackControls() { - return <div className="audiobox-file" style={{ - pointerEvents: this._isAnyChildContentActive || this.props.isContentActive() ? "all" : "none", - flexDirection: this.miniPlayer ? "row" : "column", - justifyContent: this.miniPlayer ? "flex-start" : "space-between" - }}> - <div className="audiobox-controls"> - <div className="controls-left"> - <div className="audiobox-button" - title={this.mediaState === media_state.Paused ? "play" : "pause"} - onPointerDown={this.mediaState === media_state.Paused ? this.Play : (e) => { e.stopPropagation(); this.Pause(); }}> - <FontAwesomeIcon icon={this.mediaState === media_state.Paused ? "play" : "pause"} size={"1x"} /> + return ( + <div + className="audiobox-file" + style={{ + pointerEvents: this._isAnyChildContentActive || this.props.isContentActive() ? 'all' : 'none', + flexDirection: this.miniPlayer ? 'row' : 'column', + justifyContent: this.miniPlayer ? 'flex-start' : 'space-between', + }}> + <div className="audiobox-controls"> + <div className="controls-left"> + <div + className="audiobox-button" + title={this.mediaState === media_state.Paused ? 'play' : 'pause'} + onPointerDown={ + this.mediaState === media_state.Paused + ? this.Play + : e => { + e.stopPropagation(); + this.Pause(); + } + }> + <FontAwesomeIcon icon={this.mediaState === media_state.Paused ? 'play' : 'pause'} size={'1x'} /> + </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> + )} </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>} - </div> - <div className="controls-right"> - <div className="audiobox-button" - title={this._muted ? "unmute" : "mute"} - onPointerDown={(e) => { e.stopPropagation(); this.toggleMute(); }}> - <FontAwesomeIcon icon={this._muted ? "volume-mute" : "volume-up"} /> + <div className="controls-right"> + <div + className="audiobox-button" + title={this._muted ? 'unmute' : 'mute'} + onPointerDown={e => { + e.stopPropagation(); + this.toggleMute(); + }}> + <FontAwesomeIcon icon={this._muted ? 'volume-mute' : 'volume-up'} /> + </div> + <input + type="range" + step="0.1" + min="0" + max="1" + value={this._muted ? 0 : this._volume} + className="toolbar-slider volume" + onPointerDown={(e: React.PointerEvent) => { + e.stopPropagation(); + }} + onChange={(e: React.ChangeEvent<HTMLInputElement>) => this.setVolume(Number(e.target.value))} + /> </div> - <input type="range" step="0.1" min="0" max="1" value={this._muted ? 0 : this._volume} - className="toolbar-slider volume" - onPointerDown={(e: React.PointerEvent) => { e.stopPropagation(); }} - onChange={(e: React.ChangeEvent<HTMLInputElement>) => this.setVolume(Number(e.target.value))} - /> - </div> - </div> - - <div className="audiobox-playback" style={{ width: this.miniPlayer ? 0 : "100%" }}> - <div className="audiobox-timeline"> - {this.renderTimeline} </div> - </div> - - {this.audio} - <div className="audiobox-timecodes"> - <div className="timecode-current"> - {this.timeline && formatTime(Math.round(NumCast(this.layoutDoc._currentTimecode) - NumCast(this.timeline.clipStart)))} + <div className="audiobox-playback" style={{ width: this.miniPlayer ? 0 : '100%' }}> + <div className="audiobox-timeline">{this.renderTimeline}</div> </div> - {this.miniPlayer ? - <div>/</div> - : - <div className="bottom-controls-middle"> - <FontAwesomeIcon icon="search-plus" /> - <input type="range" step="0.1" min="1" max="5" value={this.timeline?._zoomFactor} - className="toolbar-slider" id="zoom-slider" - onPointerDown={(e: React.PointerEvent) => { e.stopPropagation(); }} - onChange={(e: React.ChangeEvent<HTMLInputElement>) => { this.zoom(Number(e.target.value)); }} - /> - </div>} - <div className="timecode-duration"> - {this.timeline && formatTime(Math.round(this.timeline.clipDuration))} + {this.audio} + + <div className="audiobox-timecodes"> + <div className="timecode-current">{this.timeline && formatTime(Math.round(NumCast(this.layoutDoc._currentTimecode) - NumCast(this.timeline.clipStart)))}</div> + {this.miniPlayer ? ( + <div>/</div> + ) : ( + <div className="bottom-controls-middle"> + <FontAwesomeIcon icon="search-plus" /> + <input + type="range" + step="0.1" + min="1" + max="5" + value={this.timeline?._zoomFactor} + className="toolbar-slider" + id="zoom-slider" + onPointerDown={(e: React.PointerEvent) => { + e.stopPropagation(); + }} + onChange={(e: React.ChangeEvent<HTMLInputElement>) => { + this.zoom(Number(e.target.value)); + }} + /> + </div> + )} + + <div className="timecode-duration">{this.timeline && formatTime(Math.round(this.timeline.clipDuration))}</div> </div> </div> - - - </div>; + ); } // gets CollectionStackedTimeline @computed get renderTimeline() { return ( <CollectionStackedTimeline - ref={action((r: any) => this._stackedTimeline = r)} - {...OmitKeys(this.props, ["CollectionFreeFormDocumentView"]).omit} + ref={action((r: any) => (this._stackedTimeline = r))} + {...OmitKeys(this.props, ['CollectionFreeFormDocumentView']).omit} fieldKey={this.annotationKey} - dictationKey={this.fieldKey + "-dictation"} + dictationKey={this.fieldKey + '-dictation'} mediaPath={this.path} renderDepth={this.props.renderDepth + 1} - startTag={"_timecodeToShow" /* audioStart */} - endTag={"_timecodeToHide" /* audioEnd */} + startTag={'_timecodeToShow' /* audioStart */} + endTag={'_timecodeToHide' /* audioEnd */} bringToFront={emptyFunction} CollectionView={undefined} playFrom={this.playFrom} @@ -631,26 +674,22 @@ export class AudioBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp // returns the html audio element @computed get audio() { - return <audio ref={this.setRef} - className={`audiobox-control${this.props.isContentActive() ? "-interactive" : ""}`} - onLoadedData={action(e => - (this._ele?.duration && this._ele?.duration !== Infinity) && - (this.dataDoc[this.fieldKey + "-duration"] = this._ele.duration) - )} - > - <source src={this.path} type="audio/mpeg" /> - Not supported. - </audio>; + return ( + <audio + ref={this.setRef} + className={`audiobox-control${this.props.isContentActive() ? '-interactive' : ''}`} + onLoadedData={action(e => this._ele?.duration && this._ele?.duration !== Infinity && (this.dataDoc[this.fieldKey + '-duration'] = this._ele.duration))}> + <source src={this.path} type="audio/mpeg" /> + Not supported. + </audio> + ); } render() { - return <div - ref={this.setupTimelineDrop} - className="audiobox-container" - onContextMenu={this.specificContextMenu} - style={{ pointerEvents: this.layoutDoc._lockedPosition ? "none" : undefined }} - > - {!this.path ? this.recordingControls : this.playbackControls} - </div>; + return ( + <div ref={this.setupTimelineDrop} className="audiobox-container" onContextMenu={this.specificContextMenu} style={{ pointerEvents: this.layoutDoc._lockedPosition ? 'none' : undefined }}> + {!this.path ? this.recordingControls : this.playbackControls} + </div> + ); } } diff --git a/src/client/views/nodes/ColorBox.tsx b/src/client/views/nodes/ColorBox.tsx index 6d05fca5e..c229a966a 100644 --- a/src/client/views/nodes/ColorBox.tsx +++ b/src/client/views/nodes/ColorBox.tsx @@ -1,23 +1,24 @@ -import React = require("react"); -import { action } from "mobx"; -import { observer } from "mobx-react"; +import React = require('react'); +import { action } from 'mobx'; +import { observer } from 'mobx-react'; import { ColorState, SketchPicker } from 'react-color'; import { Doc, HeightSym, WidthSym } from '../../../fields/Doc'; -import { InkTool } from "../../../fields/InkField"; -import { StrCast } from "../../../fields/Types"; -import { DocumentType } from "../../documents/DocumentTypes"; -import { CurrentUserUtils } from "../../util/CurrentUserUtils"; -import { SelectionManager } from "../../util/SelectionManager"; -import { undoBatch } from "../../util/UndoManager"; -import { ViewBoxBaseComponent } from "../DocComponent"; -import { ActiveInkColor, ActiveInkWidth, SetActiveInkColor, SetActiveInkWidth } from "../InkingStroke"; -import "./ColorBox.scss"; +import { InkTool } from '../../../fields/InkField'; +import { StrCast } from '../../../fields/Types'; +import { DocumentType } from '../../documents/DocumentTypes'; +import { SelectionManager } from '../../util/SelectionManager'; +import { undoBatch } from '../../util/UndoManager'; +import { ViewBoxBaseComponent } from '../DocComponent'; +import { ActiveInkColor, ActiveInkWidth, SetActiveInkColor, SetActiveInkWidth } from '../InkingStroke'; +import './ColorBox.scss'; import { FieldView, FieldViewProps } from './FieldView'; -import { RichTextMenu } from "./formattedText/RichTextMenu"; +import { RichTextMenu } from './formattedText/RichTextMenu'; @observer export class ColorBox extends ViewBoxBaseComponent<FieldViewProps>() { - public static LayoutString(fieldKey: string) { return FieldView.LayoutString(ColorBox, fieldKey); } + public static LayoutString(fieldKey: string) { + return FieldView.LayoutString(ColorBox, fieldKey); + } @undoBatch @action @@ -25,17 +26,22 @@ export class ColorBox extends ViewBoxBaseComponent<FieldViewProps>() { SetActiveInkColor(color.hex); SelectionManager.Views().map(view => { - const targetDoc = view.props.Document.dragFactory instanceof Doc ? view.props.Document.dragFactory : - view.props.Document.layout instanceof Doc ? view.props.Document.layout : - view.props.Document.isTemplateForField ? view.props.Document : Doc.GetProto(view.props.Document); + const targetDoc = + view.props.Document.dragFactory instanceof Doc + ? view.props.Document.dragFactory + : view.props.Document.layout instanceof Doc + ? view.props.Document.layout + : view.props.Document.isTemplateForField + ? view.props.Document + : Doc.GetProto(view.props.Document); if (targetDoc) { - if (view.props.LayoutTemplate?.() || view.props.LayoutTemplateString) { // this situation typically occurs when you have a link dot - targetDoc.backgroundColor = color.hex; // bcz: don't know how to change the color of an inline template... - } - else if (RichTextMenu.Instance?.TextViewFieldKey && window.getSelection()?.toString() !== "") { - Doc.Layout(view.props.Document)[RichTextMenu.Instance.TextViewFieldKey + "-color"] = color.hex; + if (view.props.LayoutTemplate?.() || view.props.LayoutTemplateString) { + // this situation typically occurs when you have a link dot + targetDoc.backgroundColor = color.hex; // bcz: don't know how to change the color of an inline template... + } else if (RichTextMenu.Instance?.TextViewFieldKey && window.getSelection()?.toString() !== '') { + Doc.Layout(view.props.Document)[RichTextMenu.Instance.TextViewFieldKey + '-color'] = color.hex; } else { - Doc.Layout(view.props.Document)._backgroundColor = color.hex + (color.rgb.a ? Math.round(color.rgb.a * 255).toString(16) : ""); // '_backgroundColor' is template specific. 'backgroundColor' would apply to all templates, but has no UI at the moment + Doc.Layout(view.props.Document)._backgroundColor = color.hex + (color.rgb.a ? Math.round(color.rgb.a * 255).toString(16) : ''); // '_backgroundColor' is template specific. 'backgroundColor' would apply to all templates, but has no UI at the moment } } }); @@ -43,24 +49,34 @@ export class ColorBox extends ViewBoxBaseComponent<FieldViewProps>() { render() { const scaling = Math.min(this.layoutDoc.fitWidth ? 10000 : this.props.PanelHeight() / this.rootDoc[HeightSym](), this.props.PanelWidth() / this.rootDoc[WidthSym]()); - return <div className={`colorBox-container${this.isContentActive() ? "-interactive" : ""}`} - onPointerDown={e => e.button === 0 && !e.ctrlKey && e.stopPropagation()} onClick={e => e.stopPropagation()} - style={{ transform: `scale(${scaling})`, width: `${100 * scaling}%`, height: `${100 * scaling}%` }} > - - <SketchPicker - onChange={c => CurrentUserUtils.ActiveTool === InkTool.None && ColorBox.switchColor(c)} - color={StrCast(SelectionManager.Views()?.[0]?.rootDoc?._backgroundColor, ActiveInkColor())} - presetColors={['#D0021B', '#F5A623', '#F8E71C', '#8B572A', '#7ED321', '#417505', '#9013FE', '#4A90E2', '#50E3C2', '#B8E986', - '#000000', '#4A4A4A', '#9B9B9B', '#FFFFFF', '#f1efeb', 'transparent']} - /> + return ( + <div + className={`colorBox-container${this.isContentActive() ? '-interactive' : ''}`} + onPointerDown={e => e.button === 0 && !e.ctrlKey && e.stopPropagation()} + onClick={e => e.stopPropagation()} + style={{ transform: `scale(${scaling})`, width: `${100 * scaling}%`, height: `${100 * scaling}%` }}> + <SketchPicker + onChange={c => Doc.ActiveTool === InkTool.None && ColorBox.switchColor(c)} + color={StrCast(SelectionManager.Views()?.[0]?.rootDoc?._backgroundColor, ActiveInkColor())} + presetColors={['#D0021B', '#F5A623', '#F8E71C', '#8B572A', '#7ED321', '#417505', '#9013FE', '#4A90E2', '#50E3C2', '#B8E986', '#000000', '#4A4A4A', '#9B9B9B', '#FFFFFF', '#f1efeb', 'transparent']} + /> - <div style={{ width: this.props.PanelWidth() / scaling, display: "flex", paddingTop: "10px" }}> - <div> {ActiveInkWidth()}</div> - <input type="range" defaultValue={ActiveInkWidth()} min={1} max={100} onChange={(e: React.ChangeEvent<HTMLInputElement>) => { - SetActiveInkWidth(e.target.value); - SelectionManager.Views().filter(i => StrCast(i.rootDoc.type) === DocumentType.INK).map(i => i.rootDoc.strokeWidth = Number(e.target.value)); - }} /> + <div style={{ width: this.props.PanelWidth() / scaling, display: 'flex', paddingTop: '10px' }}> + <div> {ActiveInkWidth()}</div> + <input + type="range" + defaultValue={ActiveInkWidth()} + min={1} + max={100} + onChange={(e: React.ChangeEvent<HTMLInputElement>) => { + SetActiveInkWidth(e.target.value); + SelectionManager.Views() + .filter(i => StrCast(i.rootDoc.type) === DocumentType.INK) + .map(i => (i.rootDoc.strokeWidth = Number(e.target.value))); + }} + /> + </div> </div> - </div>; + ); } } diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx index 7569b209d..f89c65052 100644 --- a/src/client/views/nodes/DocumentView.tsx +++ b/src/client/views/nodes/DocumentView.tsx @@ -1,56 +1,57 @@ -import { IconProp } from "@fortawesome/fontawesome-svg-core"; -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { action, computed, IReactionDisposer, observable, reaction, runInAction } from "mobx"; -import { observer } from "mobx-react"; -import { AclAdmin, AclEdit, AclPrivate, DataSym, Doc, DocListCast, DocListCastAsync, Field, HeightSym, Opt, StrListCast, WidthSym } from "../../../fields/Doc"; +import { IconProp } from '@fortawesome/fontawesome-svg-core'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { action, computed, IReactionDisposer, observable, reaction, runInAction } from 'mobx'; +import { observer } from 'mobx-react'; +import { AclAdmin, AclEdit, AclPrivate, DataSym, Doc, DocListCast, Field, HeightSym, Opt, StrListCast, WidthSym } from '../../../fields/Doc'; import { Document } from '../../../fields/documentSchemas'; import { Id } from '../../../fields/FieldSymbols'; import { InkTool } from '../../../fields/InkField'; -import { List } from "../../../fields/List"; -import { ObjectField } from "../../../fields/ObjectField"; -import { listSpec } from "../../../fields/Schema"; +import { List } from '../../../fields/List'; +import { ObjectField } from '../../../fields/ObjectField'; +import { listSpec } from '../../../fields/Schema'; import { ScriptField } from '../../../fields/ScriptField'; -import { BoolCast, Cast, ImageCast, NumCast, ScriptCast, StrCast } from "../../../fields/Types"; -import { AudioField } from "../../../fields/URLField"; +import { BoolCast, Cast, ImageCast, NumCast, ScriptCast, StrCast } from '../../../fields/Types'; +import { AudioField } from '../../../fields/URLField'; import { GetEffectiveAcl, SharingPermissions, TraceMobx } from '../../../fields/util'; import { MobileInterface } from '../../../mobile/MobileInterface'; -import { emptyFunction, hasDescendantTarget, lightOrDark, OmitKeys, returnEmptyString, returnTrue, returnFalse, returnVal, simulateMouseClick, Utils } from "../../../Utils"; +import { emptyFunction, hasDescendantTarget, lightOrDark, OmitKeys, returnEmptyString, returnFalse, returnTrue, returnVal, simulateMouseClick, Utils } from '../../../Utils'; import { GooglePhotos } from '../../apis/google_docs/GooglePhotosClientUtils'; -import { Docs, DocUtils } from "../../documents/Documents"; -import { DocumentType } from '../../documents/DocumentTypes'; -import { Networking } from "../../Network"; -import { CurrentUserUtils } from '../../util/CurrentUserUtils'; -import { DocumentManager } from "../../util/DocumentManager"; -import { DragManager, dropActionType } from "../../util/DragManager"; +import { DocServer } from '../../DocServer'; +import { Docs, DocUtils } from '../../documents/Documents'; +import { CollectionViewType, DocumentType } from '../../documents/DocumentTypes'; +import { Networking } from '../../Network'; +import { DocumentManager } from '../../util/DocumentManager'; +import { DragManager, dropActionType } from '../../util/DragManager'; import { InteractionUtils } from '../../util/InteractionUtils'; +import { LinkFollower } from '../../util/LinkFollower'; import { LinkManager } from '../../util/LinkManager'; import { ScriptingGlobals } from '../../util/ScriptingGlobals'; -import { SelectionManager } from "../../util/SelectionManager"; +import { SelectionManager } from '../../util/SelectionManager'; +import { SettingsManager } from '../../util/SettingsManager'; import { SharingManager } from '../../util/SharingManager'; import { SnappingManager } from '../../util/SnappingManager'; -import { Transform } from "../../util/Transform"; -import { undoBatch, UndoManager } from "../../util/UndoManager"; -import { CollectionView, CollectionViewType } from '../collections/CollectionView'; -import { ContextMenu } from "../ContextMenu"; +import { Transform } from '../../util/Transform'; +import { undoBatch, UndoManager } from '../../util/UndoManager'; +import { CollectionView } from '../collections/CollectionView'; +import { ContextMenu } from '../ContextMenu'; import { ContextMenuProps } from '../ContextMenuItem'; -import { DocComponent } from "../DocComponent"; +import { DocComponent } from '../DocComponent'; import { EditableView } from '../EditableView'; -import { InkingStroke } from "../InkingStroke"; -import { LightboxView } from "../LightboxView"; -import { StyleProp } from "../StyleProvider"; -import { CollectionFreeFormDocumentView } from "./CollectionFreeFormDocumentView"; -import { DocumentContentsView } from "./DocumentContentsView"; +import { InkingStroke } from '../InkingStroke'; +import { LightboxView } from '../LightboxView'; +import { StyleProp } from '../StyleProvider'; +import { CollectionFreeFormDocumentView } from './CollectionFreeFormDocumentView'; +import { DocumentContentsView } from './DocumentContentsView'; import { DocumentLinksButton } from './DocumentLinksButton'; -import "./DocumentView.scss"; -import { FormattedTextBox } from "./formattedText/FormattedTextBox"; +import './DocumentView.scss'; +import { FieldViewProps } from './FieldView'; +import { FormattedTextBox } from './formattedText/FormattedTextBox'; import { LinkAnchorBox } from './LinkAnchorBox'; -import { LinkDocPreview } from "./LinkDocPreview"; +import { LinkDocPreview } from './LinkDocPreview'; import { RadialMenu } from './RadialMenu'; -import { ScriptingBox } from "./ScriptingBox"; +import { ScriptingBox } from './ScriptingBox'; import { PresBox } from './trails/PresBox'; -import React = require("react"); -import { DocServer } from "../../DocServer"; -import { FieldViewProps } from "./FieldView"; +import React = require('react'); const { Howl } = require('howler'); interface Window { @@ -64,16 +65,16 @@ declare class MediaRecorder { export enum ViewAdjustment { resetView = 1, - doNothing = 0 + doNothing = 0, } -export const ViewSpecPrefix = "viewSpec"; // field prefix for anchor fields that are immediately copied over to the target document when link is followed. Other anchor properties will be copied over in the specific setViewSpec() method on their view (which allows for seting preview values instead of writing to the document) +export const ViewSpecPrefix = 'viewSpec'; // field prefix for anchor fields that are immediately copied over to the target document when link is followed. Other anchor properties will be copied over in the specific setViewSpec() method on their view (which allows for seting preview values instead of writing to the document) export interface DocFocusOptions { originalTarget?: Doc; // set in JumpToDocument, used by TabDocView to determine whether to fit contents to tab - willZoom?: boolean; // determines whether to zoom in on target document - scale?: number; // percent of containing frame to zoom into document - afterFocus?: DocAfterFocusFunc; // function to call after focusing on a document + willZoom?: boolean; // determines whether to zoom in on target document + scale?: number; // percent of containing frame to zoom into document + afterFocus?: DocAfterFocusFunc; // function to call after focusing on a document docTransform?: Transform; // when a document can't be panned and zoomed within its own container (say a group), then we need to continue to move up the render hierarchy to find something that can pan and zoom. when this happens the docTransform must accumulate all the transforms of each level of the hierarchy instant?: boolean; // whether focus should happen instantly (as opposed to smooth zoom) } @@ -84,9 +85,9 @@ export interface DocComponentView { updateIcon?: () => void; // updates the icon representation of the document getAnchor?: () => Doc; // returns an Anchor Doc that represents the current state of the doc's componentview (e.g., the current playhead location of a an audio/video box) scrollFocus?: (doc: Doc, smooth: boolean) => Opt<number>; // returns the duration of the focus - setViewSpec?: (anchor: Doc, preview: boolean) => void; // sets viewing information for a componentview, typically when following a link. 'preview' tells the view to use the values without writing to the document + setViewSpec?: (anchor: Doc, preview: boolean) => void; // sets viewing information for a componentview, typically when following a link. 'preview' tells the view to use the values without writing to the document 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 + shrinkWrap?: () => void; // requests a document to display all of its contents with no white space. currently only implemented (needed?) for freeform views 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 getKeyFrameEditing?: () => boolean; // whether the document is in keyframe editing mode (if it is, then all hidden documents that are not active at the keyframe time will still be shown) @@ -99,20 +100,20 @@ export interface DocComponentView { annotationKey?: string; getTitle?: () => string; getScrollHeight?: () => number; - getCenter?: (xf: Transform) => { X: number, Y: number }; - ptToScreen?: (pt: { X: number, Y: number }) => { X: number, Y: number }; - ptFromScreen?: (pt: { X: number, Y: number }) => { X: number, Y: number }; - snapPt?: (pt: { X: number, Y: number }, excludeSegs?: number[]) => { nearestPt: { X: number, Y: number }, distance: number }; + getCenter?: (xf: Transform) => { X: number; Y: number }; + ptToScreen?: (pt: { X: number; Y: number }) => { X: number; Y: number }; + ptFromScreen?: (pt: { X: number; Y: number }) => { X: number; Y: number }; + snapPt?: (pt: { X: number; Y: number }, excludeSegs?: number[]) => { nearestPt: { X: number; Y: number }; distance: number }; search?: (str: string, bwd?: boolean, clear?: boolean) => boolean; } -// These props are passed to both FieldViews and DocumentViews +// These props are passed to both FieldViews and DocumentViews export interface DocumentViewSharedProps { fieldKey?: string; // only used by FieldViews but helpful here to allow styleProviders to access fieldKey of FieldViewProps. In priniciple, passing a fieldKey to a documentView could override or be the default fieldKey for fieldViews DocumentView?: () => DocumentView; renderDepth: number; Document: Doc; DataDoc?: Doc; - contentBounds?: () => (undefined|{x:number, y:number, r:number, b:number}); + contentBounds?: () => undefined | { x: number; y: number; r: number; b: number }; fitContentsToBox?: () => boolean; // used by freeformview to fit its contents to its panel. corresponds to _fitContentsToBox property on a Document ContainingCollectionView: Opt<CollectionView>; ContainingCollectionDoc: Opt<Doc>; @@ -137,7 +138,7 @@ export interface DocumentViewSharedProps { whenChildContentsActiveChanged: (isActive: boolean) => void; rootSelected: (outsideReaction?: boolean) => boolean; // whether the root of a template has been selected addDocTab: (doc: Doc, where: string) => boolean; - filterAddDocument?: (doc: Doc[]) => boolean; // allows a document that renders a Collection view to filter or modify any documents added to the collection (see PresBox for an example) + filterAddDocument?: (doc: Doc[]) => boolean; // allows a document that renders a Collection view to filter or modify any documents added to the collection (see PresBox for an example) addDocument?: (doc: Doc | Doc[]) => boolean; removeDocument?: (doc: Doc | Doc[]) => boolean; moveDocument?: (doc: Doc | Doc[], targetCollection: Doc | undefined, addDocument: (document: Doc | Doc[]) => boolean) => boolean; @@ -161,8 +162,8 @@ export interface DocumentViewSharedProps { export interface DocumentViewProps extends DocumentViewSharedProps { // properties specific to DocumentViews but not to FieldView hideResizeHandles?: boolean; // whether to suppress DocumentDecorations when this document is selected - hideTitle?: boolean; // forces suppression of title. e.g, treeView document labels suppress titles in case they are globally active via settings - hideDecorationTitle?: boolean; // forces suppression of title. e.g, treeView document labels suppress titles in case they are globally active via settings + hideTitle?: boolean; // forces suppression of title. e.g, treeView document labels suppress titles in case they are globally active via settings + hideDecorationTitle?: boolean; // forces suppression of title. e.g, treeView document labels suppress titles in case they are globally active via settings hideDocumentButtonBar?: boolean; hideOpenButton?: boolean; hideDeleteButton?: boolean; @@ -172,19 +173,19 @@ export interface DocumentViewProps extends DocumentViewSharedProps { contentPointerEvents?: string; // pointer events allowed for content of a document view. eg. set to "none" in menuSidebar for sharedDocs so that you can select a document, but not interact with its contents radialMenu?: String[]; LayoutTemplateString?: string; - dontCenter?: "x" | "y" | "xy"; + dontCenter?: 'x' | 'y' | 'xy'; dontScaleFilter?: (doc: Doc) => boolean; // decides whether a document can be scaled to fit its container vs native size with scrolling ContentScaling?: () => number; // scaling the DocumentView does to transform its contents into its panel & needed by ScreenToLocal NativeWidth?: () => number; NativeHeight?: () => number; LayoutTemplate?: () => Opt<Doc>; - contextMenuItems?: () => { script: ScriptField, filter?: ScriptField, label: string, icon: string }[]; + contextMenuItems?: () => { script: ScriptField; filter?: ScriptField; label: string; icon: string }[]; onClick?: () => ScriptField; onDoubleClick?: () => ScriptField; onPointerDown?: () => ScriptField; onPointerUp?: () => ScriptField; - onBrowseClick?: () => (ScriptField | undefined); - onKey?: (e: React.KeyboardEvent, fieldProps: FieldViewProps) => (boolean | undefined); + onBrowseClick?: () => ScriptField | undefined; + onKey?: (e: React.KeyboardEvent, fieldProps: FieldViewProps) => boolean | undefined; } // these props are only available in DocumentViewIntenral @@ -219,34 +220,88 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps protected _multiTouchDisposer?: InteractionUtils.MultiTouchEventDisposer; @observable _componentView: Opt<DocComponentView>; // needs to be accessed from DocumentView wrapper class - private get topMost() { return this.props.renderDepth === 0 && !LightboxView.LightboxDoc; } - public get displayName() { return "DocumentView(" + this.props.Document.title + ")"; } // this makes mobx trace() statements more descriptive - public get ContentDiv() { return this._mainCont.current; } - public get LayoutFieldKey() { return Doc.LayoutFieldKey(this.layoutDoc); } - @computed get ShowTitle() { return this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.ShowTitle) as (Opt<string>); } - @computed get ContentScale() { return this.props.ContentScaling?.() || 1; } - @computed get thumb() { return ImageCast(this.layoutDoc["thumb-frozen"], ImageCast(this.layoutDoc.thumb))?.url.href.replace(".png", "_m.png"); } - @computed get hidden() { return this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.Hidden); } - @computed get opacity() { return this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.Opacity); } - @computed get boxShadow() { return this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.BoxShadow); } - @computed get borderRounding() { return this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.BorderRounding); } - @computed get hideLinkButton() { return this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.HideLinkButton + (this.props.isSelected() ? ":selected" : "")); } - @computed get widgetDecorations() { return this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.Decorations + (this.props.isSelected() ? ":selected" : "")); } - @computed get backgroundColor() { return this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.BackgroundColor); } - @computed get docContents() { return this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.DocContents); } - @computed get headerMargin() { return this.props?.styleProvider?.(this.layoutDoc, this.props, StyleProp.HeaderMargin) || 0; } - @computed get titleHeight() { return this.props?.styleProvider?.(this.layoutDoc, this.props, StyleProp.TitleHeight) || 0; } - @computed get pointerEvents() { return this.props.styleProvider?.(this.Document, this.props, StyleProp.PointerEvents + (this.props.isSelected() ? ":selected" : "")); } - @computed get finalLayoutKey() { return StrCast(this.Document.layoutKey, "layout"); } - @computed get nativeWidth() { return this.props.NativeWidth(); } - @computed get nativeHeight() { return this.props.NativeHeight(); } - @computed get onClickHandler() { return this.props.onClick?.() ?? (this.props.onBrowseClick?.() ?? Cast(this.Document.onClick, ScriptField, Cast(this.layoutDoc.onClick, ScriptField, null))); } - @computed get onDoubleClickHandler() { return this.props.onDoubleClick?.() ?? (Cast(this.layoutDoc.onDoubleClick, ScriptField, null) ?? this.Document.onDoubleClick); } - @computed get onPointerDownHandler() { return this.props.onPointerDown?.() ?? ScriptCast(this.Document.onPointerDown); } - @computed get onPointerUpHandler() { return this.props.onPointerUp?.() ?? ScriptCast(this.Document.onPointerUp); } - - componentWillUnmount() { this.cleanupHandlers(true); } - componentDidMount() { this.setupHandlers(); } + private get topMost() { + return this.props.renderDepth === 0 && !LightboxView.LightboxDoc; + } + public get displayName() { + return 'DocumentView(' + this.props.Document.title + ')'; + } // this makes mobx trace() statements more descriptive + public get ContentDiv() { + return this._mainCont.current; + } + public get LayoutFieldKey() { + return Doc.LayoutFieldKey(this.layoutDoc); + } + @computed get ShowTitle() { + return this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.ShowTitle) as Opt<string>; + } + @computed get ContentScale() { + return this.props.ContentScaling?.() || 1; + } + @computed get thumb() { + return ImageCast(this.layoutDoc['thumb-frozen'], ImageCast(this.layoutDoc.thumb))?.url.href.replace('.png', '_m.png'); + } + @computed get hidden() { + return this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.Hidden); + } + @computed get opacity() { + return this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.Opacity); + } + @computed get boxShadow() { + return this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.BoxShadow); + } + @computed get borderRounding() { + return this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.BorderRounding); + } + @computed get hideLinkButton() { + return this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.HideLinkButton + (this.props.isSelected() ? ':selected' : '')); + } + @computed get widgetDecorations() { + return this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.Decorations + (this.props.isSelected() ? ':selected' : '')); + } + @computed get backgroundColor() { + return this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.BackgroundColor); + } + @computed get docContents() { + return this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.DocContents); + } + @computed get headerMargin() { + return this.props?.styleProvider?.(this.layoutDoc, this.props, StyleProp.HeaderMargin) || 0; + } + @computed get titleHeight() { + return this.props?.styleProvider?.(this.layoutDoc, this.props, StyleProp.TitleHeight) || 0; + } + @computed get pointerEvents() { + return this.props.styleProvider?.(this.Document, this.props, StyleProp.PointerEvents + (this.props.isSelected() ? ':selected' : '')); + } + @computed get finalLayoutKey() { + return StrCast(this.Document.layoutKey, 'layout'); + } + @computed get nativeWidth() { + return this.props.NativeWidth(); + } + @computed get nativeHeight() { + return this.props.NativeHeight(); + } + @computed get onClickHandler() { + return this.props.onClick?.() ?? this.props.onBrowseClick?.() ?? Cast(this.Document.onClick, ScriptField, Cast(this.layoutDoc.onClick, ScriptField, null)); + } + @computed get onDoubleClickHandler() { + return this.props.onDoubleClick?.() ?? Cast(this.layoutDoc.onDoubleClick, ScriptField, null) ?? this.Document.onDoubleClick; + } + @computed get onPointerDownHandler() { + return this.props.onPointerDown?.() ?? ScriptCast(this.Document.onPointerDown); + } + @computed get onPointerUpHandler() { + return this.props.onPointerUp?.() ?? ScriptCast(this.Document.onPointerUp); + } + + componentWillUnmount() { + this.cleanupHandlers(true); + } + componentDidMount() { + this.setupHandlers(); + } //componentDidUpdate() { this.setupHandlers(); } setupHandlers() { this.cleanupHandlers(false); @@ -268,8 +323,8 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps handle1PointerHoldStart = (e: Event, me: InteractionUtils.MultiTouchEvent<React.TouchEvent>): any => { this.removeMoveListeners(); this.removeEndListeners(); - document.removeEventListener("pointermove", this.onPointerMove); - document.removeEventListener("pointerup", this.onPointerUp); + document.removeEventListener('pointermove', this.onPointerMove); + document.removeEventListener('pointerup', this.onPointerUp); if (RadialMenu.Instance._display === false) { this.addHoldMoveListeners(); this.addHoldEndListeners(); @@ -278,7 +333,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps this._firstX = pt.pageX; this._firstY = pt.pageY; } - } + }; handle1PointerHoldMove = (e: Event, me: InteractionUtils.MultiTouchEvent<TouchEvent>): void => { const pt = me.touchEvent.touches[me.touchEvent.touches.length - 1]; @@ -289,7 +344,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps if (Math.abs(pt.pageX - this._firstX) > 150 || Math.abs(pt.pageY - this._firstY) > 150) { this.handle1PointerHoldEnd(e, me); } - } + }; handle1PointerHoldEnd = (e: Event, me: InteractionUtils.MultiTouchEvent<TouchEvent>): void => { this.removeHoldMoveListeners(); @@ -304,7 +359,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps if (RadialMenu.Instance.used) { this.onContextMenu(undefined, me.touches[0].pageX, me.touches[0].pageY); } - } + }; handle2PointersDown = (e: React.TouchEvent, me: InteractionUtils.MultiTouchEvent<React.TouchEvent>) => { if (!e.nativeEvent.cancelBubble && !this.props.isSelected()) { @@ -316,7 +371,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps this.removeEndListeners(); this.addEndListeners(); } - } + }; handle1PointerDown = (e: React.TouchEvent, me: InteractionUtils.MultiTouchEvent<React.TouchEvent>) => { SelectionManager.DeselectAll(); @@ -326,7 +381,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps this._downX = touch.clientX; this._downY = touch.clientY; if (!e.nativeEvent.cancelBubble) { - if ((this.props.isDocumentActive?.() || this.layoutDoc.onDragStart || this.onClickHandler) && !e.ctrlKey && !this.layoutDoc._lockedPosition && !DocListCast(CurrentUserUtils.MyOverlayDocs?.data).includes(this.layoutDoc)) e.stopPropagation(); + if ((this.props.isDocumentActive?.() || this.layoutDoc.onDragStart || this.onClickHandler) && !e.ctrlKey && !this.layoutDoc._lockedPosition && !DocListCast(Doc.MyOverlayDocs?.data).includes(this.layoutDoc)) e.stopPropagation(); this.removeMoveListeners(); this.addMoveListeners(); this.removeEndListeners(); @@ -334,24 +389,23 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps e.stopPropagation(); } } - } + }; handle1PointerMove = (e: TouchEvent, me: InteractionUtils.MultiTouchEvent<TouchEvent>) => { if (e.cancelBubble && this.props.isDocumentActive?.()) { this.removeMoveListeners(); - } - else if (!e.cancelBubble && (this.props.isDocumentActive?.() || this.layoutDoc.onDragStart || this.onClickHandler) && !this.layoutDoc._lockedPosition && !DocListCast(CurrentUserUtils.MyOverlayDocs?.data).includes(this.layoutDoc)) { + } else if (!e.cancelBubble && (this.props.isDocumentActive?.() || this.layoutDoc.onDragStart || this.onClickHandler) && !this.layoutDoc._lockedPosition && !DocListCast(Doc.MyOverlayDocs?.data).includes(this.layoutDoc)) { const touch = me.touchEvent.changedTouches.item(0); if (touch && (Math.abs(this._downX - touch.clientX) > 3 || Math.abs(this._downY - touch.clientY) > 3)) { if (!e.altKey && (!this.topMost || this.layoutDoc.onDragStart || this.onClickHandler)) { this.cleanUpInteractions(); - this.startDragging(this._downX, this._downY, this.Document.dropAction ? this.Document.dropAction as any : e.ctrlKey || e.altKey ? "alias" : undefined); + this.startDragging(this._downX, this._downY, this.Document.dropAction ? (this.Document.dropAction as any) : e.ctrlKey || e.altKey ? 'alias' : undefined); } } e.stopPropagation(); // doesn't actually stop propagation since all our listeners are listening to events on 'document' however it does mark the event as cancelBubble=true which we test for in the move event handlers e.preventDefault(); } - } + }; @action handle2PointersMove = (e: TouchEvent, me: InteractionUtils.MultiTouchEvent<TouchEvent>) => { @@ -362,8 +416,8 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps const oldPoint2 = this.prevPoints.get(pt2.identifier); const pinching = InteractionUtils.Pinning(pt1, pt2, oldPoint1!, oldPoint2!); if (pinching !== 0 && oldPoint1 && oldPoint2) { - const dW = (Math.abs(pt1.clientX - pt2.clientX) - Math.abs(oldPoint1.clientX - oldPoint2.clientX)); - const dH = (Math.abs(pt1.clientY - pt2.clientY) - Math.abs(oldPoint1.clientY - oldPoint2.clientY)); + const dW = Math.abs(pt1.clientX - pt2.clientX) - Math.abs(oldPoint1.clientX - oldPoint2.clientX); + const dH = Math.abs(pt1.clientY - pt2.clientY) - Math.abs(oldPoint1.clientY - oldPoint2.clientY); const dX = -1 * Math.sign(dW); const dY = -1 * Math.sign(dH); @@ -372,33 +426,32 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps const layoutDoc = Document(Doc.Layout(this.props.Document)); let nwidth = Doc.NativeWidth(layoutDoc); let nheight = Doc.NativeHeight(layoutDoc); - const width = (layoutDoc._width || 0); - const height = (layoutDoc._height || (nheight / nwidth * width)); + const width = layoutDoc._width || 0; + const height = layoutDoc._height || (nheight / nwidth) * width; const scale = this.props.ScreenToLocalTransform().Scale * this.ContentScale; - const actualdW = Math.max(width + (dW * scale), 20); - const actualdH = Math.max(height + (dH * scale), 20); + const actualdW = Math.max(width + dW * scale, 20); + const actualdH = Math.max(height + dH * scale, 20); doc.x = (doc.x || 0) + dX * (actualdW - width); doc.y = (doc.y || 0) + dY * (actualdH - height); const fixedAspect = e.ctrlKey || (nwidth && nheight); if (fixedAspect && (!nwidth || !nheight)) { - Doc.SetNativeWidth(layoutDoc, nwidth = layoutDoc._width || 0); - Doc.SetNativeHeight(layoutDoc, nheight = layoutDoc._height || 0); + Doc.SetNativeWidth(layoutDoc, (nwidth = layoutDoc._width || 0)); + Doc.SetNativeHeight(layoutDoc, (nheight = layoutDoc._height || 0)); } if (nwidth > 0 && nheight > 0) { if (Math.abs(dW) > Math.abs(dH)) { if (!fixedAspect) { - Doc.SetNativeWidth(layoutDoc, actualdW / (layoutDoc._width || 1) * Doc.NativeWidth(layoutDoc)); + Doc.SetNativeWidth(layoutDoc, (actualdW / (layoutDoc._width || 1)) * Doc.NativeWidth(layoutDoc)); } layoutDoc._width = actualdW; - if (fixedAspect && !this.props.DocumentView().fitWidth) layoutDoc._height = nheight / nwidth * layoutDoc._width; + if (fixedAspect && !this.props.DocumentView().fitWidth) layoutDoc._height = (nheight / nwidth) * layoutDoc._width; else layoutDoc._height = actualdH; - } - else { + } else { if (!fixedAspect) { - Doc.SetNativeHeight(layoutDoc, actualdH / (layoutDoc._height || 1) * Doc.NativeHeight(doc)); + Doc.SetNativeHeight(layoutDoc, (actualdH / (layoutDoc._height || 1)) * Doc.NativeHeight(doc)); } layoutDoc._height = actualdH; - if (fixedAspect && !this.props.DocumentView().fitWidth) layoutDoc._width = nwidth / nheight * layoutDoc._height; + if (fixedAspect && !this.props.DocumentView().fitWidth) layoutDoc._width = (nwidth / nheight) * layoutDoc._height; else layoutDoc._width = actualdW; } } else { @@ -410,7 +463,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps e.stopPropagation(); e.preventDefault(); } - } + }; @action onRadialMenu = (e: Event, me: InteractionUtils.MultiTouchEvent<React.TouchEvent>): void => { @@ -419,19 +472,30 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps // RadialMenu.Instance.addItem({ description: "Open Fields", event: () => this.props.addDocTab(Docs.Create.KVPDocument(this.props.Document, { _width: 300, _height: 300 }), "add:right"), icon: "map-pin", selected: -1 }); const effectiveAcl = GetEffectiveAcl(this.props.Document[DataSym]); - (effectiveAcl === AclEdit || effectiveAcl === AclAdmin) && RadialMenu.Instance.addItem({ description: "Delete", event: () => { this.props.ContainingCollectionView?.removeDocument(this.props.Document), RadialMenu.Instance.closeMenu(); }, icon: "external-link-square-alt", selected: -1 }); + (effectiveAcl === AclEdit || effectiveAcl === AclAdmin) && + RadialMenu.Instance.addItem({ + description: 'Delete', + event: () => { + this.props.ContainingCollectionView?.removeDocument(this.props.Document), RadialMenu.Instance.closeMenu(); + }, + icon: 'external-link-square-alt', + selected: -1, + }); // RadialMenu.Instance.addItem({ description: "Open in a new tab", event: () => this.props.addDocTab(this.props.Document, "add:right"), icon: "trash", selected: -1 }); - RadialMenu.Instance.addItem({ description: "Pin", event: () => this.props.pinToPres(this.props.Document), icon: "map-pin", selected: -1 }); - RadialMenu.Instance.addItem({ description: "Open", event: () => MobileInterface.Instance.handleClick(this.props.Document), icon: "trash", selected: -1 }); + RadialMenu.Instance.addItem({ description: 'Pin', event: () => this.props.pinToPres(this.props.Document), icon: 'map-pin', selected: -1 }); + RadialMenu.Instance.addItem({ description: 'Open', event: () => MobileInterface.Instance.handleClick(this.props.Document), icon: 'trash', selected: -1 }); SelectionManager.DeselectAll(); - } + }; startDragging(x: number, y: number, dropAction: dropActionType, hideSource = false) { if (this._mainCont.current) { const dragData = new DragManager.DocumentDragData([this.props.Document]); const [left, top] = this.props.ScreenToLocalTransform().scale(this.ContentScale).inverse().transformPoint(0, 0); - dragData.offset = this.props.ScreenToLocalTransform().scale(this.ContentScale).transformDirection(x - left, y - top); + dragData.offset = this.props + .ScreenToLocalTransform() + .scale(this.ContentScale) + .transformDirection(x - left, y - top); dragData.offset[0] = Math.min(this.rootDoc[WidthSym](), dragData.offset[0]); dragData.offset[1] = Math.min(this.rootDoc[HeightSym](), dragData.offset[1]); dragData.dropAction = dropAction; @@ -440,8 +504,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps dragData.moveDocument = this.props.moveDocument; const ffview = this.props.CollectionFreeFormDocumentView?.().props.CollectionFreeFormView; ffview && runInAction(() => (ffview.ChildDrag = this.props.DocumentView())); - DragManager.StartDocumentDrag([this._mainCont.current], dragData, x, y, { hideSource: hideSource || (!dropAction && !this.layoutDoc.onDragStart) }, - () => setTimeout(action(() => ffview && (ffview.ChildDrag = undefined)))); // this needs to happen after the drop event is processed. + DragManager.StartDocumentDrag([this._mainCont.current], dragData, x, y, { hideSource: hideSource || (!dropAction && !this.layoutDoc.onDragStart) }, () => setTimeout(action(() => ffview && (ffview.ChildDrag = undefined)))); // this needs to happen after the drop event is processed. ffview?.setupDragLines(false); } } @@ -450,91 +513,114 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps if (e.altKey && !e.nativeEvent.cancelBubble) { e.stopPropagation(); e.preventDefault(); - if (e.key === "†" || e.key === "t") { - if (!StrCast(this.layoutDoc._showTitle)) this.layoutDoc._showTitle = "title"; + if (e.key === '†' || e.key === 't') { + if (!StrCast(this.layoutDoc._showTitle)) this.layoutDoc._showTitle = 'title'; if (!this._titleRef.current) setTimeout(() => this._titleRef.current?.setIsFocused(true), 0); - else if (!this._titleRef.current.setIsFocused(true)) { // if focus didn't change, focus on interior text... + else if (!this._titleRef.current.setIsFocused(true)) { + // if focus didn't change, focus on interior text... this._titleRef.current?.setIsFocused(false); this._componentView?.setFocus?.(); } } } - } + }; focus = (anchor: Doc, options?: DocFocusOptions) => { - LightboxView.SetCookie(StrCast(anchor["cookies-set"])); + LightboxView.SetCookie(StrCast(anchor['cookies-set'])); // copying over VIEW fields immediately allows the view type to switch to create the right _componentView - Array.from(Object.keys(Doc.GetProto(anchor))).filter(key => key.startsWith(ViewSpecPrefix)).forEach(spec => { - this.layoutDoc[spec.replace(ViewSpecPrefix, "")] = ((field) => field instanceof ObjectField ? ObjectField.MakeCopy(field) : field)(anchor[spec]); - }); + Array.from(Object.keys(Doc.GetProto(anchor))) + .filter(key => key.startsWith(ViewSpecPrefix)) + .forEach(spec => { + this.layoutDoc[spec.replace(ViewSpecPrefix, '')] = (field => (field instanceof ObjectField ? ObjectField.MakeCopy(field) : field))(anchor[spec]); + }); // after a timeout, the right _componentView should have been created, so call it to update its view spec values setTimeout(() => this._componentView?.setViewSpec?.(anchor, LinkDocPreview.LinkInfo ? true : false)); const focusSpeed = this._componentView?.scrollFocus?.(anchor, options?.instant === false || !LinkDocPreview.LinkInfo); // bcz: smooth parameter should really be passed into focus() instead of inferred here - const endFocus = focusSpeed === undefined ? options?.afterFocus : async (moved: boolean) => (options?.afterFocus?.(true) ?? ViewAdjustment.doNothing); + const endFocus = focusSpeed === undefined ? options?.afterFocus : async (moved: boolean) => options?.afterFocus?.(true) ?? ViewAdjustment.doNothing; this.props.focus(options?.docTransform ? anchor : this.rootDoc, { - ...options, afterFocus: (didFocus: boolean) => - new Promise<ViewAdjustment>(res => setTimeout(async () => res(endFocus ? - await endFocus(didFocus||focusSpeed !== undefined) : - ViewAdjustment.doNothing), focusSpeed ?? 0) - ) + ...options, + afterFocus: (didFocus: boolean) => new Promise<ViewAdjustment>(res => setTimeout(async () => res(endFocus ? await endFocus(didFocus || focusSpeed !== undefined) : ViewAdjustment.doNothing), focusSpeed ?? 0)), }); - - } + }; onClick = action((e: React.MouseEvent | React.PointerEvent) => { - if (!e.nativeEvent.cancelBubble && !this.Document.ignoreClick && this.props.renderDepth >= 0 && - (Math.abs(e.clientX - this._downX) < Utils.DRAG_THRESHOLD && Math.abs(e.clientY - this._downY) < Utils.DRAG_THRESHOLD)) { + if (!e.nativeEvent.cancelBubble && !this.Document.ignoreClick && this.props.renderDepth >= 0 && Math.abs(e.clientX - this._downX) < Utils.DRAG_THRESHOLD && Math.abs(e.clientY - this._downY) < Utils.DRAG_THRESHOLD) { let stopPropagate = true; let preventDefault = true; const isScriptBox = () => StrCast(Doc.LayoutField(this.layoutDoc))?.includes(ScriptingBox.name); (this.rootDoc._raiseWhenDragged === undefined ? DragManager.GetRaiseWhenDragged() : this.rootDoc._raiseWhenDragged) && this.props.bringToFront(this.rootDoc); - if (this._doubleTap && (this.props.Document.type !== DocumentType.FONTICON || this.onDoubleClickHandler)) {// && !this.onClickHandler?.script) { // disable double-click to show full screen for things that have an on click behavior since clicking them twice can be misinterpreted as a double click + if (this._doubleTap && (this.props.Document.type !== DocumentType.FONTICON || this.onDoubleClickHandler)) { + // && !this.onClickHandler?.script) { // disable double-click to show full screen for things that have an on click behavior since clicking them twice can be misinterpreted as a double click if (this._timeout) { clearTimeout(this._timeout); this._pendingDoubleClick = false; this._timeout = undefined; } - if (this.onDoubleClickHandler?.script && !StrCast(Doc.LayoutField(this.layoutDoc))?.includes(ScriptingBox.name)) { // bcz: hack? don't execute script if you're clicking on a scripting box itself + if (this.onDoubleClickHandler?.script && !StrCast(Doc.LayoutField(this.layoutDoc))?.includes(ScriptingBox.name)) { + // bcz: hack? don't execute script if you're clicking on a scripting box itself const { clientX, clientY, shiftKey } = e; - const func = () => this.onDoubleClickHandler.script.run({ - this: this.layoutDoc, - self: this.rootDoc, - scriptContext: this.props.scriptContext, - thisContainer: this.props.ContainingCollectionDoc, - documentView: this.props.DocumentView(), - clientX, clientY, shiftKey - }, console.log); - UndoManager.RunInBatch(() => func().result?.select === true ? this.props.select(false) : "", "on double click"); + const func = () => + this.onDoubleClickHandler.script.run( + { + this: this.layoutDoc, + self: this.rootDoc, + scriptContext: this.props.scriptContext, + thisContainer: this.props.ContainingCollectionDoc, + documentView: this.props.DocumentView(), + clientX, + clientY, + shiftKey, + }, + console.log + ); + UndoManager.RunInBatch(() => (func().result?.select === true ? this.props.select(false) : ''), 'on double click'); } else if (!Doc.IsSystem(this.rootDoc)) { - UndoManager.RunInBatch(() => - LightboxView.AddDocTab(this.rootDoc, "lightbox", this.props.LayoutTemplate?.(), this.props.addDocTab) - , "double tap"); + UndoManager.RunInBatch(() => LightboxView.AddDocTab(this.rootDoc, 'lightbox', this.props.LayoutTemplate?.(), this.props.addDocTab), 'double tap'); SelectionManager.DeselectAll(); Doc.UnBrushDoc(this.props.Document); } - } else if (this.onClickHandler?.script && !isScriptBox()) { // bcz: hack? don't execute script if you're clicking on a scripting box itself + } else if (this.onClickHandler?.script && !isScriptBox()) { + // bcz: hack? don't execute script if you're clicking on a scripting box itself const { clientX, clientY, shiftKey } = e; - const func = () => this.onClickHandler.script.run({ - this: this.layoutDoc, - self: this.rootDoc, - _readOnly_: false, - scriptContext: this.props.scriptContext, - thisContainer: this.props.ContainingCollectionDoc, - documentView: this.props.DocumentView(), - clientX, clientY, shiftKey - }, console.log).result?.select === true ? this.props.select(false) : ""; - const clickFunc = () => this.props.Document.dontUndo ? func() : UndoManager.RunInBatch(func, "on click"); + const func = () => + this.onClickHandler.script.run( + { + this: this.layoutDoc, + self: this.rootDoc, + _readOnly_: false, + scriptContext: this.props.scriptContext, + thisContainer: this.props.ContainingCollectionDoc, + documentView: this.props.DocumentView(), + clientX, + clientY, + shiftKey, + }, + console.log + ).result?.select === true + ? this.props.select(false) + : ''; + const clickFunc = () => (this.props.Document.dontUndo ? func() : UndoManager.RunInBatch(func, 'on click')); if (this.onDoubleClickHandler) { - runInAction(() => this._pendingDoubleClick = true); - this._timeout = setTimeout(() => { this._timeout = undefined; clickFunc(); }, 350); + runInAction(() => (this._pendingDoubleClick = true)); + this._timeout = setTimeout(() => { + this._timeout = undefined; + clickFunc(); + }, 350); } else clickFunc(); } else if (this.allLinks && this.Document.type !== DocumentType.LINK && !isScriptBox() && this.Document.isLinkButton && !e.shiftKey && !e.ctrlKey) { - this.allLinks.length && LinkManager.FollowLink(undefined, this.props.Document, this.props, e.altKey); + this.allLinks.length && LinkFollower.FollowLink(undefined, this.props.Document, this.props, e.altKey); } else { - if ((this.layoutDoc.onDragStart || this.props.Document.rootDocument) && !(e.ctrlKey || e.button > 0)) { // onDragStart implies a button doc that we don't want to select when clicking. RootDocument & isTemplaetForField implies we're clicking on part of a template instance and we want to select the whole template, not the part + if ((this.layoutDoc.onDragStart || this.props.Document.rootDocument) && !(e.ctrlKey || e.button > 0)) { + // onDragStart implies a button doc that we don't want to select when clicking. RootDocument & isTemplaetForField implies we're clicking on part of a template instance and we want to select the whole template, not the part stopPropagate = false; // don't stop propagation for field templates -- want the selection to propagate up to the root document of the template } else { - runInAction(() => this._pendingDoubleClick = true); - this._timeout = setTimeout(action(() => { this._pendingDoubleClick = false; this._timeout = undefined; }), 350); + runInAction(() => (this._pendingDoubleClick = true)); + this._timeout = setTimeout( + action(() => { + this._pendingDoubleClick = false; + this._timeout = undefined; + }), + 350 + ); this.props.select(e.ctrlKey || e.shiftKey); } preventDefault = false; @@ -545,9 +631,9 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps }); onPointerDown = (e: React.PointerEvent): void => { - if (this.rootDoc.type === DocumentType.INK && CurrentUserUtils.ActiveTool === InkTool.Eraser) return; + if (this.rootDoc.type === DocumentType.INK && Doc.ActiveTool === InkTool.Eraser) return; // continue if the event hasn't been canceled AND we are using a mouse or this has an onClick or onDragStart function (meaning it is a button document) - if (!(InteractionUtils.IsType(e, InteractionUtils.MOUSETYPE) || [InkTool.Highlighter, InkTool.Pen, InkTool.Write].includes(CurrentUserUtils.ActiveTool))) { + if (!(InteractionUtils.IsType(e, InteractionUtils.MOUSETYPE) || [InkTool.Highlighter, InkTool.Pen, InkTool.Write].includes(Doc.ActiveTool))) { if (!InteractionUtils.IsType(e, InteractionUtils.PENTYPE)) { e.stopPropagation(); if (SelectionManager.IsSelected(this.props.DocumentView(), true) && this.props.Document._viewType !== CollectionViewType.Docking) e.preventDefault(); // goldenlayout needs to be able to move its tabs, so can't preventDefault for it @@ -557,50 +643,54 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps } this._downX = e.clientX; this._downY = e.clientY; - if ((!e.nativeEvent.cancelBubble || this.onClickHandler || this.layoutDoc.onDragStart) && + if ( + (!e.nativeEvent.cancelBubble || this.onClickHandler || this.layoutDoc.onDragStart) && // if this is part of a template, let the event go up to the tempalte root unless right/ctrl clicking - !(this.props.Document.rootDocument && !(e.ctrlKey || e.button > 0))) { - if ((this.props.isDocumentActive?.() || this.layoutDoc.onDragStart) && + !(this.props.Document.rootDocument && !(e.ctrlKey || e.button > 0)) + ) { + if ( + (this.props.isDocumentActive?.() || this.layoutDoc.onDragStart) && !this.props.onBrowseClick?.() && !this.Document.ignoreClick && !e.ctrlKey && (e.button === 0 || InteractionUtils.IsType(e, InteractionUtils.TOUCHTYPE)) && - !DocListCast(CurrentUserUtils.MyOverlayDocs?.data).includes(this.layoutDoc)) { + !DocListCast(Doc.MyOverlayDocs?.data).includes(this.layoutDoc) + ) { e.stopPropagation(); // don't preventDefault anymore. Goldenlayout, PDF text selection and RTF text selection all need it to go though //if (this.props.isSelected(true) && this.rootDoc.type !== DocumentType.PDF && this.layoutDoc._viewType !== CollectionViewType.Docking) e.preventDefault(); } if (this.props.isDocumentActive?.()) { - document.removeEventListener("pointermove", this.onPointerMove); - document.addEventListener("pointermove", this.onPointerMove); + document.removeEventListener('pointermove', this.onPointerMove); + document.addEventListener('pointermove', this.onPointerMove); } - document.removeEventListener("pointerup", this.onPointerUp); - document.addEventListener("pointerup", this.onPointerUp); + document.removeEventListener('pointerup', this.onPointerUp); + document.addEventListener('pointerup', this.onPointerUp); } - } + }; onPointerMove = (e: PointerEvent): void => { if (e.cancelBubble) return; - if ((InteractionUtils.IsType(e, InteractionUtils.PENTYPE) || [InkTool.Highlighter, InkTool.Pen, InkTool.Write].includes(CurrentUserUtils.ActiveTool))) return; + if (InteractionUtils.IsType(e, InteractionUtils.PENTYPE) || [InkTool.Highlighter, InkTool.Pen, InkTool.Write].includes(Doc.ActiveTool)) return; - if ((this.props.isDocumentActive?.() || this.layoutDoc.onDragStart) && !this.layoutDoc._lockedPosition && !DocListCast(CurrentUserUtils.MyOverlayDocs?.data).includes(this.layoutDoc)) { + if ((this.props.isDocumentActive?.() || this.layoutDoc.onDragStart) && !this.layoutDoc._lockedPosition && !DocListCast(Doc.MyOverlayDocs?.data).includes(this.layoutDoc)) { if (Math.abs(this._downX - e.clientX) > 3 || Math.abs(this._downY - e.clientY) > 3) { if (!e.altKey && (!this.topMost || this.layoutDoc.onDragStart || this.onClickHandler) && (e.buttons === 1 || InteractionUtils.IsType(e, InteractionUtils.TOUCHTYPE))) { - document.removeEventListener("pointermove", this.onPointerMove); - document.removeEventListener("pointerup", this.onPointerUp); - this.startDragging(this._downX, this._downY, ((e.ctrlKey || e.altKey) && "alias") || (this.props.dropAction || this.Document.dropAction || undefined) as dropActionType); + document.removeEventListener('pointermove', this.onPointerMove); + document.removeEventListener('pointerup', this.onPointerUp); + this.startDragging(this._downX, this._downY, ((e.ctrlKey || e.altKey) && 'alias') || ((this.props.dropAction || this.Document.dropAction || undefined) as dropActionType)); } } e.stopPropagation(); // doesn't actually stop propagation since all our listeners are listening to events on 'document' however it does mark the event as cancelBubble=true which we test for in the move event handlers e.preventDefault(); } - } + }; cleanupPointerEvents = () => { this.cleanUpInteractions(); - document.removeEventListener("pointermove", this.onPointerMove); - document.removeEventListener("pointerup", this.onPointerUp); - } + document.removeEventListener('pointermove', this.onPointerMove); + document.removeEventListener('pointerup', this.onPointerUp); + }; onPointerUp = (e: PointerEvent): void => { this.cleanupPointerEvents(); @@ -608,13 +698,14 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps if (this.onPointerUpHandler?.script && !InteractionUtils.IsType(e, InteractionUtils.PENTYPE)) { this.onPointerUpHandler.script.run({ self: this.rootDoc, this: this.layoutDoc }, console.log); } else { - this._doubleTap = (Date.now() - this._lastTap < 300 && e.button === 0 && Math.abs(e.clientX - this._downX) < 2 && Math.abs(e.clientY - this._downY) < 2); + this._doubleTap = Date.now() - this._lastTap < 300 && e.button === 0 && Math.abs(e.clientX - this._downX) < 2 && Math.abs(e.clientY - this._downY) < 2; // bcz: this is a placeholder. documents, when selected, should stopPropagation on doubleClicks if they want to keep the DocumentView from getting them - if (!this.props.isSelected(true) || ![DocumentType.PDF, DocumentType.RTF].includes(StrCast(this.rootDoc.type) as any)) this._lastTap = Date.now();// don't want to process the start of a double tap if the doucment is selected + if (!this.props.isSelected(true) || ![DocumentType.PDF, DocumentType.RTF].includes(StrCast(this.rootDoc.type) as any)) this._lastTap = Date.now(); // don't want to process the start of a double tap if the doucment is selected } - } + }; - @undoBatch @action + @undoBatch + @action toggleFollowLink = (location: Opt<string>, zoom?: boolean, setPushpin?: boolean): void => { this.Document.ignoreClick = false; if (setPushpin) { @@ -628,46 +719,48 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps this.Document.followLinkLocation = location; } else if (this.Document._isLinkButton && this.onClickHandler) { this.Document._isLinkButton = false; - this.Document["onClick-rawScript"] = this.dataDoc["onClick-rawScript"] = this.dataDoc.onClick = this.Document.onClick = this.layoutDoc.onClick = undefined; + this.Document['onClick-rawScript'] = this.dataDoc['onClick-rawScript'] = this.dataDoc.onClick = this.Document.onClick = this.layoutDoc.onClick = undefined; } - } - @undoBatch @action + }; + @undoBatch + @action toggleTargetOnClick = (): void => { this.Document.ignoreClick = false; this.Document._isLinkButton = true; this.Document.isPushpin = true; - } - @undoBatch @action - followLinkOnClick = (location: Opt<string>, zoom: boolean,): void => { + }; + @undoBatch + @action + followLinkOnClick = (location: Opt<string>, zoom: boolean): void => { this.Document.ignoreClick = false; this.Document._isLinkButton = true; this.Document.isPushpin = false; this.Document.followLinkZoom = zoom; this.Document.followLinkLocation = location; - } - @undoBatch @action + }; + @undoBatch + @action selectOnClick = (): void => { this.Document.ignoreClick = false; this.Document._isLinkButton = false; this.Document.isPushpin = false; this.Document.onClick = this.layoutDoc.onClick = undefined; - } + }; @undoBatch noOnClick = (): void => { this.Document.ignoreClick = false; this.Document._isLinkButton = false; - } + }; @undoBatch deleteClicked = () => this.props.removeDocument?.(this.props.Document); - @undoBatch setToggleDetail = () => this.Document.onClick = ScriptField.MakeScript(`toggleDetail(documentView, "${StrCast(this.Document.layoutKey).replace("layout_", "")}")`, { documentView: "any" }); + @undoBatch setToggleDetail = () => (this.Document.onClick = ScriptField.MakeScript(`toggleDetail(documentView, "${StrCast(this.Document.layoutKey).replace('layout_', '')}")`, { documentView: 'any' })); - @undoBatch @action + @undoBatch + @action drop = async (e: Event, de: DragManager.DropEvent) => { if (this.props.dontRegisterView || this.props.LayoutTemplateString?.includes(LinkAnchorBox.name)) return; - if (this.props.Document === CurrentUserUtils.ActiveDashboard) { - alert((e.target as any)?.closest?.("*.lm_content") ? - "You can't perform this move most likely because you don't have permission to modify the destination." : - "Linking to document tabs not yet supported. Drop link on document content."); + if (this.props.Document === Doc.ActiveDashboard) { + alert((e.target as any)?.closest?.('*.lm_content') ? "You can't perform this move most likely because you don't have permission to modify the destination." : 'Linking to document tabs not yet supported. Drop link on document content.'); return; } const linkdrag = de.complete.annoDragData ?? de.complete.linkDragData; @@ -682,20 +775,20 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps de.complete.linkDocument = DocUtils.MakeLink({ doc: linkdrag.linkSourceDoc }, { doc: dropDoc }, undefined, undefined, undefined, undefined, [de.x, de.y - 50]); } } - } + }; @undoBatch @action makeIntoPortal = async () => { const portalLink = this.allLinks.find(d => d.anchor1 === this.props.Document); if (!portalLink) { - const portal = Docs.Create.FreeformDocument([], { _width: NumCast(this.layoutDoc._width) + 10, _height: NumCast(this.layoutDoc._height), _fitWidth: true, title: StrCast(this.props.Document.title) + " [Portal]" }); - DocUtils.MakeLink({ doc: this.props.Document }, { doc: portal }, "portal to:portal from"); + const portal = Docs.Create.FreeformDocument([], { _width: NumCast(this.layoutDoc._width) + 10, _height: NumCast(this.layoutDoc._height), _fitWidth: true, title: StrCast(this.props.Document.title) + ' [Portal]' }); + DocUtils.MakeLink({ doc: this.props.Document }, { doc: portal }, 'portal to:portal from'); } - this.Document.followLinkLocation = "inPlace"; + this.Document.followLinkLocation = 'inPlace'; this.Document.followLinkZoom = true; this.Document._isLinkButton = true; - } + }; @action onContextMenu = (e?: React.MouseEvent, pageX?: number, pageY?: number) => { @@ -707,7 +800,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps } // the touch onContextMenu is button 0, the pointer onContextMenu is button 2 if (e) { - if (e.button === 0 && !e.ctrlKey || e.isDefaultPrevented()) { + if ((e.button === 0 && !e.ctrlKey) || e.isDefaultPrevented()) { e.preventDefault(); return; } @@ -715,7 +808,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps e.stopPropagation(); e.persist(); - if (!navigator.userAgent.includes("Mozilla") && (Math.abs(this._downX - e?.clientX) > 3 || Math.abs(this._downY - e?.clientY) > 3)) { + if (!navigator.userAgent.includes('Mozilla') && (Math.abs(this._downX - e?.clientX) > 3 || Math.abs(this._downY - e?.clientY) > 3)) { return; } } @@ -724,17 +817,17 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps if (!cm || (e as any)?.nativeEvent?.SchemaHandled) return; if (e && !(e.nativeEvent as any).dash) { - const onDisplay = () => setTimeout(() => { - DocumentViewInternal.SelectAfterContextMenu && !this.props.isSelected(true) && SelectionManager.SelectView(this.props.DocumentView(), false); // on a mac, the context menu is triggered on mouse down, but a YouTube video becaomes interactive when selected which means that the context menu won't show up. by delaying the selection until hopefully after the pointer up, the context menu will appear. + const onDisplay = () => setTimeout(() => { - const ele = document.elementFromPoint(e.clientX, e.clientY); - simulateMouseClick(ele, e.clientX, e.clientY, e.screenX, e.screenY); + DocumentViewInternal.SelectAfterContextMenu && !this.props.isSelected(true) && SelectionManager.SelectView(this.props.DocumentView(), false); // on a mac, the context menu is triggered on mouse down, but a YouTube video becaomes interactive when selected which means that the context menu won't show up. by delaying the selection until hopefully after the pointer up, the context menu will appear. + setTimeout(() => { + const ele = document.elementFromPoint(e.clientX, e.clientY); + simulateMouseClick(ele, e.clientX, e.clientY, e.screenX, e.screenY); + }); }); - }); - if (navigator.userAgent.includes("Macintosh")) { + if (navigator.userAgent.includes('Macintosh')) { cm.displayMenu((e?.pageX || pageX || 0) - 15, (e?.pageY || pageY || 0) - 15, undefined, undefined, onDisplay); - } - else { + } else { onDisplay(); } return; @@ -742,232 +835,291 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps const customScripts = Cast(this.props.Document.contextMenuScripts, listSpec(ScriptField), []); StrListCast(this.Document.contextMenuLabels).forEach((label, i) => - cm.addItem({ description: label, event: () => customScripts[i]?.script.run({ documentView: this, this: this.layoutDoc, scriptContext: this.props.scriptContext, self: this.rootDoc }), icon: "sticky-note" })); - this.props.contextMenuItems?.().forEach(item => - item.label && cm.addItem({ description: item.label, event: () => item.script.script.run({ this: this.layoutDoc, scriptContext: this.props.scriptContext, self: this.rootDoc }), icon: item.icon as IconProp })); + cm.addItem({ description: label, event: () => customScripts[i]?.script.run({ documentView: this, this: this.layoutDoc, scriptContext: this.props.scriptContext, self: this.rootDoc }), icon: 'sticky-note' }) + ); + this.props + .contextMenuItems?.() + .forEach(item => item.label && cm.addItem({ description: item.label, event: () => item.script.script.run({ this: this.layoutDoc, scriptContext: this.props.scriptContext, self: this.rootDoc }), icon: item.icon as IconProp })); if (!this.props.Document.isFolder) { const templateDoc = Cast(this.props.Document[StrCast(this.props.Document.layoutKey)], Doc, null); - const appearance = cm.findByDescription("UI Controls..."); - const appearanceItems: ContextMenuProps[] = appearance && "subitems" in appearance ? appearance.subitems : []; - !Doc.noviceMode && templateDoc && appearanceItems.push({ description: "Open Template ", event: () => this.props.addDocTab(templateDoc, "add:right"), icon: "eye" }); - !Doc.noviceMode && appearanceItems.push({ - description: "Add a Field", event: () => { - const alias = Doc.MakeAlias(this.rootDoc); - alias.layout = FormattedTextBox.LayoutString("newfield"); - alias.title = "newfield"; - alias._height = 35; - alias._width = 100; - alias.syncLayoutFieldWithTitle = true; - alias.x = NumCast(this.rootDoc.x) + NumCast(this.rootDoc.width); - alias.y = NumCast(this.rootDoc.y); - this.props.addDocument?.(alias); - }, icon: "eye" - }); - DocListCast(this.Document.links).length && appearanceItems.splice(0, 0, { description: `${this.layoutDoc.hideLinkButton ? "Show" : "Hide"} Link Button`, event: action(() => this.layoutDoc.hideLinkButton = !this.layoutDoc.hideLinkButton), icon: "eye" }); - !appearance && cm.addItem({ description: "UI Controls...", subitems: appearanceItems, icon: "compass" }); + const appearance = cm.findByDescription('UI Controls...'); + const appearanceItems: ContextMenuProps[] = appearance && 'subitems' in appearance ? appearance.subitems : []; + !Doc.noviceMode && templateDoc && appearanceItems.push({ description: 'Open Template ', event: () => this.props.addDocTab(templateDoc, 'add:right'), icon: 'eye' }); + !Doc.noviceMode && + appearanceItems.push({ + description: 'Add a Field', + event: () => { + const alias = Doc.MakeAlias(this.rootDoc); + alias.layout = FormattedTextBox.LayoutString('newfield'); + alias.title = 'newfield'; + alias._height = 35; + alias._width = 100; + alias.syncLayoutFieldWithTitle = true; + alias.x = NumCast(this.rootDoc.x) + NumCast(this.rootDoc.width); + alias.y = NumCast(this.rootDoc.y); + this.props.addDocument?.(alias); + }, + icon: 'eye', + }); + DocListCast(this.Document.links).length && + appearanceItems.splice(0, 0, { description: `${this.layoutDoc.hideLinkButton ? 'Show' : 'Hide'} Link Button`, event: action(() => (this.layoutDoc.hideLinkButton = !this.layoutDoc.hideLinkButton)), icon: 'eye' }); + !appearance && cm.addItem({ description: 'UI Controls...', subitems: appearanceItems, icon: 'compass' }); if (!Doc.IsSystem(this.rootDoc) && this.rootDoc._viewType !== CollectionViewType.Docking && this.props.ContainingCollectionDoc?._viewType !== CollectionViewType.Tree) { - !Doc.noviceMode && appearanceItems.splice(0, 0, { description: `${!this.layoutDoc._showAudio ? "Show" : "Hide"} Audio Button`, event: action(() => this.layoutDoc._showAudio = !this.layoutDoc._showAudio), icon: "microphone" }); - const existingOnClick = cm.findByDescription("OnClick..."); - const onClicks: ContextMenuProps[] = existingOnClick && "subitems" in existingOnClick ? existingOnClick.subitems : []; + !Doc.noviceMode && appearanceItems.splice(0, 0, { description: `${!this.layoutDoc._showAudio ? 'Show' : 'Hide'} Audio Button`, event: action(() => (this.layoutDoc._showAudio = !this.layoutDoc._showAudio)), icon: 'microphone' }); + const existingOnClick = cm.findByDescription('OnClick...'); + const onClicks: ContextMenuProps[] = existingOnClick && 'subitems' in existingOnClick ? existingOnClick.subitems : []; - const zorders = cm.findByDescription("ZOrder..."); - const zorderItems: ContextMenuProps[] = zorders && "subitems" in zorders ? zorders.subitems : []; + const zorders = cm.findByDescription('ZOrder...'); + const zorderItems: ContextMenuProps[] = zorders && 'subitems' in zorders ? zorders.subitems : []; if (this.props.bringToFront !== emptyFunction) { - zorderItems.push({ description: "Bring to Front", event: () => SelectionManager.Views().forEach(dv => dv.props.bringToFront(dv.rootDoc, false)), icon: "expand-arrows-alt" }); - zorderItems.push({ description: "Send to Back", event: () => SelectionManager.Views().forEach(dv => dv.props.bringToFront(dv.rootDoc, true)), icon: "expand-arrows-alt" }); - zorderItems.push({ description: this.rootDoc._raiseWhenDragged !== false ? "Keep ZIndex when dragged" : "Allow ZIndex to change when dragged", event: undoBatch(action(() => this.rootDoc._raiseWhenDragged = this.rootDoc._raiseWhenDragged === undefined ? false : undefined)), icon: "expand-arrows-alt" }); + zorderItems.push({ description: 'Bring to Front', event: () => SelectionManager.Views().forEach(dv => dv.props.bringToFront(dv.rootDoc, false)), icon: 'expand-arrows-alt' }); + zorderItems.push({ description: 'Send to Back', event: () => SelectionManager.Views().forEach(dv => dv.props.bringToFront(dv.rootDoc, true)), icon: 'expand-arrows-alt' }); + zorderItems.push({ + description: this.rootDoc._raiseWhenDragged !== false ? 'Keep ZIndex when dragged' : 'Allow ZIndex to change when dragged', + event: undoBatch(action(() => (this.rootDoc._raiseWhenDragged = this.rootDoc._raiseWhenDragged === undefined ? false : undefined))), + icon: 'expand-arrows-alt', + }); } - !zorders && cm.addItem({ description: "ZOrder...", noexpand: true, subitems: zorderItems, icon: "compass" }); + !zorders && cm.addItem({ description: 'ZOrder...', noexpand: true, subitems: zorderItems, icon: 'compass' }); - !Doc.noviceMode && onClicks.push({ description: "Enter Portal", event: this.makeIntoPortal, icon: "window-restore" }); - !Doc.noviceMode && onClicks.push({ description: "Toggle Detail", event: this.setToggleDetail, icon: "concierge-bell" }); - this.props.CollectionFreeFormDocumentView && onClicks.push({ description: (this.Document.followLinkZoom ? "Don't" : "") + " zoom following link", event: () => this.Document.followLinkZoom = !this.Document.followLinkZoom, icon: this.Document.ignoreClick ? "unlock" : "lock" }); + !Doc.noviceMode && onClicks.push({ description: 'Enter Portal', event: this.makeIntoPortal, icon: 'window-restore' }); + !Doc.noviceMode && onClicks.push({ description: 'Toggle Detail', event: this.setToggleDetail, icon: 'concierge-bell' }); + this.props.CollectionFreeFormDocumentView && + onClicks.push({ + description: (this.Document.followLinkZoom ? "Don't" : '') + ' zoom following link', + event: () => (this.Document.followLinkZoom = !this.Document.followLinkZoom), + icon: this.Document.ignoreClick ? 'unlock' : 'lock', + }); if (!this.Document.annotationOn) { - const options = cm.findByDescription("Options..."); - const optionItems: ContextMenuProps[] = options && "subitems" in options ? options.subitems : []; - !options && cm.addItem({ description: "Options...", subitems: optionItems, icon: "compass" }); - - onClicks.push({ description: this.Document.ignoreClick ? "Select" : "Do Nothing", event: () => this.Document.ignoreClick = !this.Document.ignoreClick, icon: this.Document.ignoreClick ? "unlock" : "lock" }); - onClicks.push({ description: this.Document.isLinkButton ? "Remove Follow Behavior" : "Follow Link in Place", event: () => this.toggleFollowLink("inPlace", true, false), icon: "link" }); - !this.Document.isLinkButton && onClicks.push({ description: "Follow Link on Right", event: () => this.toggleFollowLink("add:right", false, false), icon: "link" }); - onClicks.push({ description: this.Document.isLinkButton || this.onClickHandler ? "Remove Click Behavior" : "Follow Link", event: () => this.toggleFollowLink(undefined, false, false), icon: "link" }); - onClicks.push({ description: (this.Document.isPushpin ? "Remove" : "Make") + " Pushpin", event: () => this.toggleFollowLink(undefined, false, true), icon: "map-pin" }); - 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...", addDivider: true, noexpand: true, subitems: onClicks, icon: "mouse-pointer" }); + const options = cm.findByDescription('Options...'); + const optionItems: ContextMenuProps[] = options && 'subitems' in options ? options.subitems : []; + !options && cm.addItem({ description: 'Options...', subitems: optionItems, icon: 'compass' }); + + onClicks.push({ description: this.Document.ignoreClick ? 'Select' : 'Do Nothing', event: () => (this.Document.ignoreClick = !this.Document.ignoreClick), icon: this.Document.ignoreClick ? 'unlock' : 'lock' }); + onClicks.push({ description: this.Document.isLinkButton ? 'Remove Follow Behavior' : 'Follow Link in Place', event: () => this.toggleFollowLink('inPlace', true, false), icon: 'link' }); + !this.Document.isLinkButton && onClicks.push({ description: 'Follow Link on Right', event: () => this.toggleFollowLink('add:right', false, false), icon: 'link' }); + onClicks.push({ description: this.Document.isLinkButton || this.onClickHandler ? 'Remove Click Behavior' : 'Follow Link', event: () => this.toggleFollowLink(undefined, false, false), icon: 'link' }); + onClicks.push({ description: (this.Document.isPushpin ? 'Remove' : 'Make') + ' Pushpin', event: () => this.toggleFollowLink(undefined, false, true), icon: 'map-pin' }); + 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...', addDivider: true, noexpand: true, subitems: onClicks, icon: 'mouse-pointer' }); } else if (DocListCast(this.Document.links).length) { - onClicks.push({ description: "Select on Click", event: () => this.selectOnClick(), icon: "link" }); - onClicks.push({ description: "Follow Link on Click", event: () => this.followLinkOnClick(undefined, false), icon: "link" }); - onClicks.push({ description: "Toggle Link Target on Click", event: () => this.toggleTargetOnClick(), icon: "map-pin" }); - !existingOnClick && cm.addItem({ description: "OnClick...", addDivider: true, subitems: onClicks, icon: "mouse-pointer" }); + onClicks.push({ description: 'Select on Click', event: () => this.selectOnClick(), icon: 'link' }); + onClicks.push({ description: 'Follow Link on Click', event: () => this.followLinkOnClick(undefined, false), icon: 'link' }); + onClicks.push({ description: 'Toggle Link Target on Click', event: () => this.toggleTargetOnClick(), icon: 'map-pin' }); + !existingOnClick && cm.addItem({ description: 'OnClick...', addDivider: true, subitems: onClicks, icon: 'mouse-pointer' }); } } const funcs: ContextMenuProps[] = []; if (!Doc.noviceMode && this.layoutDoc.onDragStart) { - funcs.push({ description: "Drag an Alias", icon: "edit", event: () => this.Document.dragFactory && (this.layoutDoc.onDragStart = ScriptField.MakeFunction('getAlias(this.dragFactory)')) }); - funcs.push({ description: "Drag a Copy", icon: "edit", event: () => this.Document.dragFactory && (this.layoutDoc.onDragStart = ScriptField.MakeFunction('getCopy(this.dragFactory, true)')) }); - funcs.push({ description: "Drag Document", icon: "edit", event: () => this.layoutDoc.onDragStart = undefined }); - cm.addItem({ description: "OnDrag...", noexpand: true, subitems: funcs, icon: "asterisk" }); + funcs.push({ description: 'Drag an Alias', icon: 'edit', event: () => this.Document.dragFactory && (this.layoutDoc.onDragStart = ScriptField.MakeFunction('getAlias(this.dragFactory)')) }); + funcs.push({ description: 'Drag a Copy', icon: 'edit', event: () => this.Document.dragFactory && (this.layoutDoc.onDragStart = ScriptField.MakeFunction('getCopy(this.dragFactory, true)')) }); + funcs.push({ description: 'Drag Document', icon: 'edit', event: () => (this.layoutDoc.onDragStart = undefined) }); + cm.addItem({ description: 'OnDrag...', noexpand: true, subitems: funcs, icon: 'asterisk' }); } - const more = cm.findByDescription("More..."); - const moreItems = more && "subitems" in more ? more.subitems : []; + const more = cm.findByDescription('More...'); + const moreItems = more && 'subitems' in more ? more.subitems : []; if (!Doc.IsSystem(this.rootDoc)) { - (this.rootDoc._viewType !== CollectionViewType.Docking || !Doc.noviceMode) && moreItems.push({ description: "Share", event: () => SharingManager.Instance.open(this.props.DocumentView()), icon: "users" }); + (this.rootDoc._viewType !== CollectionViewType.Docking || !Doc.noviceMode) && moreItems.push({ description: 'Share', event: () => SharingManager.Instance.open(this.props.DocumentView()), icon: 'users' }); if (!Doc.noviceMode) { - moreItems.push({ description: "Make View of Metadata Field", event: () => Doc.MakeMetadataFieldTemplate(this.props.Document, this.props.DataDoc), icon: "concierge-bell" }); - moreItems.push({ description: `${this.Document._chromeHidden ? "Show" : "Hide"} Chrome`, event: () => this.Document._chromeHidden = !this.Document._chromeHidden, icon: "project-diagram" }); + moreItems.push({ description: 'Make View of Metadata Field', event: () => Doc.MakeMetadataFieldTemplate(this.props.Document, this.props.DataDoc), icon: 'concierge-bell' }); + moreItems.push({ description: `${this.Document._chromeHidden ? 'Show' : 'Hide'} Chrome`, event: () => (this.Document._chromeHidden = !this.Document._chromeHidden), icon: 'project-diagram' }); if (Cast(Doc.GetProto(this.props.Document).data, listSpec(Doc))) { - moreItems.push({ description: "Export to Google Photos Album", event: () => GooglePhotos.Export.CollectionToAlbum({ collection: this.props.Document }).then(console.log), icon: "caret-square-right" }); - moreItems.push({ description: "Tag Child Images via Google Photos", event: () => GooglePhotos.Query.TagChildImages(this.props.Document), icon: "caret-square-right" }); - moreItems.push({ description: "Write Back Link to Album", event: () => GooglePhotos.Transactions.AddTextEnrichment(this.props.Document), icon: "caret-square-right" }); + moreItems.push({ description: 'Export to Google Photos Album', event: () => GooglePhotos.Export.CollectionToAlbum({ collection: this.props.Document }).then(console.log), icon: 'caret-square-right' }); + moreItems.push({ description: 'Tag Child Images via Google Photos', event: () => GooglePhotos.Query.TagChildImages(this.props.Document), icon: 'caret-square-right' }); + moreItems.push({ description: 'Write Back Link to Album', event: () => GooglePhotos.Transactions.AddTextEnrichment(this.props.Document), icon: 'caret-square-right' }); } - moreItems.push({ description: "Copy ID", event: () => Utils.CopyText(Doc.globalServerPath(this.props.Document)), icon: "fingerprint" }); + moreItems.push({ description: 'Copy ID', event: () => Utils.CopyText(Doc.globalServerPath(this.props.Document)), icon: 'fingerprint' }); } } - if (this.props.removeDocument && !Doc.IsSystem(this.rootDoc) && CurrentUserUtils.ActiveDashboard !== this.props.Document) { // need option to gray out menu items ... preferably with a '?' that explains why they're grayed out (eg., no permissions) - moreItems.push({ description: "Close", event: this.deleteClicked, icon: "times" }); + if (this.props.removeDocument && !Doc.IsSystem(this.rootDoc) && Doc.ActiveDashboard !== this.props.Document) { + // need option to gray out menu items ... preferably with a '?' that explains why they're grayed out (eg., no permissions) + moreItems.push({ description: 'Close', event: this.deleteClicked, icon: 'times' }); } - !more && moreItems.length && cm.addItem({ description: "More...", subitems: moreItems, icon: "compass" }); - - const help = cm.findByDescription("Help..."); - const helpItems: ContextMenuProps[] = help && "subitems" in help ? help.subitems : []; - helpItems.push({ description: "Show Fields ", event: () => this.props.addDocTab(Docs.Create.KVPDocument(this.props.Document, { _width: 300, _height: 300 }), "add:right"), icon: "layer-group" }); - !Doc.noviceMode && helpItems.push({ description: "Text Shortcuts Ctrl+/", event: () => this.props.addDocTab(Docs.Create.PdfDocument("/assets/cheat-sheet.pdf", { _width: 300, _height: 300 }), "add:right"), icon: "keyboard" }); - !Doc.noviceMode && helpItems.push({ description: "Print Document in Console", event: () => console.log(this.props.Document), icon: "hand-point-right" }); - !Doc.noviceMode && helpItems.push({ description: "Print DataDoc in Console", event: () => console.log(this.props.Document[DataSym]), icon: "hand-point-right" }); - cm.addItem({ description: "Help...", noexpand: true, subitems: helpItems, icon: "question" }); + !more && moreItems.length && cm.addItem({ description: 'More...', subitems: moreItems, icon: 'compass' }); + + const help = cm.findByDescription('Help...'); + const helpItems: ContextMenuProps[] = help && 'subitems' in help ? help.subitems : []; + helpItems.push({ description: 'Show Fields ', event: () => this.props.addDocTab(Docs.Create.KVPDocument(this.props.Document, { _width: 300, _height: 300 }), 'add:right'), icon: 'layer-group' }); + !Doc.noviceMode && helpItems.push({ description: 'Text Shortcuts Ctrl+/', event: () => this.props.addDocTab(Docs.Create.PdfDocument('/assets/cheat-sheet.pdf', { _width: 300, _height: 300 }), 'add:right'), icon: 'keyboard' }); + !Doc.noviceMode && helpItems.push({ description: 'Print Document in Console', event: () => console.log(this.props.Document), icon: 'hand-point-right' }); + !Doc.noviceMode && helpItems.push({ description: 'Print DataDoc in Console', event: () => console.log(this.props.Document[DataSym]), icon: 'hand-point-right' }); + cm.addItem({ description: 'Help...', noexpand: true, subitems: helpItems, icon: 'question' }); } if (!this.topMost) e?.stopPropagation(); // DocumentViews should stop propagation of this event cm.displayMenu((e?.pageX || pageX || 0) - 15, (e?.pageY || pageY || 0) - 15); - } + }; collectionFilters = () => StrListCast(this.props.Document._docFilters); collectionRangeDocFilters = () => StrListCast(this.props.Document._docRangeFilters); @computed get showFilterIcon() { - return this.collectionFilters().length || this.collectionRangeDocFilters().length ? "hasFilter" : - this.props.docFilters?.().filter(f => Utils.IsRecursiveFilter(f)).length || this.props.docRangeFilters().length ? "inheritsFilter" : undefined; + return this.collectionFilters().length || this.collectionRangeDocFilters().length ? 'hasFilter' : this.props.docFilters?.().filter(f => Utils.IsRecursiveFilter(f)).length || this.props.docRangeFilters().length ? 'inheritsFilter' : undefined; } rootSelected = (outsideReaction?: boolean) => this.props.isSelected(outsideReaction) || (this.props.Document.rootDocument && this.props.rootSelected?.(outsideReaction)) || false; panelHeight = () => this.props.PanelHeight() - this.headerMargin; screenToLocal = () => this.props.ScreenToLocalTransform().translate(0, -this.headerMargin); contentScaling = () => this.ContentScale; onClickFunc = () => this.onClickHandler; - setHeight = (height: number) => this.layoutDoc._height = height; - setContentView = action((view: { getAnchor?: () => Doc, forward?: () => boolean, back?: () => boolean }) => this._componentView = view); + setHeight = (height: number) => (this.layoutDoc._height = height); + setContentView = action((view: { getAnchor?: () => Doc; forward?: () => boolean; back?: () => boolean }) => (this._componentView = view)); isContentActive = (outsideReaction?: boolean) => { - return this.props.isContentActive() === false ? false : ( - CurrentUserUtils.ActiveTool !== InkTool.None || - SnappingManager.GetIsDragging() || - this.rootSelected() || - this.props.Document.forceActive || - this.props.isSelected(outsideReaction) || - this._componentView?.isAnyChildContentActive?.() || - this.props.isContentActive()) ? true : undefined; - } + return this.props.isContentActive() === false + ? false + : Doc.ActiveTool !== InkTool.None || + SnappingManager.GetIsDragging() || + this.rootSelected() || + this.props.Document.forceActive || + this.props.isSelected(outsideReaction) || + this._componentView?.isAnyChildContentActive?.() || + this.props.isContentActive() + ? true + : undefined; + }; @observable _retryThumb = 1; thumbShown = () => { - return !this.props.isSelected() && LightboxView.LightboxDoc !== this.rootDoc && this.thumb && + return !this.props.isSelected() && + LightboxView.LightboxDoc !== this.rootDoc && + this.thumb && !Doc.AreProtosEqual(DocumentLinksButton.StartLink, this.rootDoc) && !Doc.isBrushedHighlightedDegree(this.props.Document) && - !this._componentView?.isAnyChildContentActive?.() ? true : false; - } + !this._componentView?.isAnyChildContentActive?.() + ? true + : false; + }; @computed get contents() { TraceMobx(); - const audioView = !this.layoutDoc._showAudio ? (null) : - <div className="documentView-audioBackground" onPointerDown={this.recordAudioAnnotation} onPointerEnter={this.onPointerEnter} > - <FontAwesomeIcon className="documentView-audioFont" - style={{ color: [DocListCast(this.dataDoc[this.LayoutFieldKey + "-audioAnnotations"]).length ? "blue" : "gray", "green", "red"][this._mediaState] }} - icon={!DocListCast(this.dataDoc[this.LayoutFieldKey + "-audioAnnotations"]).length ? "microphone" : "file-audio"} size="sm" /> - </div>; - - return <div className="documentView-contentsView" - style={{ - pointerEvents: this.props.pointerEvents?.() as any ?? this.rootDoc.layoutKey === "layout_icon" ? "none" : - this.props.contentPointerEvents as any ? this.props.contentPointerEvents as any : - this.rootDoc.type !== DocumentType.INK && this.isContentActive() ? "all" : - "none", - height: this.headerMargin ? `calc(100% - ${this.headerMargin}px)` : undefined, - }}> - {!this._retryThumb || !this.thumbShown() ? (null) : - <img style={{ background: "white", top: 0, position: "relative" }} src={this.thumb} // + '?d=' + (new Date()).getTime()} - width={this.props.PanelWidth()} height={this.props.PanelHeight()} - onError={(e: any) => { - setTimeout(action(() => this._retryThumb = 0), 0); - setTimeout(action(() => this._retryThumb = 1), 150); - }} />} - <DocumentContentsView key={1} - {...this.props} - docViewPath={this.props.viewPath} - thumbShown={this.thumbShown} - isHovering={this.isHovering} - setContentView={this.setContentView} - scaling={this.contentScaling} - PanelHeight={this.panelHeight} - setHeight={!this.props.suppressSetHeight ? this.setHeight : undefined} - isContentActive={this.isContentActive} - ScreenToLocalTransform={this.screenToLocal} - rootSelected={this.rootSelected} - onClick={this.onClickFunc} - focus={this.focus} - layoutKey={this.finalLayoutKey} /> - {this.layoutDoc.hideAllLinks ? (null) : this.allLinkEndpoints} - {(!this.props.isSelected() && !this._isHovering) || this.hideLinkButton || this.props.renderDepth === -1 || SnappingManager.GetIsDragging() ? (null) : - <DocumentLinksButton View={this.props.DocumentView()} - ContentScaling={this.props.ContentScaling} - Offset={[this.topMost ? 0 : !this.props.isSelected() ? - 15 : -30, undefined, undefined, this.topMost ? 10 : !this.props.isSelected() ? - 15 : -30]} /> - } - {audioView} - </div>; + const audioView = !this.layoutDoc._showAudio ? null : ( + <div className="documentView-audioBackground" onPointerDown={this.recordAudioAnnotation} onPointerEnter={this.onPointerEnter}> + <FontAwesomeIcon + className="documentView-audioFont" + style={{ color: [DocListCast(this.dataDoc[this.LayoutFieldKey + '-audioAnnotations']).length ? 'blue' : 'gray', 'green', 'red'][this._mediaState] }} + icon={!DocListCast(this.dataDoc[this.LayoutFieldKey + '-audioAnnotations']).length ? 'microphone' : 'file-audio'} + size="sm" + /> + </div> + ); + + return ( + <div + className="documentView-contentsView" + style={{ + pointerEvents: + (this.props.pointerEvents?.() as any) ?? this.rootDoc.layoutKey === 'layout_icon' + ? 'none' + : (this.props.contentPointerEvents as any) + ? (this.props.contentPointerEvents as any) + : this.rootDoc.type !== DocumentType.INK && this.isContentActive() + ? 'all' + : 'none', + height: this.headerMargin ? `calc(100% - ${this.headerMargin}px)` : undefined, + }}> + {!this._retryThumb || !this.thumbShown() ? null : ( + <img + style={{ background: 'white', top: 0, position: 'relative' }} + src={this.thumb} // + '?d=' + (new Date()).getTime()} + width={this.props.PanelWidth()} + height={this.props.PanelHeight()} + onError={(e: any) => { + setTimeout( + action(() => (this._retryThumb = 0)), + 0 + ); + setTimeout( + action(() => (this._retryThumb = 1)), + 150 + ); + }} + /> + )} + <DocumentContentsView + key={1} + {...this.props} + docViewPath={this.props.viewPath} + thumbShown={this.thumbShown} + isHovering={this.isHovering} + setContentView={this.setContentView} + scaling={this.contentScaling} + PanelHeight={this.panelHeight} + setHeight={!this.props.suppressSetHeight ? this.setHeight : undefined} + isContentActive={this.isContentActive} + ScreenToLocalTransform={this.screenToLocal} + rootSelected={this.rootSelected} + onClick={this.onClickFunc} + focus={this.focus} + layoutKey={this.finalLayoutKey} + /> + {this.layoutDoc.hideAllLinks ? null : this.allLinkEndpoints} + {(!this.props.isSelected() && !this._isHovering) || this.hideLinkButton || this.props.renderDepth === -1 || SnappingManager.GetIsDragging() ? null : ( + <DocumentLinksButton + View={this.props.DocumentView()} + ContentScaling={this.props.ContentScaling} + Offset={[this.topMost ? 0 : !this.props.isSelected() ? -15 : -30, undefined, undefined, this.topMost ? 10 : !this.props.isSelected() ? -15 : -30]} + /> + )} + {audioView} + </div> + ); } get indicatorIcon() { - if (this.props.Document["acl-Public"] !== SharingPermissions.None) return "globe-americas"; - else if (this.props.Document.numGroupsShared || NumCast(this.props.Document.numUsersShared, 0) > 1) return "users"; - else return "user"; + if (this.props.Document['acl-Public'] !== SharingPermissions.None) return 'globe-americas'; + else if (this.props.Document.numGroupsShared || NumCast(this.props.Document.numUsersShared, 0) > 1) return 'users'; + else return 'user'; } @undoBatch - hideLinkAnchor = (doc: Doc | Doc[]) => (doc instanceof Doc ? [doc] : doc).reduce((flg, doc) => flg && (doc.hidden = true), true) + hideLinkAnchor = (doc: Doc | Doc[]) => (doc instanceof Doc ? [doc] : doc).reduce((flg, doc) => flg && (doc.hidden = true), true); anchorPanelWidth = () => this.props.PanelWidth() || 1; anchorPanelHeight = () => this.props.PanelHeight() || 1; anchorStyleProvider = (doc: Opt<Doc>, props: Opt<DocumentViewProps>, property: string): any => { switch (property) { - case StyleProp.ShowTitle: return ""; - case StyleProp.PointerEvents: return "none"; - case StyleProp.LinkSource: return this.props.Document;// pass the LinkSource to the LinkAnchorBox - default: return this.props.styleProvider?.(doc, props, property); + case StyleProp.ShowTitle: + return ''; + case StyleProp.PointerEvents: + return 'none'; + case StyleProp.LinkSource: + return this.props.Document; // pass the LinkSource to the LinkAnchorBox + default: + return this.props.styleProvider?.(doc, props, property); } - } - // We need to use allrelatedLinks to get not just links to the document as a whole, but links to - // anchors that are not rendered as DocumentViews (marked as 'unrendered' with their 'annotationOn' set to this document). e.g., + }; + // We need to use allrelatedLinks to get not just links to the document as a whole, but links to + // anchors that are not rendered as DocumentViews (marked as 'unrendered' with their 'annotationOn' set to this document). e.g., // - PDF text regions are rendered as an Annotations without generating a DocumentView, ' // - RTF selections are rendered via Prosemirror and have a mark which contains the Document ID for the annotation link // - and links to PDF/Web docs at a certain scroll location never create an explicit view. // For each of these, we create LinkAnchorBox's on the border of the DocumentView. @computed get directLinks() { - TraceMobx(); return LinkManager.Instance.getAllRelatedLinks(this.rootDoc).filter(link => - Doc.AreProtosEqual(link.anchor1 as Doc, this.rootDoc) || - Doc.AreProtosEqual(link.anchor2 as Doc, this.rootDoc) || - ((link.anchor1 as Doc).unrendered && Doc.AreProtosEqual((link.anchor1 as Doc).annotationOn as Doc, this.rootDoc)) || - ((link.anchor2 as Doc).unrendered && Doc.AreProtosEqual((link.anchor2 as Doc).annotationOn as Doc, this.rootDoc)) + TraceMobx(); + return LinkManager.Instance.getAllRelatedLinks(this.rootDoc).filter( + link => + Doc.AreProtosEqual(link.anchor1 as Doc, this.rootDoc) || + Doc.AreProtosEqual(link.anchor2 as Doc, this.rootDoc) || + ((link.anchor1 as Doc).unrendered && Doc.AreProtosEqual((link.anchor1 as Doc).annotationOn as Doc, this.rootDoc)) || + ((link.anchor2 as Doc).unrendered && Doc.AreProtosEqual((link.anchor2 as Doc).annotationOn as Doc, this.rootDoc)) ); } - @computed get allLinks() { TraceMobx(); return LinkManager.Instance.getAllRelatedLinks(this.rootDoc); } - @computed get allLinkEndpoints() { // the small blue dots that mark the endpoints of links + @computed get allLinks() { + TraceMobx(); + return LinkManager.Instance.getAllRelatedLinks(this.rootDoc); + } + @computed get allLinkEndpoints() { + // the small blue dots that mark the endpoints of links TraceMobx(); if (this.layoutDoc.unrendered || this.props.LayoutTemplateString?.includes(LinkAnchorBox.name)) return null; - if (this.rootDoc.type=== DocumentType.PRES || this.rootDoc.type === DocumentType.LINK || this.props.dontRegisterView) return (null); + if (this.rootDoc.type === DocumentType.PRES || this.rootDoc.type === DocumentType.LINK || this.props.dontRegisterView) return null; const filtered = DocUtils.FilterDocs(this.directLinks, this.props.docFilters?.() ?? [], []).filter(d => !d.hidden); - return filtered.map((link, i) => + return filtered.map((link, i) => ( <div className="documentView-anchorCont" key={link[Id]}> - <DocumentView {...this.props} + <DocumentView + {...this.props} isContentActive={returnFalse} Document={link} PanelWidth={this.anchorPanelWidth} @@ -979,80 +1131,87 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps styleProvider={this.anchorStyleProvider} removeDocument={this.hideLinkAnchor} LayoutTemplate={undefined} - LayoutTemplateString={LinkAnchorBox.LayoutString(`anchor${Doc.LinkEndpoint(link, this.rootDoc)}`)} /> - </div >); + LayoutTemplateString={LinkAnchorBox.LayoutString(`anchor${Doc.LinkEndpoint(link, this.rootDoc)}`)} + /> + </div> + )); } @action onPointerEnter = () => { const self = this; - const audioAnnos = DocListCast(this.dataDoc[this.LayoutFieldKey + "-audioAnnotations"]); + const audioAnnos = DocListCast(this.dataDoc[this.LayoutFieldKey + '-audioAnnotations']); if (audioAnnos && audioAnnos.length && this._mediaState === 0) { const anno = audioAnnos[Math.floor(Math.random() * audioAnnos.length)]; - anno.data instanceof AudioField && new Howl({ - src: [anno.data.url.href], - format: ["mp3"], - autoplay: true, - loop: false, - volume: 0.5, - onend: function () { - runInAction(() => self._mediaState = 0); - } - }); + anno.data instanceof AudioField && + new Howl({ + src: [anno.data.url.href], + format: ['mp3'], + autoplay: true, + loop: false, + volume: 0.5, + onend: function () { + runInAction(() => (self._mediaState = 0)); + }, + }); this._mediaState = 1; } - } + }; recordAudioAnnotation = () => { let gumStream: any; let recorder: any; const self = this; - navigator.mediaDevices.getUserMedia({ - audio: true - }).then(function (stream) { - gumStream = stream; - recorder = new MediaRecorder(stream); - recorder.ondataavailable = async (e: any) => { - const [{ result }] = await Networking.UploadFilesToServer(e.data); - if (!(result instanceof Error)) { - const audioDoc = Docs.Create.AudioDocument(result.accessPaths.agnostic.client, { title: "audio test", _width: 200, _height: 32 }); - audioDoc.treeViewExpandedView = "layout"; - const audioAnnos = Cast(self.dataDoc[self.LayoutFieldKey + "-audioAnnotations"], listSpec(Doc)); - if (audioAnnos === undefined) { - self.dataDoc[self.LayoutFieldKey + "-audioAnnotations"] = new List([audioDoc]); - } else { - audioAnnos.push(audioDoc); + navigator.mediaDevices + .getUserMedia({ + audio: true, + }) + .then(function (stream) { + gumStream = stream; + recorder = new MediaRecorder(stream); + recorder.ondataavailable = async (e: any) => { + const [{ result }] = await Networking.UploadFilesToServer(e.data); + if (!(result instanceof Error)) { + const audioDoc = Docs.Create.AudioDocument(result.accessPaths.agnostic.client, { title: 'audio test', _width: 200, _height: 32 }); + audioDoc.treeViewExpandedView = 'layout'; + const audioAnnos = Cast(self.dataDoc[self.LayoutFieldKey + '-audioAnnotations'], listSpec(Doc)); + if (audioAnnos === undefined) { + self.dataDoc[self.LayoutFieldKey + '-audioAnnotations'] = new List([audioDoc]); + } else { + audioAnnos.push(audioDoc); + } } - } - }; - runInAction(() => self._mediaState = 2); - recorder.start(); - setTimeout(() => { - recorder.stop(); - runInAction(() => self._mediaState = 0); - gumStream.getAudioTracks()[0].stop(); - }, 5000); - }); - } + }; + runInAction(() => (self._mediaState = 2)); + recorder.start(); + setTimeout(() => { + recorder.stop(); + runInAction(() => (self._mediaState = 0)); + gumStream.getAudioTracks()[0].stop(); + }, 5000); + }); + }; - captionStyleProvider = (doc: Opt<Doc>, props: Opt<DocumentViewInternalProps>, property: string) => this.props?.styleProvider?.(doc, props, property + ":caption"); + captionStyleProvider = (doc: Opt<Doc>, props: Opt<DocumentViewInternalProps>, 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.ShowTitle?.split(":")[0]; - const showTitleHover = this.ShowTitle?.includes(":hover"); + const ffscale = () => this.props.DocumentView().props.CollectionFreeFormDocumentView?.().props.ScreenToLocalTransform().Scale || 1; + const showTitle = this.ShowTitle?.split(':')[0]; + const showTitleHover = this.ShowTitle?.includes(':hover'); const showCaption = !this.props.hideCaptions && this.Document._viewType !== CollectionViewType.Carousel ? StrCast(this.layoutDoc._showCaption) : undefined; - const captionView = !showCaption ? (null) : - <div className="documentView-captionWrapper" + const captionView = !showCaption ? null : ( + <div + className="documentView-captionWrapper" style={{ - pointerEvents: this.onClickHandler || this.Document.ignoreClick ? "none" : this.isContentActive() || this.props.isDocumentActive?.() ? "all" : undefined, + pointerEvents: this.onClickHandler || this.Document.ignoreClick ? 'none' : this.isContentActive() || this.props.isDocumentActive?.() ? 'all' : undefined, minWidth: 50 * ffscale(), - maxHeight: `max(100%, ${20 * ffscale()}px)` + maxHeight: `max(100%, ${20 * ffscale()}px)`, }}> - <FormattedTextBox {...OmitKeys(this.props, ['children']).omit} + <FormattedTextBox + {...OmitKeys(this.props, ['children']).omit} yPadding={10} xPadding={10} fieldKey={showCaption} - fontSize={12 * Math.max(1, 2 * ffscale() / 3)} + fontSize={12 * Math.max(1, (2 * ffscale()) / 3)} styleProvider={this.captionStyleProvider} dontRegisterView={true} noSidebar={true} @@ -1060,192 +1219,272 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps isContentActive={this.isContentActive} onClick={this.onClickFunc} /> - </div>; - const targetDoc = (showTitle?.startsWith("_") ? this.layoutDoc : this.rootDoc); - const background = StrCast(SharingManager.Instance.users.find(users => users.user.email === this.dataDoc.author)?.sharingDoc.userColor, - Doc.UserDoc().showTitle && [DocumentType.RTF, DocumentType.COL].includes(this.rootDoc.type as any) ? StrCast(Doc.SharingDoc().userColor) : "rgba(0,0,0,0.4)"); - const titleView = !showTitle ? (null) : - <div className={`documentView-titleWrapper${showTitleHover ? "-hover" : ""}`} key="title" style={{ - position: this.headerMargin ? "relative" : "absolute", - height: this.titleHeight, - color: lightOrDark(background), - background, - pointerEvents: this.onClickHandler || this.Document.ignoreClick ? "none" : this.isContentActive() || this.props.isDocumentActive?.() ? "all" : undefined, - }}> - <EditableView ref={this._titleRef} - contents={showTitle.split(";").map(field => field.trim()).map(field => targetDoc[field]?.toString()).join("\\")} - display={"block"} + </div> + ); + const targetDoc = showTitle?.startsWith('_') ? this.layoutDoc : this.rootDoc; + const background = StrCast( + SharingManager.Instance.users.find(users => users.user.email === this.dataDoc.author)?.sharingDoc.userColor, + Doc.UserDoc().showTitle && [DocumentType.RTF, DocumentType.COL].includes(this.rootDoc.type as any) ? StrCast(Doc.SharingDoc().userColor) : 'rgba(0,0,0,0.4)' + ); + const titleView = !showTitle ? null : ( + <div + className={`documentView-titleWrapper${showTitleHover ? '-hover' : ''}`} + key="title" + style={{ + position: this.headerMargin ? 'relative' : 'absolute', + height: this.titleHeight, + color: lightOrDark(background), + background, + pointerEvents: this.onClickHandler || this.Document.ignoreClick ? 'none' : this.isContentActive() || this.props.isDocumentActive?.() ? 'all' : undefined, + }}> + <EditableView + ref={this._titleRef} + contents={showTitle + .split(';') + .map(field => field.trim()) + .map(field => targetDoc[field]?.toString()) + .join('\\')} + display={'block'} fontSize={10} - GetValue={() => showTitle.split(";").length === 1 ? showTitle + "=" + Field.toString(targetDoc[showTitle.split(";")[0]] as any as Field) : "#" + showTitle} + GetValue={() => (showTitle.split(';').length === 1 ? showTitle + '=' + Field.toString(targetDoc[showTitle.split(';')[0]] as any as Field) : '#' + showTitle)} SetValue={undoBatch((input: string) => { - if (input?.startsWith("#")) { + if (input?.startsWith('#')) { if (this.props.showTitle) { this.rootDoc._showTitle = input?.substring(1) ? input.substring(1) : undefined; } else { - Doc.UserDoc().showTitle = input?.substring(1) ? input.substring(1) : "creationDate"; + Doc.UserDoc().showTitle = input?.substring(1) ? input.substring(1) : 'creationDate'; } } else { - var value = input.replace(new RegExp(showTitle + "="), "") as string | number; - if (showTitle !== "title" && Number(value).toString() === value) value = Number(value); - if (showTitle.includes("Date") || showTitle === "author") return true; + var value = input.replace(new RegExp(showTitle + '='), '') as string | number; + if (showTitle !== 'title' && Number(value).toString() === value) value = Number(value); + if (showTitle.includes('Date') || showTitle === 'author') return true; Doc.SetInPlace(targetDoc, showTitle, value, true); } return true; })} /> - </div>; - return this.props.hideTitle || (!showTitle && !showCaption) ? - this.contents : - <div className="documentView-styleWrapper" > - {!this.headerMargin ? <> {this.contents} {titleView} </> : <> {titleView} {this.contents} </>} + </div> + ); + return this.props.hideTitle || (!showTitle && !showCaption) ? ( + this.contents + ) : ( + <div className="documentView-styleWrapper"> + {!this.headerMargin ? ( + <> + {' '} + {this.contents} {titleView}{' '} + </> + ) : ( + <> + {' '} + {titleView} {this.contents}{' '} + </> + )} {captionView} - </div>; + </div> + ); } isHovering = () => this._isHovering; @observable _isHovering = false; - @observable _: string = ""; + @observable _: string = ''; @computed get renderDoc() { TraceMobx(); - const thumb = ImageCast(this.layoutDoc["thumb-frozen"], ImageCast(this.layoutDoc.thumb))?.url.href.replace(".png", "_m.png"); + const thumb = ImageCast(this.layoutDoc['thumb-frozen'], ImageCast(this.layoutDoc.thumb))?.url.href.replace('.png', '_m.png'); const isButton = this.props.Document.type === DocumentType.FONTICON; if (!(this.props.Document instanceof Doc) || GetEffectiveAcl(this.props.Document[DataSym]) === AclPrivate || (this.hidden && !this.props.treeViewDoc)) return null; - return this.docContents ?? - <div className={`documentView-node${this.topMost ? "-topmost" : ""}`} - id={this.props.Document[Id]} - onPointerEnter={action(() => this._isHovering = true)} - onPointerLeave={action(() => this._isHovering = false)} - style={{ - background: isButton || thumb ? undefined : this.backgroundColor, - opacity: this.opacity, - color: StrCast(this.layoutDoc.color, "inherit"), - fontFamily: StrCast(this.Document._fontFamily, "inherit"), - fontSize: Cast(this.Document._fontSize, "string", null), - transform: this._animateScalingTo ? `scale(${this._animateScalingTo})` : undefined, - transition: !this._animateScalingTo ? StrCast(this.Document.dataTransition) : `transform ${this._animateScaleTime / 1000}s ease-${this._animateScalingTo < 1 ? "in" : "out"}`, - }}> - - {this.innards} - {this.onClickHandler && this.props.ContainingCollectionView?.props.Document._viewType === CollectionViewType.Time ? <div className="documentView-contentBlocker" /> : (null)} - {this.widgetDecorations ?? null} - </div>; + return ( + this.docContents ?? ( + <div + className={`documentView-node${this.topMost ? '-topmost' : ''}`} + id={this.props.Document[Id]} + onPointerEnter={action(() => (this._isHovering = true))} + onPointerLeave={action(() => (this._isHovering = false))} + style={{ + background: isButton || thumb ? undefined : this.backgroundColor, + opacity: this.opacity, + color: StrCast(this.layoutDoc.color, 'inherit'), + fontFamily: StrCast(this.Document._fontFamily, 'inherit'), + fontSize: Cast(this.Document._fontSize, 'string', null), + transform: this._animateScalingTo ? `scale(${this._animateScalingTo})` : undefined, + transition: !this._animateScalingTo ? StrCast(this.Document.dataTransition) : `transform ${this._animateScaleTime / 1000}s ease-${this._animateScalingTo < 1 ? 'in' : 'out'}`, + }}> + {this.innards} + {this.onClickHandler && this.props.ContainingCollectionView?.props.Document._viewType === CollectionViewType.Time ? <div className="documentView-contentBlocker" /> : null} + {this.widgetDecorations ?? null} + </div> + ) + ); } render() { TraceMobx(); const highlightIndex = this.props.LayoutTemplateString ? (Doc.IsHighlighted(this.props.Document) ? 6 : Doc.DocBrushStatus.unbrushed) : Doc.isBrushedHighlightedDegree(this.props.Document); // bcz: Argh!! need to identify a tree view doc better than a LayoutTemlatString - const highlightColor = ["transparent", "rgb(68, 118, 247)", "rgb(68, 118, 247)", "orange", "lightBlue"][highlightIndex]; - const highlightStyle = ["solid", "dashed", "solid", "solid", "solid"][highlightIndex]; + const highlightColor = ['transparent', 'rgb(68, 118, 247)', 'rgb(68, 118, 247)', 'orange', 'lightBlue'][highlightIndex]; + const highlightStyle = ['solid', 'dashed', 'solid', 'solid', 'solid'][highlightIndex]; const excludeTypes = !this.props.treeViewDoc ? [DocumentType.FONTICON, DocumentType.INK] : [DocumentType.FONTICON]; let highlighting = !this.props.disableDocBrushing && highlightIndex && !excludeTypes.includes(this.layoutDoc.type as any) && this.layoutDoc._viewType !== CollectionViewType.Linear; - highlighting = highlighting && this.props.focus !== emptyFunction && this.layoutDoc.title !== "[pres element template]"; // bcz: hack to turn off highlighting onsidebar panel documents. need to flag a document as not highlightable in a more direct way + highlighting = highlighting && this.props.focus !== emptyFunction && this.layoutDoc.title !== '[pres element template]'; // bcz: hack to turn off highlighting onsidebar panel documents. need to flag a document as not highlightable in a more direct way const borderPath = this.props.styleProvider?.(this.props.Document, this.props, StyleProp.BorderPath) || { path: undefined }; const internal = PresBox.EffectsProvider(this.layoutDoc, this.renderDoc) || this.renderDoc; - const boxShadow = this.props.treeViewDoc ? null : highlighting && this.borderRounding && highlightStyle !== "dashed" ? `0 0 0 ${highlightIndex}px ${highlightColor}` : - this.boxShadow || (this.props.Document.isTemplateForField ? "black 0.2vw 0.2vw 0.8vw" : undefined); - - // Return surrounding highlight - return <div className={`${DocumentView.ROOT_DIV} docView-hack`} ref={this._mainCont} - onContextMenu={this.onContextMenu} - onKeyDown={this.onKeyDown} - onPointerDown={this.onPointerDown} - onClick={this.onClick} - onPointerEnter={action(e => !SnappingManager.GetIsDragging() && Doc.BrushDoc(this.props.Document))} - onPointerLeave={action(e => !hasDescendantTarget(e.nativeEvent.x, e.nativeEvent.y, this.ContentDiv) && Doc.UnBrushDoc(this.props.Document))} - style={{ - display: this.hidden ? "inline" : undefined, - borderRadius: this.borderRounding, - pointerEvents: this.pointerEvents, - outline: highlighting && !this.borderRounding ? `${highlightColor} ${highlightStyle} ${highlightIndex}px` : "solid 0px", - border: highlighting && this.borderRounding && highlightStyle === "dashed" ? `${highlightStyle} ${highlightColor} ${highlightIndex}px` : undefined, - boxShadow, - clipPath: borderPath.path ? `path('${borderPath.path}')` : undefined - }}> - {!borderPath.path ? internal : - <> - {/* <div style={{ clipPath: `path('${borderPath.fill}')` }}> + const boxShadow = this.props.treeViewDoc + ? null + : highlighting && this.borderRounding && highlightStyle !== 'dashed' + ? `0 0 0 ${highlightIndex}px ${highlightColor}` + : this.boxShadow || (this.props.Document.isTemplateForField ? 'black 0.2vw 0.2vw 0.8vw' : undefined); + + // Return surrounding highlight + return ( + <div + className={`${DocumentView.ROOT_DIV} docView-hack`} + ref={this._mainCont} + onContextMenu={this.onContextMenu} + onKeyDown={this.onKeyDown} + onPointerDown={this.onPointerDown} + onClick={this.onClick} + onPointerEnter={action(e => !SnappingManager.GetIsDragging() && Doc.BrushDoc(this.props.Document))} + onPointerLeave={action(e => !hasDescendantTarget(e.nativeEvent.x, e.nativeEvent.y, this.ContentDiv) && Doc.UnBrushDoc(this.props.Document))} + style={{ + display: this.hidden ? 'inline' : undefined, + borderRadius: this.borderRounding, + pointerEvents: this.pointerEvents, + outline: highlighting && !this.borderRounding ? `${highlightColor} ${highlightStyle} ${highlightIndex}px` : 'solid 0px', + border: highlighting && this.borderRounding && highlightStyle === 'dashed' ? `${highlightStyle} ${highlightColor} ${highlightIndex}px` : undefined, + boxShadow, + clipPath: borderPath.path ? `path('${borderPath.path}')` : undefined, + }}> + {!borderPath.path ? ( + internal + ) : ( + <> + {/* <div style={{ clipPath: `path('${borderPath.fill}')` }}> {internal} </div> */} - {internal} - <div key="border2" className="documentView-customBorder" style={{ pointerEvents: "none" }} > - <svg style={{ overflow: "visible" }} viewBox={`0 0 ${this.props.PanelWidth()} ${this.props.PanelHeight()}`}> - <path d={borderPath.path} style={{ stroke: "black", fill: "transparent", strokeWidth: borderPath.width }} /> - </svg> - </div> - </> - } - {this.showFilterIcon ? - <FontAwesomeIcon icon={"filter"} size="lg" - style={{ position: 'absolute', top: '1%', right: '1%', cursor: "pointer", padding: 1, color: this.showFilterIcon === "hasFilter" ? '#18c718bd' : "orange", zIndex: 1 }} - onPointerDown={action(e => { this.props.select(false); CurrentUserUtils.propertiesWidth = 250; e.stopPropagation(); })} - /> - : (null)} - </div>; + {internal} + <div key="border2" className="documentView-customBorder" style={{ pointerEvents: 'none' }}> + <svg style={{ overflow: 'visible' }} viewBox={`0 0 ${this.props.PanelWidth()} ${this.props.PanelHeight()}`}> + <path d={borderPath.path} style={{ stroke: 'black', fill: 'transparent', strokeWidth: borderPath.width }} /> + </svg> + </div> + </> + )} + {this.showFilterIcon ? ( + <FontAwesomeIcon + icon={'filter'} + size="lg" + style={{ position: 'absolute', top: '1%', right: '1%', cursor: 'pointer', padding: 1, color: this.showFilterIcon === 'hasFilter' ? '#18c718bd' : 'orange', zIndex: 1 }} + onPointerDown={action(e => { + this.props.select(false); + SettingsManager.propertiesWidth = 250; + e.stopPropagation(); + })} + /> + ) : null} + </div> + ); } } @observer export class DocumentView extends React.Component<DocumentViewProps> { - public static ROOT_DIV = "documentView-effectsWrapper"; - public get displayName() { return "DocumentView(" + this.props.Document?.title + ")"; } // this makes mobx trace() statements more descriptive + public static ROOT_DIV = 'documentView-effectsWrapper'; + public get displayName() { + return 'DocumentView(' + this.props.Document?.title + ')'; + } // this makes mobx trace() statements more descriptive public ContentRef = React.createRef<HTMLDivElement>(); private _disposers: { [name: string]: IReactionDisposer } = {}; public static showBackLinks(linkSource: Doc) { - const docid = Doc.CurrentUserEmail + Doc.GetProto(linkSource)[Id] + "-pivotish"; + const docid = Doc.CurrentUserEmail + Doc.GetProto(linkSource)[Id] + '-pivotish'; DocServer.GetRefField(docid).then(docx => { const rootAlias = () => { const rootAlias = Doc.MakeAlias(linkSource); rootAlias.x = rootAlias.y = 0; return rootAlias; }; - const linkCollection = ((docx instanceof Doc) && docx) || Docs.Create.StackingDocument([/*rootAlias()*/], { title: linkSource.title + "-pivot", _width: 500, _height: 500, }, docid); + const linkCollection = + (docx instanceof Doc && docx) || + Docs.Create.StackingDocument( + [ + /*rootAlias()*/ + ], + { title: linkSource.title + '-pivot', _width: 500, _height: 500 }, + docid + ); linkCollection.linkSource = linkSource; - if (!linkCollection.reactionScript) linkCollection.reactionScript = ScriptField.MakeScript("updateLinkCollection(self)"); + if (!linkCollection.reactionScript) linkCollection.reactionScript = ScriptField.MakeScript('updateLinkCollection(self)'); LightboxView.SetLightboxDoc(linkCollection); }); } @observable public docView: DocumentViewInternal | undefined | null; - showContextMenu(pageX:number, pageY:number) { return this.docView?.onContextMenu(undefined, pageX, pageY); } - get Document() { return this.props.Document; } - get topMost() { return this.props.renderDepth === 0; } - get rootDoc() { return this.docView?.rootDoc || this.Document; } - get dataDoc() { return this.docView?.dataDoc || this.Document; } - get finalLayoutKey() { return this.docView?.finalLayoutKey || "layout"; } - get ContentDiv() { return this.docView?.ContentDiv; } - get ComponentView() { return this.docView?._componentView; } - get allLinks() { return this.docView?.allLinks || []; } - get LayoutFieldKey() { return this.docView?.LayoutFieldKey || "layout"; } - get fitWidth() { return this.props.fitWidth?.(this.rootDoc) || this.layoutDoc.fitWidth; } - - @computed get docViewPath(): DocumentView[] { return this.props.docViewPath ? [...this.props.docViewPath(), this] : [this]; } - @computed get layoutDoc() { return Doc.Layout(this.Document, this.props.LayoutTemplate?.()); } + showContextMenu(pageX: number, pageY: number) { + return this.docView?.onContextMenu(undefined, pageX, pageY); + } + get Document() { + return this.props.Document; + } + get topMost() { + return this.props.renderDepth === 0; + } + get rootDoc() { + return this.docView?.rootDoc || this.Document; + } + get dataDoc() { + return this.docView?.dataDoc || this.Document; + } + get finalLayoutKey() { + return this.docView?.finalLayoutKey || 'layout'; + } + get ContentDiv() { + return this.docView?.ContentDiv; + } + get ComponentView() { + return this.docView?._componentView; + } + get allLinks() { + return this.docView?.allLinks || []; + } + get LayoutFieldKey() { + return this.docView?.LayoutFieldKey || 'layout'; + } + get fitWidth() { + return this.props.fitWidth?.(this.rootDoc) || this.layoutDoc.fitWidth; + } + + @computed get docViewPath(): DocumentView[] { + return this.props.docViewPath ? [...this.props.docViewPath(), this] : [this]; + } + @computed get layoutDoc() { + return Doc.Layout(this.Document, this.props.LayoutTemplate?.()); + } @computed get nativeWidth() { - return this.docView?._componentView?.reverseNativeScaling?.() ? 0 : - returnVal(this.props.NativeWidth?.(), Doc.NativeWidth(this.layoutDoc, this.props.DataDoc, !this.fitWidth)); + return this.docView?._componentView?.reverseNativeScaling?.() ? 0 : returnVal(this.props.NativeWidth?.(), Doc.NativeWidth(this.layoutDoc, this.props.DataDoc, !this.fitWidth)); } @computed get nativeHeight() { - return this.docView?._componentView?.reverseNativeScaling?.() ? 0 : - returnVal(this.props.NativeHeight?.(), Doc.NativeHeight(this.layoutDoc, this.props.DataDoc, !this.fitWidth)); + return this.docView?._componentView?.reverseNativeScaling?.() ? 0 : returnVal(this.props.NativeHeight?.(), Doc.NativeHeight(this.layoutDoc, this.props.DataDoc, !this.fitWidth)); } @computed get shouldNotScale() { - return (this.fitWidth && !this.nativeWidth) || - this.props.dontScaleFilter?.(this.Document) || - this.props.treeViewDoc || [CollectionViewType.Docking].includes(this.Document._viewType as any); + return (this.fitWidth && !this.nativeWidth) || this.props.dontScaleFilter?.(this.Document) || this.props.treeViewDoc || [CollectionViewType.Docking].includes(this.Document._viewType as any); + } + @computed get effectiveNativeWidth() { + return this.shouldNotScale ? 0 : this.nativeWidth || NumCast(this.layoutDoc.width); + } + @computed get effectiveNativeHeight() { + return this.shouldNotScale ? 0 : this.nativeHeight || NumCast(this.layoutDoc.height); } - @computed get effectiveNativeWidth() { return this.shouldNotScale ? 0 : (this.nativeWidth || NumCast(this.layoutDoc.width)); } - @computed get effectiveNativeHeight() { return this.shouldNotScale ? 0 : (this.nativeHeight || NumCast(this.layoutDoc.height)); } @computed get nativeScaling() { if (this.shouldNotScale) return 1; const minTextScale = this.Document.type === DocumentType.RTF ? 0.1 : 0; if (this.fitWidth || this.props.PanelHeight() / (this.effectiveNativeHeight || 1) > this.props.PanelWidth() / (this.effectiveNativeWidth || 1)) { - return Math.max(minTextScale, this.props.PanelWidth() / (this.effectiveNativeWidth || 1)); // width-limited or fitWidth + return Math.max(minTextScale, this.props.PanelWidth() / (this.effectiveNativeWidth || 1)); // width-limited or fitWidth } return Math.max(minTextScale, this.props.PanelHeight() / (this.effectiveNativeHeight || 1)); // height-limited or unscaled } - @computed get panelWidth() { return this.effectiveNativeWidth ? this.effectiveNativeWidth * this.nativeScaling : this.props.PanelWidth(); } + @computed get panelWidth() { + return this.effectiveNativeWidth ? this.effectiveNativeWidth * this.nativeScaling : this.props.PanelWidth(); + } @computed get panelHeight() { if (this.effectiveNativeHeight && !this.layoutDoc.nativeHeightUnfrozen) { const scrollHeight = this.fitWidth ? Math.max(this.ComponentView?.getScrollHeight?.() ?? NumCast(this.layoutDoc.scrollHeight)) : 0; @@ -1253,10 +1492,18 @@ export class DocumentView extends React.Component<DocumentViewProps> { } return this.props.PanelHeight(); } - @computed get Xshift() { return this.effectiveNativeWidth ? (this.props.PanelWidth() - this.effectiveNativeWidth * this.nativeScaling) / 2 : 0; } - @computed get Yshift() { return this.effectiveNativeWidth && this.effectiveNativeHeight && Math.abs(this.Xshift) < 0.001 && !this.layoutDoc.nativeHeightUnfrozen ? Math.max(0, (this.props.PanelHeight() - this.effectiveNativeHeight * this.nativeScaling) / 2) : 0; } - @computed get centeringX() { return this.props.dontCenter?.includes("x") ? 0 : this.Xshift; } - @computed get centeringY() { return this.props.dontCenter?.includes("y") ? 0 : this.Yshift; } + @computed get Xshift() { + return this.effectiveNativeWidth ? (this.props.PanelWidth() - this.effectiveNativeWidth * this.nativeScaling) / 2 : 0; + } + @computed get Yshift() { + return this.effectiveNativeWidth && this.effectiveNativeHeight && Math.abs(this.Xshift) < 0.001 && !this.layoutDoc.nativeHeightUnfrozen ? Math.max(0, (this.props.PanelHeight() - this.effectiveNativeHeight * this.nativeScaling) / 2) : 0; + } + @computed get centeringX() { + return this.props.dontCenter?.includes('x') ? 0 : this.Xshift; + } + @computed get centeringY() { + return this.props.dontCenter?.includes('y') ? 0 : this.Yshift; + } toggleNativeDimensions = () => this.docView && Doc.toggleNativeDimensions(this.layoutDoc, this.docView.ContentScale, this.props.PanelWidth(), this.props.PanelHeight()); focus = (doc: Doc, options?: DocFocusOptions) => this.docView?.focus(doc, options); @@ -1264,23 +1511,23 @@ export class DocumentView extends React.Component<DocumentViewProps> { if (!this.docView || !this.docView.ContentDiv || this.props.Document.presBox || this.docView.props.treeViewDoc || Doc.AreProtosEqual(this.props.Document, Doc.UserDoc())) { return undefined; } - const xf = (this.docView?.props.ScreenToLocalTransform().scale(this.nativeScaling)).inverse(); + const xf = this.docView?.props.ScreenToLocalTransform().scale(this.nativeScaling).inverse(); const [[left, top], [right, bottom]] = [xf.transformPoint(0, 0), xf.transformPoint(this.panelWidth, this.panelHeight)]; if (this.docView.props.LayoutTemplateString?.includes(LinkAnchorBox.name)) { - const docuBox = this.docView.ContentDiv.getElementsByClassName("linkAnchorBox-cont"); + const docuBox = this.docView.ContentDiv.getElementsByClassName('linkAnchorBox-cont'); if (docuBox.length) return { ...docuBox[0].getBoundingClientRect(), center: undefined }; } return { left, top, right, bottom, center: this.ComponentView?.getCenter?.(xf) }; - } + }; public iconify(finished?: () => void) { this.ComponentView?.updateIcon?.(); - const layoutKey = Cast(this.Document.layoutKey, "string", null); - if (layoutKey !== "layout_icon") { - this.switchViews(true, "icon", finished); - if (layoutKey && layoutKey !== "layout" && layoutKey !== "layout_icon") this.Document.deiconifyLayout = layoutKey.replace("layout_", ""); + const layoutKey = Cast(this.Document.layoutKey, 'string', null); + if (layoutKey !== 'layout_icon') { + this.switchViews(true, 'icon', finished); + if (layoutKey && layoutKey !== 'layout' && layoutKey !== 'layout_icon') this.Document.deiconifyLayout = layoutKey.replace('layout_', ''); } else { - const deiconifyLayout = Cast(this.Document.deiconifyLayout, "string", null); + const deiconifyLayout = Cast(this.Document.deiconifyLayout, 'string', null); this.switchViews(deiconifyLayout ? true : false, deiconifyLayout, finished); this.Document.deiconifyLayout = undefined; this.props.bringToFront(this.rootDoc); @@ -1291,17 +1538,23 @@ export class DocumentView extends React.Component<DocumentViewProps> { setCustomView = (custom: boolean, layout: string): void => { Doc.setNativeView(this.props.Document); custom && DocUtils.makeCustomViewClicked(this.props.Document, Docs.Create.StackingDocument, layout, undefined); - } + }; switchViews = action((custom: boolean, view: string, finished?: () => void) => { - this.docView && (this.docView._animateScalingTo = 0.1); // shrink doc - setTimeout(action(() => { - this.setCustomView(custom, view); - this.docView && (this.docView._animateScalingTo = 1); // expand it - setTimeout(action(() => { - this.docView && (this.docView._animateScalingTo = 0); - finished?.(); - }), this.docView!._animateScaleTime - 10); - }), this.docView!._animateScaleTime - 10); + this.docView && (this.docView._animateScalingTo = 0.1); // shrink doc + setTimeout( + action(() => { + this.setCustomView(custom, view); + this.docView && (this.docView._animateScalingTo = 1); // expand it + setTimeout( + action(() => { + this.docView && (this.docView._animateScalingTo = 0); + finished?.(); + }), + this.docView!._animateScaleTime - 10 + ); + }), + this.docView!._animateScaleTime - 10 + ); }); startDragging = (x: number, y: number, dropAction: dropActionType, hideSource = false) => this.docView?.startDragging(x, y, dropAction, hideSource); @@ -1315,7 +1568,11 @@ export class DocumentView extends React.Component<DocumentViewProps> { PanelHeight = () => this.panelHeight; ContentScale = () => this.nativeScaling; selfView = () => this; - screenToLocalTransform = () =>this.props.ScreenToLocalTransform().translate(-this.centeringX, -this.centeringY).scale(1 / this.nativeScaling); + screenToLocalTransform = () => + this.props + .ScreenToLocalTransform() + .translate(-this.centeringX, -this.centeringY) + .scale(1 / this.nativeScaling); componentDidMount() { this._disposers.reactionScript = reaction( () => ScriptCast(this.rootDoc.reactionScript)?.script?.run({ this: this.props.Document, self: Cast(this.rootDoc, Doc, null) || this.props.Document }).result, @@ -1341,32 +1598,41 @@ export class DocumentView extends React.Component<DocumentViewProps> { const yshift = () => (this.props.Document.isInkMask ? InkingStroke.MaskDim : Math.abs(this.Yshift) <= 0.001 ? this.props.PanelHeight() : undefined); const isPresTreeElement: boolean = this.props.treeViewDoc?.type === DocumentType.PRES; const isButton: boolean = this.props.Document.type === DocumentType.FONTICON || this.props.Document._viewType === CollectionViewType.Linear; - return (<div className="contentFittingDocumentView"> - {!this.props.Document || !this.props.PanelWidth() ? (null) : ( - <div className="contentFittingDocumentView-previewDoc" ref={this.ContentRef} - style={{ - transition: this.props.dataTransition, - position: this.props.Document.isInkMask ? "absolute" : undefined, - transform: isButton ? undefined : `translate(${this.centeringX}px, ${this.centeringY}px)`, - width: isButton || isPresTreeElement ? "100%" : xshift() ?? `${100 * (this.props.PanelWidth() - this.Xshift * 2) / this.props.PanelWidth()}%`, - height: isButton || this.props.forceAutoHeight ? undefined : yshift() ?? (this.fitWidth ? `${this.panelHeight}px` : - `${100 * this.effectiveNativeHeight / this.effectiveNativeWidth * this.props.PanelWidth() / this.props.PanelHeight()}%`), - }}> - <DocumentViewInternal {...this.props} - DocumentView={this.selfView} - viewPath={this.docViewPathFunc} - PanelWidth={this.PanelWidth} - PanelHeight={this.PanelHeight} - NativeWidth={this.NativeWidth} - NativeHeight={this.NativeHeight} - isSelected={this.isSelected} - select={this.select} - ContentScaling={this.ContentScale} - ScreenToLocalTransform={this.screenToLocalTransform} - focus={this.props.focus || emptyFunction} - ref={action((r: DocumentViewInternal | null) => r && (this.docView = r))} /> - </div>)} - </div>); + return ( + <div className="contentFittingDocumentView"> + {!this.props.Document || !this.props.PanelWidth() ? null : ( + <div + className="contentFittingDocumentView-previewDoc" + ref={this.ContentRef} + style={{ + transition: this.props.dataTransition, + position: this.props.Document.isInkMask ? 'absolute' : undefined, + transform: isButton ? undefined : `translate(${this.centeringX}px, ${this.centeringY}px)`, + width: isButton || isPresTreeElement ? '100%' : xshift() ?? `${(100 * (this.props.PanelWidth() - this.Xshift * 2)) / this.props.PanelWidth()}%`, + height: + isButton || this.props.forceAutoHeight + ? undefined + : yshift() ?? (this.fitWidth ? `${this.panelHeight}px` : `${(((100 * this.effectiveNativeHeight) / this.effectiveNativeWidth) * this.props.PanelWidth()) / this.props.PanelHeight()}%`), + }}> + <DocumentViewInternal + {...this.props} + DocumentView={this.selfView} + viewPath={this.docViewPathFunc} + PanelWidth={this.PanelWidth} + PanelHeight={this.PanelHeight} + NativeWidth={this.NativeWidth} + NativeHeight={this.NativeHeight} + isSelected={this.isSelected} + select={this.select} + ContentScaling={this.ContentScale} + ScreenToLocalTransform={this.screenToLocalTransform} + focus={this.props.focus || emptyFunction} + ref={action((r: DocumentViewInternal | null) => r && (this.docView = r))} + /> + </div> + )} + </div> + ); } } @@ -1380,7 +1646,7 @@ ScriptingGlobals.add(function deiconifyView(documentView: DocumentView) { }); ScriptingGlobals.add(function toggleDetail(dv: DocumentView, detailLayoutKeySuffix: string) { - if (dv.Document.layoutKey === "layout_" + detailLayoutKeySuffix) dv.switchViews(false, "layout"); + if (dv.Document.layoutKey === 'layout_' + detailLayoutKeySuffix) dv.switchViews(false, 'layout'); else dv.switchViews(true, detailLayoutKeySuffix); }); @@ -1398,8 +1664,8 @@ ScriptingGlobals.add(function updateLinkCollection(linkCollection: Doc) { alias.y = 0; alias._lockedPosition = false; wid += otherdoc[WidthSym](); - Doc.AddDocToList(Doc.GetProto(linkCollection), "data", alias); + Doc.AddDocToList(Doc.GetProto(linkCollection), 'data', alias); } }); return links; -});
\ No newline at end of file +}); diff --git a/src/client/views/nodes/FilterBox.tsx b/src/client/views/nodes/FilterBox.tsx index 17b57cb3b..ff04a293c 100644 --- a/src/client/views/nodes/FilterBox.tsx +++ b/src/client/views/nodes/FilterBox.tsx @@ -1,52 +1,80 @@ -import React = require("react"); -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { action, computed, observable, reaction, runInAction } from "mobx"; -import { observer } from "mobx-react"; -import Select from "react-select"; -import { Doc, DocListCast, DocListCastAsync, Field, HeightSym, Opt } from "../../../fields/Doc"; -import { List } from "../../../fields/List"; -import { RichTextField } from "../../../fields/RichTextField"; -import { listSpec } from "../../../fields/Schema"; -import { ComputedField, ScriptField } from "../../../fields/ScriptField"; -import { Cast, NumCast, StrCast, DocCast } from "../../../fields/Types"; -import { emptyFunction, returnEmptyDoclist, returnEmptyFilter, returnFalse, returnTrue } from "../../../Utils"; -import { Docs } from "../../documents/Documents"; -import { DocumentType } from "../../documents/DocumentTypes"; -import { CurrentUserUtils } from "../../util/CurrentUserUtils"; -import { UserOptions } from "../../util/GroupManager"; -import { ScriptingGlobals } from "../../util/ScriptingGlobals"; -import { SelectionManager } from "../../util/SelectionManager"; -import { CollectionTreeView } from "../collections/CollectionTreeView"; -import { CollectionView } from "../collections/CollectionView"; -import { ViewBoxBaseComponent } from "../DocComponent"; -import { EditableView } from "../EditableView"; -import { SearchBox } from "../search/SearchBox"; -import { DashboardToggleButton, DefaultStyleProvider, StyleProp } from "../StyleProvider"; -import { DocumentViewProps } from "./DocumentView"; +import React = require('react'); +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { action, computed, observable, reaction, runInAction } from 'mobx'; +import { observer } from 'mobx-react'; +import Select from 'react-select'; +import { Doc, DocListCast, DocListCastAsync, Field, HeightSym, Opt } from '../../../fields/Doc'; +import { List } from '../../../fields/List'; +import { RichTextField } from '../../../fields/RichTextField'; +import { listSpec } from '../../../fields/Schema'; +import { ComputedField, ScriptField } from '../../../fields/ScriptField'; +import { Cast, DocCast, NumCast, StrCast } from '../../../fields/Types'; +import { emptyFunction, returnEmptyDoclist, returnEmptyFilter, returnFalse, returnTrue } from '../../../Utils'; +import { Docs, DocumentOptions } from '../../documents/Documents'; +import { DocumentType } from '../../documents/DocumentTypes'; +import { UserOptions } from '../../util/GroupManager'; +import { ScriptingGlobals } from '../../util/ScriptingGlobals'; +import { SelectionManager } from '../../util/SelectionManager'; +import { CollectionTreeView } from '../collections/CollectionTreeView'; +import { CollectionView } from '../collections/CollectionView'; +import { ViewBoxBaseComponent } from '../DocComponent'; +import { EditableView } from '../EditableView'; +import { SearchBox } from '../search/SearchBox'; +import { DashboardToggleButton, DefaultStyleProvider, StyleProp } from '../StyleProvider'; +import { DocumentViewProps } from './DocumentView'; import { FieldView, FieldViewProps } from './FieldView'; import './FilterBox.scss'; -const higflyout = require("@hig/flyout"); +const higflyout = require('@hig/flyout'); export const { anchorPoints } = higflyout; export const Flyout = higflyout.default; @observer export class FilterBox extends ViewBoxBaseComponent<FieldViewProps>() { - constructor(props: Readonly<FieldViewProps>) { super(props); const targetDoc = FilterBox.targetDoc; - if (targetDoc && !targetDoc.currentFilter) targetDoc.currentFilter = CurrentUserUtils.createFilterDoc(); + if (targetDoc && !targetDoc.currentFilter) targetDoc.currentFilter = FilterBox.createFilterDoc(); + } + + /// creates a new, empty filter doc + static createFilterDoc() { + const clearAll = `getProto(self).data = new List([])`; + const reqdOpts: DocumentOptions = { + _lockedPosition: true, + _autoHeight: true, + _fitWidth: true, + _height: 150, + _xPadding: 5, + _yPadding: 5, + _gridGap: 5, + _forceActive: true, + title: 'Unnamed Filter', + filterBoolean: 'AND', + boxShadow: '0 0', + childDontRegisterViews: true, + targetDropAction: 'same', + ignoreClick: true, + system: true, + childDropAction: 'none', + treeViewHideTitle: true, + treeViewTruncateTitleWidth: 150, + childContextMenuLabels: new List<string>(['Clear All']), + childContextMenuScripts: new List<ScriptField>([ScriptField.MakeFunction(clearAll)!]), + }; + return Docs.Create.FilterDocument(reqdOpts); + } + public static LayoutString(fieldKey: string) { + return FieldView.LayoutString(FilterBox, fieldKey); } - public static LayoutString(fieldKey: string) { return FieldView.LayoutString(FilterBox, fieldKey); } public _filterSelected = false; - public _filterMatch = "matched"; + public _filterMatch = 'matched'; @computed static get currentContainingCollectionDoc() { let docView: any = SelectionManager.Views()[0]; let doc = docView.Document; - while (docView && doc && doc.type !== "collection") { + while (docView && doc && doc.type !== 'collection') { doc = docView.props.ContainingCollectionDoc; docView = docView.props.ContainingCollectionView; } @@ -58,7 +86,6 @@ export class FilterBox extends ViewBoxBaseComponent<FieldViewProps>() { // * @returns the relevant doc according to the value of FilterBox._filterScope i.e. either the Current Dashboard or the Current Collection // */ - // trying to get this to work rather than the version below this // @computed static get targetDoc() { @@ -70,31 +97,33 @@ export class FilterBox extends ViewBoxBaseComponent<FieldViewProps>() { // // return FilterBox._filterScope === "Current Collection" ? SelectionManager.Views()[0].Document || CollectionDockingView.Instance.props.Document : CollectionDockingView.Instance.props.Document; // } - /** - * @returns the relevant doc according to the value of FilterBox._filterScope i.e. either the Current Dashboard or the Current Collection - */ + * @returns the relevant doc according to the value of FilterBox._filterScope i.e. either the Current Dashboard or the Current Collection + */ @computed static get targetDoc() { - return SelectionManager.Docs().length ? SelectionManager.Docs()[0] : CurrentUserUtils.ActiveDashboard; + return SelectionManager.Docs().length ? SelectionManager.Docs()[0] : Doc.ActiveDashboard; } @computed static get targetDocChildKey() { if (SelectionManager.Views().length) { - return SelectionManager.Views()[0].ComponentView?.annotationKey || SelectionManager.Views()[0].ComponentView?.fieldKey || "data"; + return SelectionManager.Views()[0].ComponentView?.annotationKey || SelectionManager.Views()[0].ComponentView?.fieldKey || 'data'; } - return "data"; + return 'data'; } @computed static get targetDocChildren() { - return DocListCast(FilterBox.targetDoc?.[FilterBox.targetDocChildKey] || CurrentUserUtils.ActiveDashboard?.data); + return DocListCast(FilterBox.targetDoc?.[FilterBox.targetDocChildKey] || Doc.ActiveDashboard?.data); } @observable _loaded = false; componentDidMount() { - reaction(() => DocListCastAsync(this.layoutDoc.data), - async (activeTabsAsync) => { + reaction( + () => DocListCastAsync(this.layoutDoc.data), + async activeTabsAsync => { const activeTabs = await activeTabsAsync; activeTabs && (await SearchBox.foreachRecursiveDocAsync(activeTabs, emptyFunction)); - runInAction(() => this._loaded = true); - }, { fireImmediately: true }); + runInAction(() => (this._loaded = true)); + }, + { fireImmediately: true } + ); } @computed get allDocs() { @@ -109,16 +138,18 @@ export class FilterBox extends ViewBoxBaseComponent<FieldViewProps>() { @computed get _allFacets() { // trace(); - const noviceReqFields = ["author", "tags", "text", "type"]; - const noviceLayoutFields: string[] = [];//["_curPage"]; + const noviceReqFields = ['author', 'tags', 'text', 'type']; + const noviceLayoutFields: string[] = []; //["_curPage"]; const noviceFields = [...noviceReqFields, ...noviceLayoutFields]; const keys = new Set<string>(noviceFields); this.allDocs.forEach(doc => SearchBox.documentKeys(doc).filter(key => keys.add(key))); - return Array.from(keys.keys()).filter(key => key[0]).filter(key => key[0] === "#" || key.indexOf("lastModified") !== -1 || (key[0] === key[0].toUpperCase() && !key.startsWith("_")) || noviceFields.includes(key) || !Doc.noviceMode).sort(); + return Array.from(keys.keys()) + .filter(key => key[0]) + .filter(key => key[0] === '#' || key.indexOf('lastModified') !== -1 || (key[0] === key[0].toUpperCase() && !key.startsWith('_')) || noviceFields.includes(key) || !Doc.noviceMode) + .sort(); } - /** * The current attributes selected to filter based on */ @@ -136,26 +167,26 @@ export class FilterBox extends ViewBoxBaseComponent<FieldViewProps>() { gatherFieldValues(childDocs: Doc[], facetKey: string) { const valueSet = new Set<string>(); let rtFields = 0; - childDocs.forEach((d) => { + childDocs.forEach(d => { const facetVal = d[facetKey]; if (facetVal instanceof RichTextField) rtFields++; valueSet.add(Field.toString(facetVal as Field)); const fieldKey = Doc.LayoutFieldKey(d); - const annos = !Field.toString(Doc.LayoutField(d) as Field).includes("CollectionView"); - const data = d[annos ? fieldKey + "-annotations" : fieldKey]; + const annos = !Field.toString(Doc.LayoutField(d) as Field).includes('CollectionView'); + const data = d[annos ? fieldKey + '-annotations' : fieldKey]; if (data !== undefined) { let subDocs = DocListCast(data); if (subDocs.length > 0) { let newarray: Doc[] = []; while (subDocs.length > 0) { newarray = []; - subDocs.forEach((t) => { + subDocs.forEach(t => { const facetVal = t[facetKey]; if (facetVal instanceof RichTextField) rtFields++; facetVal !== undefined && valueSet.add(Field.toString(facetVal as Field)); const fieldKey = Doc.LayoutFieldKey(t); - const annos = !Field.toString(Doc.LayoutField(t) as Field).includes("CollectionView"); - DocListCast(t[annos ? fieldKey + "-annotations" : fieldKey]).forEach((newdoc) => newarray.push(newdoc)); + const annos = !Field.toString(Doc.LayoutField(t) as Field).includes('CollectionView'); + DocListCast(t[annos ? fieldKey + '-annotations' : fieldKey]).forEach(newdoc => newarray.push(newdoc)); }); subDocs = newarray; } @@ -164,7 +195,7 @@ export class FilterBox extends ViewBoxBaseComponent<FieldViewProps>() { }); return { strings: Array.from(valueSet.keys()), rtFields }; } - removeFilterDoc = (doc: Doc | Doc[]) => (doc instanceof Doc ? [doc] : doc).map(doc => this.removeFilter(StrCast(doc.title))).length ? true : false; + removeFilterDoc = (doc: Doc | Doc[]) => ((doc instanceof Doc ? [doc] : doc).map(doc => this.removeFilter(StrCast(doc.title))).length ? true : false); public removeFilter = (filterName: string) => { const targetDoc = FilterBox.targetDoc; if (targetDoc) { @@ -173,24 +204,24 @@ export class FilterBox extends ViewBoxBaseComponent<FieldViewProps>() { const found = attributes.findIndex(doc => doc.title === filterName); if (found !== -1) { (filterDoc.data as List<Doc>).splice(found, 1); - const docFilter = Cast(targetDoc._docFilters, listSpec("string")); + const docFilter = Cast(targetDoc._docFilters, listSpec('string')); if (docFilter) { let index: number; - while ((index = docFilter.findIndex(item => item.split(":")[0] === filterName)) !== -1) { + while ((index = docFilter.findIndex(item => item.split(':')[0] === filterName)) !== -1) { docFilter.splice(index, 1); } } - const docRangeFilters = Cast(targetDoc._docRangeFilters, listSpec("string")); + const docRangeFilters = Cast(targetDoc._docRangeFilters, listSpec('string')); if (docRangeFilters) { let index: number; - while ((index = docRangeFilters.findIndex(item => item.split(":")[0] === filterName)) !== -1) { + while ((index = docRangeFilters.findIndex(item => item.split(':')[0] === filterName)) !== -1) { docRangeFilters.splice(index, 3); } } } } return true; - } + }; /** * Responds to clicking the check box in the flyout menu */ @@ -205,7 +236,8 @@ export class FilterBox extends ViewBoxBaseComponent<FieldViewProps>() { const facetValues = this.gatherFieldValues(targetDocChildren, facetHeader); let nonNumbers = 0; - let minVal = Number.MAX_VALUE, maxVal = -Number.MAX_VALUE; + let minVal = Number.MAX_VALUE, + maxVal = -Number.MAX_VALUE; facetValues.strings.map(val => { const num = val ? Number(val) : Number.NaN; if (Number.isNaN(num)) { @@ -216,40 +248,56 @@ export class FilterBox extends ViewBoxBaseComponent<FieldViewProps>() { } }); let newFacet: Opt<Doc>; - if (facetHeader === "text" || facetValues.rtFields / allCollectionDocs.length > 0.1) { - newFacet = Docs.Create.TextDocument("", { - title: facetHeader, system: true, target: targetDoc, _width: 100, _height: 25, _stayInCollection: true, - treeViewExpandedView: "layout", _treeViewOpen: true, _forceActive: true, ignoreClick: true, + if (facetHeader === 'text' || facetValues.rtFields / allCollectionDocs.length > 0.1) { + newFacet = Docs.Create.TextDocument('', { + title: facetHeader, + system: true, + target: targetDoc, + _width: 100, + _height: 25, + _stayInCollection: true, + treeViewExpandedView: 'layout', + _treeViewOpen: true, + _forceActive: true, + ignoreClick: true, }); Doc.GetProto(newFacet).forceActive = true; // required for FormattedTextBox to be able to gain focus since it will never be 'selected' Doc.GetProto(newFacet).type = DocumentType.COL; // forces item to show an open/close button instead ofa checkbox newFacet._textBoxPaddingX = newFacet._textBoxPaddingY = 4; const scriptText = `setDocFilter(this?.target, "${facetHeader}", text, "match")`; - newFacet.onTextChanged = ScriptField.MakeScript(scriptText, { this: Doc.name, text: "string" }); - } else if (facetHeader !== "tags" && nonNumbers / facetValues.strings.length < .1) { + newFacet.onTextChanged = ScriptField.MakeScript(scriptText, { this: Doc.name, text: 'string' }); + } else if (facetHeader !== 'tags' && nonNumbers / facetValues.strings.length < 0.1) { newFacet = Docs.Create.SliderDocument({ - title: facetHeader, system: true, target: targetDoc, _fitWidth: true, _height: 40, _stayInCollection: true, - treeViewExpandedView: "layout", _treeViewOpen: true, _forceActive: true, _overflow: "visible", + title: facetHeader, + system: true, + target: targetDoc, + _fitWidth: true, + _height: 40, + _stayInCollection: true, + treeViewExpandedView: 'layout', + _treeViewOpen: true, + _forceActive: true, + _overflow: 'visible', }); const newFacetField = Doc.LayoutFieldKey(newFacet); const ranged = Doc.readDocRangeFilter(targetDoc, facetHeader); Doc.GetProto(newFacet).type = DocumentType.COL; // forces item to show an open/close button instead ofa checkbox - const extendedMinVal = minVal - Math.min(1, Math.floor(Math.abs(maxVal - minVal) * .1)); - const extendedMaxVal = Math.max(minVal + 1, maxVal + Math.min(1, Math.ceil(Math.abs(maxVal - minVal) * .05))); - newFacet[newFacetField + "-min"] = ranged === undefined ? extendedMinVal : ranged[0]; - newFacet[newFacetField + "-max"] = ranged === undefined ? extendedMaxVal : ranged[1]; - Doc.GetProto(newFacet)[newFacetField + "-minThumb"] = extendedMinVal; - Doc.GetProto(newFacet)[newFacetField + "-maxThumb"] = extendedMaxVal; + const extendedMinVal = minVal - Math.min(1, Math.floor(Math.abs(maxVal - minVal) * 0.1)); + const extendedMaxVal = Math.max(minVal + 1, maxVal + Math.min(1, Math.ceil(Math.abs(maxVal - minVal) * 0.05))); + newFacet[newFacetField + '-min'] = ranged === undefined ? extendedMinVal : ranged[0]; + newFacet[newFacetField + '-max'] = ranged === undefined ? extendedMaxVal : ranged[1]; + Doc.GetProto(newFacet)[newFacetField + '-minThumb'] = extendedMinVal; + Doc.GetProto(newFacet)[newFacetField + '-maxThumb'] = extendedMaxVal; const scriptText = `setDocRangeFilter(this?.target, "${facetHeader}", range)`; - newFacet.onThumbChanged = ScriptField.MakeScript(scriptText, { this: Doc.name, range: "number" }); + newFacet.onThumbChanged = ScriptField.MakeScript(scriptText, { this: Doc.name, range: 'number' }); newFacet.data = ComputedField.MakeFunction(`readNumFacetData(self.target, self, "${FilterBox.targetDocChildKey}", "${facetHeader}")`); } else { newFacet = new Doc(); newFacet.system = true; newFacet.title = facetHeader; newFacet.treeViewOpen = true; - newFacet.layout = CollectionView.LayoutString("data"); - newFacet.layoutKey = "layout"; + newFacet.layout = CollectionView.LayoutString('data'); + newFacet.layoutKey = 'layout'; newFacet.type = DocumentType.COL; newFacet.target = targetDoc; newFacet.data = ComputedField.MakeFunction(`readFacetData(self.target, "${FilterBox.targetDocChildKey}", "${facetHeader}")`); @@ -257,11 +305,11 @@ export class FilterBox extends ViewBoxBaseComponent<FieldViewProps>() { newFacet.hideContextMenu = true; Doc.AddDocToList(this.dataDoc, this.props.fieldKey, newFacet); } - } + }; @computed get scriptField() { - const scriptText = "setDocFilter(this?.target, heading, this.title, checked)"; - const script = ScriptField.MakeScript(scriptText, { this: Doc.name, heading: "string", checked: "string", containingTreeView: Doc.name }); + const scriptText = 'setDocFilter(this?.target, heading, this.title, checked)'; + const script = ScriptField.MakeScript(scriptText, { this: Doc.name, heading: 'string', checked: 'string', containingTreeView: Doc.name }); return script ? () => script : undefined; } @@ -271,7 +319,7 @@ export class FilterBox extends ViewBoxBaseComponent<FieldViewProps>() { @action changeBool = (e: any) => { FilterBox.targetDoc && (DocCast(FilterBox.targetDoc.currentFilter).filterBoolean = e.currentTarget.value); - } + }; /** * Changes whether to select matched or unmatched documents @@ -279,7 +327,7 @@ export class FilterBox extends ViewBoxBaseComponent<FieldViewProps>() { @action changeMatch = (e: any) => { this._filterMatch = e.currentTarget.value; - } + }; @action changeSelected = () => { @@ -290,36 +338,42 @@ export class FilterBox extends ViewBoxBaseComponent<FieldViewProps>() { this._filterSelected = true; // helper method to select specified docs } - } + }; FilteringStyleProvider(doc: Opt<Doc>, props: Opt<FieldViewProps | DocumentViewProps>, property: string) { - switch (property.split(":")[0]) { + switch (property.split(':')[0]) { case StyleProp.Decorations: if (doc && !doc.treeViewHideHeaderFields) { - return <> - <div style={{ marginRight: "5px", fontSize: "10px" }}> - <select className="filterBox-selection"> - <option value="Is" key="Is">Is</option> - <option value="Is Not" key="Is Not">Is Not</option> - </select> - </div> - <div className="filterBox-treeView-close" onClick={e => this.removeFilter(StrCast(doc.title))}> - <FontAwesomeIcon icon={"times"} size="sm" /> - </div> - </>; + return ( + <> + <div style={{ marginRight: '5px', fontSize: '10px' }}> + <select className="filterBox-selection"> + <option value="Is" key="Is"> + Is + </option> + <option value="Is Not" key="Is Not"> + Is Not + </option> + </select> + </div> + <div className="filterBox-treeView-close" onClick={e => this.removeFilter(StrCast(doc.title))}> + <FontAwesomeIcon icon={'times'} size="sm" /> + </div> + </> + ); } } return DefaultStyleProvider(doc, props, property); } - suppressChildClick = () => ScriptField.MakeScript("")!; + suppressChildClick = () => ScriptField.MakeScript('')!; /** * Adds a filterDoc to the list of saved filters */ saveFilter = () => { - Doc.AddDocToList(Doc.UserDoc(), "savedFilters", this.props.Document); - } + Doc.AddDocToList(Doc.UserDoc(), 'savedFilters', this.props.Document); + }; /** * Changes the title of the filterDoc @@ -327,116 +381,106 @@ export class FilterBox extends ViewBoxBaseComponent<FieldViewProps>() { onTitleValueChange = (val: string) => { this.props.Document.title = val || `FilterDoc for ${FilterBox.targetDoc?.title}`; return true; - } + }; /** * The flyout from which you can select a saved filter to apply */ @computed get flyoutPanel() { return DocListCast(Doc.UserDoc().savedFilters).map(doc => { - return <> - <div className="filterBox-tempFlyout" onWheel={e => e.stopPropagation()} style={{ height: 50, border: "2px" }} onPointerDown={() => this.props.updateFilterDoc?.(doc)}> - {StrCast(doc.title)} - </div> - </>; - } - ); + return ( + <> + <div className="filterBox-tempFlyout" onWheel={e => e.stopPropagation()} style={{ height: 50, border: '2px' }} onPointerDown={() => this.props.updateFilterDoc?.(doc)}> + {StrCast(doc.title)} + </div> + </> + ); + }); } setTreeHeight = (hgt: number) => { this.layoutDoc._height = NumCast(this.layoutDoc._autoHeightMargins) + 150; // need to add all the border sizes together. - } + }; /** * add lock and hide button decorations for the "Dashboards" flyout TreeView */ FilterStyleProvider = (doc: Opt<Doc>, props: Opt<FieldViewProps | DocumentViewProps>, property: string) => { - if (property.split(":")[0] === StyleProp.Decorations) { - return !doc || doc.treeViewHideHeaderFields ? (null) : - DashboardToggleButton(doc, "hidden", "trash", "trash", () => this.removeFilter(StrCast(doc.title))); + if (property.split(':')[0] === StyleProp.Decorations) { + return !doc || doc.treeViewHideHeaderFields ? null : DashboardToggleButton(doc, 'hidden', 'trash', 'trash', () => this.removeFilter(StrCast(doc.title))); } return this.props.styleProvider?.(doc, props, property); - } + }; layoutHeight = () => this.layoutDoc[HeightSym](); render() { const facetCollection = this.props.Document; const options = this._allFacets.filter(facet => this.currentFacets.indexOf(facet) === -1).map(facet => ({ value: facet, label: facet })); - return this.props.dontRegisterView ? (null) : <div className="filterBox-treeView" style={{ width: "100%" }}> - - <div className="filterBox-title"> - <EditableView - key="editableView" - contents={this.props.Document.title} - height={24} - fontSize={15} - GetValue={() => StrCast(this.props.Document.title)} - SetValue={this.onTitleValueChange} - /> - </div> + return this.props.dontRegisterView ? null : ( + <div className="filterBox-treeView" style={{ width: '100%' }}> + <div className="filterBox-title"> + <EditableView key="editableView" contents={this.props.Document.title} height={24} fontSize={15} GetValue={() => StrCast(this.props.Document.title)} SetValue={this.onTitleValueChange} /> + </div> - <div className="filterBox-select-bool"> - <select className="filterBox-selection" onChange={this.changeBool} defaultValue={StrCast((FilterBox.targetDoc?.currentFilter as Doc)?.filterBoolean)}> - <option value="AND" key="AND">AND</option> - <option value="OR" key="OR">OR</option> - </select> - <div className="filterBox-select-text">filters together</div> - </div> + <div className="filterBox-select-bool"> + <select className="filterBox-selection" onChange={this.changeBool} defaultValue={StrCast((FilterBox.targetDoc?.currentFilter as Doc)?.filterBoolean)}> + <option value="AND" key="AND"> + AND + </option> + <option value="OR" key="OR"> + OR + </option> + </select> + <div className="filterBox-select-text">filters together</div> + </div> - <div className="filterBox-select"> - <Select - placeholder="Add a filter..." - options={options} - isMulti={false} - onChange={val => this.facetClick((val as UserOptions).value)} - onKeyDown={e => e.stopPropagation()} - value={null} - closeMenuOnSelect={true} - /> - </div> + <div className="filterBox-select"> + <Select placeholder="Add a filter..." options={options} isMulti={false} onChange={val => this.facetClick((val as UserOptions).value)} onKeyDown={e => e.stopPropagation()} value={null} closeMenuOnSelect={true} /> + </div> - <div className="filterBox-tree" key="tree"> - <CollectionTreeView - Document={facetCollection} - DataDoc={Doc.GetProto(facetCollection)} - fieldKey={this.props.fieldKey} - CollectionView={undefined} - disableDocBrushing={true} - setHeight={this.setTreeHeight} // if the tree view can trigger the height of the filter box to change, then this needs to be filled in. - docFilters={returnEmptyFilter} - docRangeFilters={returnEmptyFilter} - searchFilterDocs={returnEmptyDoclist} - childDocumentsActive={returnTrue} - ContainingCollectionDoc={this.props.ContainingCollectionDoc} - ContainingCollectionView={this.props.ContainingCollectionView} - PanelWidth={this.props.PanelWidth} - PanelHeight={this.layoutHeight} - rootSelected={this.props.rootSelected} - renderDepth={1} - dropAction={this.props.dropAction} - ScreenToLocalTransform={this.props.ScreenToLocalTransform} - isAnyChildContentActive={returnFalse} - addDocTab={returnFalse} - pinToPres={returnFalse} - isSelected={returnFalse} - select={returnFalse} - bringToFront={emptyFunction} - isContentActive={returnTrue} - whenChildContentsActiveChanged={returnFalse} - treeViewHideTitle={true} - focus={returnFalse} - onCheckedClick={this.scriptField} - treeViewHideHeaderFields={false} - dontRegisterView={true} - styleProvider={this.FilterStyleProvider} - docViewPath={this.props.docViewPath} - scriptContext={this.props.scriptContext} - moveDocument={returnFalse} - removeDocument={this.removeFilterDoc} - addDocument={returnFalse} /> - </div> - <div className="filterBox-bottom"> - {/* <div className="filterBox-select-matched"> + <div className="filterBox-tree" key="tree"> + <CollectionTreeView + Document={facetCollection} + DataDoc={Doc.GetProto(facetCollection)} + fieldKey={this.props.fieldKey} + CollectionView={undefined} + disableDocBrushing={true} + setHeight={this.setTreeHeight} // if the tree view can trigger the height of the filter box to change, then this needs to be filled in. + docFilters={returnEmptyFilter} + docRangeFilters={returnEmptyFilter} + searchFilterDocs={returnEmptyDoclist} + childDocumentsActive={returnTrue} + ContainingCollectionDoc={this.props.ContainingCollectionDoc} + ContainingCollectionView={this.props.ContainingCollectionView} + PanelWidth={this.props.PanelWidth} + PanelHeight={this.layoutHeight} + rootSelected={this.props.rootSelected} + renderDepth={1} + dropAction={this.props.dropAction} + ScreenToLocalTransform={this.props.ScreenToLocalTransform} + isAnyChildContentActive={returnFalse} + addDocTab={returnFalse} + pinToPres={returnFalse} + isSelected={returnFalse} + select={returnFalse} + bringToFront={emptyFunction} + isContentActive={returnTrue} + whenChildContentsActiveChanged={returnFalse} + treeViewHideTitle={true} + focus={returnFalse} + onCheckedClick={this.scriptField} + treeViewHideHeaderFields={false} + dontRegisterView={true} + styleProvider={this.FilterStyleProvider} + docViewPath={this.props.docViewPath} + scriptContext={this.props.scriptContext} + moveDocument={returnFalse} + removeDocument={this.removeFilterDoc} + addDocument={returnFalse} + /> + </div> + <div className="filterBox-bottom"> + {/* <div className="filterBox-select-matched"> <input className="filterBox-select-box" type="checkbox" onChange={this.changeSelected} /> <div className="filterBox-select-text">select</div> @@ -447,38 +491,35 @@ export class FilterBox extends ViewBoxBaseComponent<FieldViewProps>() { <div className="filterBox-select-text">documents</div> </div> */} - <div style={{ display: "flex" }}> - <div className="filterBox-saveWrapper"> - <div className="filterBox-saveBookmark" - onPointerDown={this.saveFilter} - > - <div>SAVE</div> + <div style={{ display: 'flex' }}> + <div className="filterBox-saveWrapper"> + <div className="filterBox-saveBookmark" onPointerDown={this.saveFilter}> + <div>SAVE</div> + </div> </div> - </div> - <div className="filterBox-saveWrapper"> - <div className="filterBox-saveBookmark"> - <Flyout className="myFilters-flyout" anchorPoint={anchorPoints.TOP} content={this.flyoutPanel}> - <div>FILTERS</div> - </Flyout> + <div className="filterBox-saveWrapper"> + <div className="filterBox-saveBookmark"> + <Flyout className="myFilters-flyout" anchorPoint={anchorPoints.TOP} content={this.flyoutPanel}> + <div>FILTERS</div> + </Flyout> + </div> </div> - </div> - <div className="filterBox-saveWrapper"> - <div className="filterBox-saveBookmark" - onPointerDown={this.props.createNewFilterDoc} - > - <div>NEW</div> + <div className="filterBox-saveWrapper"> + <div className="filterBox-saveBookmark" onPointerDown={this.props.createNewFilterDoc}> + <div>NEW</div> + </div> </div> </div> </div> </div> - </div>; + ); } } ScriptingGlobals.add(function determineCheckedState(layoutDoc: Doc, facetHeader: string, facetValue: string) { - const docFilters = Cast(layoutDoc._docFilters, listSpec("string"), []); + const docFilters = Cast(layoutDoc._docFilters, listSpec('string'), []); for (const filter of docFilters) { - const fields = filter.split(":"); // split into key:value:modifiers + const fields = filter.split(':'); // split into key:value:modifiers if (fields[0] === facetHeader && fields[1] === facetValue) { return fields[2]; } @@ -490,11 +531,17 @@ ScriptingGlobals.add(function readNumFacetData(layoutDoc: Doc, facetDoc: Doc, ch const activeTabs = DocListCast(layoutDoc[childKey]); SearchBox.foreachRecursiveDoc(activeTabs, (depth: number, doc: Doc) => allCollectionDocs.add(doc)); const set = new Set<string>(); - if (facetHeader === "tags") allCollectionDocs.forEach(child => Field.toString(child[facetHeader] as Field).split(":").forEach(key => set.add(key))); + if (facetHeader === 'tags') + allCollectionDocs.forEach(child => + Field.toString(child[facetHeader] as Field) + .split(':') + .forEach(key => set.add(key)) + ); else allCollectionDocs.forEach(child => set.add(Field.toString(child[facetHeader] as Field))); const facetValues = Array.from(set).filter(v => v); - let minVal = Number.MAX_VALUE, maxVal = -Number.MAX_VALUE; + let minVal = Number.MAX_VALUE, + maxVal = -Number.MAX_VALUE; facetValues.map(val => { const num = val ? Number(val) : Number.NaN; if (!Number.isNaN(num)) { @@ -504,26 +551,31 @@ ScriptingGlobals.add(function readNumFacetData(layoutDoc: Doc, facetDoc: Doc, ch }); const newFacetField = Doc.LayoutFieldKey(facetDoc); const ranged = Doc.readDocRangeFilter(layoutDoc, facetHeader); - const extendedMinVal = minVal - Math.min(1, Math.floor(Math.abs(maxVal - minVal) * .1)); - const extendedMaxVal = Math.max(minVal + 1, maxVal + Math.min(1, Math.ceil(Math.abs(maxVal - minVal) * .05))); - facetDoc[newFacetField + "-min"] = ranged === undefined ? extendedMinVal : ranged[0]; - facetDoc[newFacetField + "-max"] = ranged === undefined ? extendedMaxVal : ranged[1]; - Doc.GetProto(facetDoc)[newFacetField + "-minThumb"] = extendedMinVal; - Doc.GetProto(facetDoc)[newFacetField + "-maxThumb"] = extendedMaxVal; -}) + const extendedMinVal = minVal - Math.min(1, Math.floor(Math.abs(maxVal - minVal) * 0.1)); + const extendedMaxVal = Math.max(minVal + 1, maxVal + Math.min(1, Math.ceil(Math.abs(maxVal - minVal) * 0.05))); + facetDoc[newFacetField + '-min'] = ranged === undefined ? extendedMinVal : ranged[0]; + facetDoc[newFacetField + '-max'] = ranged === undefined ? extendedMaxVal : ranged[1]; + Doc.GetProto(facetDoc)[newFacetField + '-minThumb'] = extendedMinVal; + Doc.GetProto(facetDoc)[newFacetField + '-maxThumb'] = extendedMaxVal; +}); ScriptingGlobals.add(function readFacetData(layoutDoc: Doc, childKey: string, facetHeader: string) { const allCollectionDocs = new Set<Doc>(); const activeTabs = DocListCast(layoutDoc[childKey]); SearchBox.foreachRecursiveDoc(activeTabs, (depth: number, doc: Doc) => allCollectionDocs.add(doc)); const set = new Set<string>(); - if (facetHeader === "tags") allCollectionDocs.forEach(child => Field.toString(child[facetHeader] as Field).split(":").forEach(key => set.add(key))); + if (facetHeader === 'tags') + allCollectionDocs.forEach(child => + Field.toString(child[facetHeader] as Field) + .split(':') + .forEach(key => set.add(key)) + ); else allCollectionDocs.forEach(child => set.add(Field.toString(child[facetHeader] as Field))); const facetValues = Array.from(set).filter(v => v); let nonNumbers = 0; facetValues.map(val => Number.isNaN(Number(val)) && nonNumbers++); - const facetValueDocSet = (nonNumbers / facetValues.length > .1 ? facetValues.sort() : facetValues.sort((n1: string, n2: string) => Number(n1) - Number(n2))).map(facetValue => { + const facetValueDocSet = (nonNumbers / facetValues.length > 0.1 ? facetValues.sort() : facetValues.sort((n1: string, n2: string) => Number(n1) - Number(n2))).map(facetValue => { const doc = new Doc(); doc.system = true; doc.title = facetValue.toString(); @@ -531,8 +583,8 @@ ScriptingGlobals.add(function readFacetData(layoutDoc: Doc, childKey: string, fa doc.facetHeader = facetHeader; doc.facetValue = facetValue; doc.treeViewHideHeaderFields = true; - doc.treeViewChecked = ComputedField.MakeFunction("determineCheckedState(self.target, self.facetHeader, self.facetValue)"); + doc.treeViewChecked = ComputedField.MakeFunction('determineCheckedState(self.target, self.facetHeader, self.facetValue)'); return doc; }); return new List<Doc>(facetValueDocSet); -});
\ No newline at end of file +}); diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx index 60eb48114..ffa839fcb 100644 --- a/src/client/views/nodes/ImageBox.tsx +++ b/src/client/views/nodes/ImageBox.tsx @@ -1,5 +1,5 @@ import { action, computed, IReactionDisposer, observable, ObservableMap, reaction, runInAction } from 'mobx'; -import { observer } from "mobx-react"; +import { observer } from 'mobx-react'; import { extname } from 'path'; import { DataSym, Doc, DocListCast, Opt, WidthSym } from '../../../fields/Doc'; import { Id } from '../../../fields/FieldSymbols'; @@ -14,13 +14,12 @@ import { TraceMobx } from '../../../fields/util'; import { emptyFunction, OmitKeys, returnFalse, returnOne, setupMoveUpEvents, Utils } from '../../../Utils'; import { GooglePhotos } from '../../apis/google_docs/GooglePhotosClientUtils'; import { CognitiveServices, Confidence, Service, Tag } from '../../cognitive_services/CognitiveServices'; -import { Docs, DocUtils } from '../../documents/Documents'; +import { DocUtils } from '../../documents/Documents'; import { DocumentType } from '../../documents/DocumentTypes'; import { Networking } from '../../Network'; -import { CurrentUserUtils } from '../../util/CurrentUserUtils'; import { DragManager } from '../../util/DragManager'; import { undoBatch } from '../../util/UndoManager'; -import { ContextMenu } from "../../views/ContextMenu"; +import { ContextMenu } from '../../views/ContextMenu'; import { CollectionFreeFormView } from '../collections/collectionFreeForm/CollectionFreeFormView'; import { ContextMenuProps } from '../ContextMenuItem'; import { ViewBoxAnnotatableComponent, ViewBoxAnnotatableProps } from '../DocComponent'; @@ -29,62 +28,64 @@ import { AnchorMenu } from '../pdf/AnchorMenu'; import { StyleProp } from '../StyleProvider'; import { FaceRectangles } from './FaceRectangles'; import { FieldView, FieldViewProps } from './FieldView'; -import "./ImageBox.scss"; -import React = require("react"); +import './ImageBox.scss'; +import React = require('react'); export const pageSchema = createSchema({ - googlePhotosUrl: "string", - googlePhotosTags: "string" + googlePhotosUrl: 'string', + googlePhotosTags: 'string', }); const uploadIcons = { - idle: "downarrow.png", - loading: "loading.gif", - success: "greencheck.png", - failure: "redx.png" + idle: 'downarrow.png', + loading: 'loading.gif', + success: 'greencheck.png', + failure: 'redx.png', }; @observer export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps & FieldViewProps>() { - protected _multiTouchDisposer?: import("../../util/InteractionUtils").InteractionUtils.MultiTouchEventDisposer | undefined; - public static LayoutString(fieldKey: string) { return FieldView.LayoutString(ImageBox, fieldKey); } + protected _multiTouchDisposer?: import('../../util/InteractionUtils').InteractionUtils.MultiTouchEventDisposer | undefined; + public static LayoutString(fieldKey: string) { + return FieldView.LayoutString(ImageBox, fieldKey); + } private _dropDisposer?: DragManager.DragDropDisposer; private _disposers: { [name: string]: IReactionDisposer } = {}; private _getAnchor: (savedAnnotations?: ObservableMap<number, HTMLDivElement[]>) => Opt<Doc> = () => undefined; - @observable _curSuffix = ""; + @observable _curSuffix = ''; @observable _uploadIcon = uploadIcons.idle; protected createDropTarget = (ele: HTMLDivElement) => { this._dropDisposer?.(); ele && (this._dropDisposer = DragManager.MakeDropTarget(ele, this.drop.bind(this), this.props.Document)); - } - setViewSpec = (anchor: Doc, preview: boolean) => { - - } // sets viewing information for a componentview, typically when following a link. 'preview' tells the view to use the values without writing to the document - + }; + setViewSpec = (anchor: Doc, preview: boolean) => {}; // sets viewing information for a componentview, typically when following a link. 'preview' tells the view to use the values without writing to the document getAnchor = () => { const anchor = this._getAnchor?.(this._savedAnnotations); anchor && this.addDocument(anchor); return anchor ?? this.rootDoc; - } + }; componentDidMount() { this.props.setContentView?.(this); // bcz: do not remove this. without it, stepping into an image in the lightbox causes an infinite loop.... - this._disposers.sizer = reaction(() => ( - { + this._disposers.sizer = reaction( + () => ({ forceFull: this.props.renderDepth < 1 || this.layoutDoc._showFullRes, scrSize: this.props.ScreenToLocalTransform().inverse().transformDirection(this.nativeSize.nativeWidth, this.nativeSize.nativeHeight)[0] / this.nativeSize.nativeWidth, - selected: this.props.isSelected() + selected: this.props.isSelected(), }), - ({ forceFull, scrSize, selected }) => this._curSuffix = this.fieldKey === "icon" ? "_m" : forceFull ? "_o" : scrSize < 0.25 ? "_s" : scrSize < 0.5 ? "_m" : scrSize < 0.8 || !selected ? "_l" : "_o", - { fireImmediately: true, delay: 1000 }); - this._disposers.path = reaction(() => ({ nativeSize: this.nativeSize, width: this.layoutDoc[WidthSym]() }), + ({ forceFull, scrSize, selected }) => (this._curSuffix = this.fieldKey === 'icon' ? '_m' : forceFull ? '_o' : scrSize < 0.25 ? '_s' : scrSize < 0.5 ? '_m' : scrSize < 0.8 || !selected ? '_l' : '_o'), + { fireImmediately: true, delay: 1000 } + ); + this._disposers.path = reaction( + () => ({ nativeSize: this.nativeSize, width: this.layoutDoc[WidthSym]() }), ({ nativeSize, width }) => { if (true || !this.layoutDoc._height) { - this.layoutDoc._height = width * nativeSize.nativeHeight / nativeSize.nativeWidth; + this.layoutDoc._height = (width * nativeSize.nativeHeight) / nativeSize.nativeWidth; } }, - { fireImmediately: true }); + { fireImmediately: true } + ); } componentWillUnmount() { @@ -96,10 +97,12 @@ export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp drop = (e: Event, de: DragManager.DropEvent) => { if (de.complete.docDragData) { if (de.metaKey) { - de.complete.docDragData.droppedDocuments.forEach(action((drop: Doc) => { - Doc.AddDocToList(this.dataDoc, this.fieldKey + "-alternates", drop); - e.stopPropagation(); - })); + de.complete.docDragData.droppedDocuments.forEach( + action((drop: Doc) => { + Doc.AddDocToList(this.dataDoc, this.fieldKey + '-alternates', drop); + e.stopPropagation(); + }) + ); } else if (de.altKey || !this.dataDoc[this.fieldKey]) { const layoutDoc = de.complete.docDragData?.draggedDocuments[0]; const targetField = Doc.LayoutFieldKey(layoutDoc); @@ -112,20 +115,20 @@ export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp } } } - } + }; @undoBatch - resolution = () => this.layoutDoc._showFullRes = !this.layoutDoc._showFullRes + resolution = () => (this.layoutDoc._showFullRes = !this.layoutDoc._showFullRes); @undoBatch rotate = action(() => { - const nw = NumCast(this.dataDoc[this.fieldKey + "-nativeWidth"]); - const nh = NumCast(this.dataDoc[this.fieldKey + "-nativeHeight"]); + const nw = NumCast(this.dataDoc[this.fieldKey + '-nativeWidth']); + const nh = NumCast(this.dataDoc[this.fieldKey + '-nativeHeight']); const w = this.layoutDoc._width; const h = this.layoutDoc._height; - this.dataDoc[this.fieldKey + "-rotation"] = (NumCast(this.dataDoc[this.fieldKey + "-rotation"]) + 90) % 360; - this.dataDoc[this.fieldKey + "-nativeWidth"] = nh; - this.dataDoc[this.fieldKey + "-nativeHeight"] = nw; + this.dataDoc[this.fieldKey + '-rotation'] = (NumCast(this.dataDoc[this.fieldKey + '-rotation']) + 90) % 360; + this.dataDoc[this.fieldKey + '-nativeWidth'] = nh; + this.dataDoc[this.fieldKey + '-nativeHeight'] = nw; this.layoutDoc._width = h; this.layoutDoc._height = w; }); @@ -134,15 +137,15 @@ export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp if (!region) return; const cropping = Doc.MakeCopy(region, true); Doc.GetProto(region).lockedPosition = true; - Doc.GetProto(region).title = "region:" + this.rootDoc.title; + Doc.GetProto(region).title = 'region:' + this.rootDoc.title; Doc.GetProto(region).isPushpin = true; this.addDocument(region); const anchx = NumCast(cropping.x); const anchy = NumCast(cropping.y); const anchw = NumCast(cropping._width); const anchh = NumCast(cropping._height); - const viewScale = NumCast(this.rootDoc[this.fieldKey + "-nativeWidth"]) / anchw; - cropping.title = "crop: " + this.rootDoc.title; + const viewScale = NumCast(this.rootDoc[this.fieldKey + '-nativeWidth']) / anchw; + cropping.title = 'crop: ' + this.rootDoc.title; cropping.x = NumCast(this.rootDoc.x) + NumCast(this.rootDoc._width); cropping.y = NumCast(this.rootDoc.y); cropping._width = anchw * (this.props.scaling?.() || 1); @@ -153,70 +156,70 @@ export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp croppingProto.isPrototype = true; croppingProto.proto = Cast(this.rootDoc.proto, Doc, null)?.proto; // set proto of cropping's data doc to be IMAGE_PROTO croppingProto.type = DocumentType.IMG; - croppingProto.layout = ImageBox.LayoutString("data"); + croppingProto.layout = ImageBox.LayoutString('data'); croppingProto.data = ObjectField.MakeCopy(this.rootDoc[this.fieldKey] as ObjectField); - croppingProto["data-nativeWidth"] = anchw; - croppingProto["data-nativeHeight"] = anchh; + croppingProto['data-nativeWidth'] = anchw; + croppingProto['data-nativeHeight'] = anchh; croppingProto.viewScale = viewScale; croppingProto.viewScaleMin = viewScale; croppingProto.panX = anchx / viewScale; croppingProto.panY = anchy / viewScale; - croppingProto.panXMin = (anchx) / viewScale; - croppingProto.panXMax = (anchw) / viewScale; - croppingProto.panYMin = (anchy) / viewScale; - croppingProto.panYMax = (anchh) / viewScale; + croppingProto.panXMin = anchx / viewScale; + croppingProto.panXMax = anchw / viewScale; + croppingProto.panYMin = anchy / viewScale; + croppingProto.panYMax = anchh / viewScale; if (addCrop) { - DocUtils.MakeLink({ doc: region }, { doc: cropping }, "cropped image", ""); + DocUtils.MakeLink({ doc: region }, { doc: cropping }, 'cropped image', ''); } this.props.bringToFront(cropping); return cropping; - } + }; specificContextMenu = (e: React.MouseEvent): void => { const field = Cast(this.dataDoc[this.fieldKey], ImageField); if (field) { const funcs: ContextMenuProps[] = []; - funcs.push({ description: "Rotate Clockwise 90", event: this.rotate, icon: "expand-arrows-alt" }); - funcs.push({ description: `Show ${this.layoutDoc._showFullRes ? "Dynamic Res" : "Full Res"}`, event: this.resolution, icon: "expand-arrows-alt" }); - funcs.push({ description: "Copy path", event: () => Utils.CopyText(this.choosePath(field.url)), icon: "expand-arrows-alt" }); + funcs.push({ description: 'Rotate Clockwise 90', event: this.rotate, icon: 'expand-arrows-alt' }); + funcs.push({ description: `Show ${this.layoutDoc._showFullRes ? 'Dynamic Res' : 'Full Res'}`, event: this.resolution, icon: 'expand-arrows-alt' }); + funcs.push({ description: 'Copy path', event: () => Utils.CopyText(this.choosePath(field.url)), icon: 'expand-arrows-alt' }); if (!Doc.noviceMode) { - funcs.push({ description: "Export to Google Photos", event: () => GooglePhotos.Transactions.UploadImages([this.props.Document]), icon: "caret-square-right" }); + funcs.push({ description: 'Export to Google Photos', event: () => GooglePhotos.Transactions.UploadImages([this.props.Document]), icon: 'caret-square-right' }); - const existingAnalyze = ContextMenu.Instance?.findByDescription("Analyzers..."); - const modes: ContextMenuProps[] = existingAnalyze && "subitems" in existingAnalyze ? existingAnalyze.subitems : []; - modes.push({ description: "Generate Tags", event: this.generateMetadata, icon: "tag" }); - modes.push({ description: "Find Faces", event: this.extractFaces, icon: "camera" }); + const existingAnalyze = ContextMenu.Instance?.findByDescription('Analyzers...'); + const modes: ContextMenuProps[] = existingAnalyze && 'subitems' in existingAnalyze ? existingAnalyze.subitems : []; + modes.push({ description: 'Generate Tags', event: this.generateMetadata, icon: 'tag' }); + modes.push({ description: 'Find Faces', event: this.extractFaces, icon: 'camera' }); //modes.push({ description: "Recommend", event: this.extractText, icon: "brain" }); - !existingAnalyze && ContextMenu.Instance?.addItem({ description: "Analyzers...", subitems: modes, icon: "hand-point-right" }); + !existingAnalyze && ContextMenu.Instance?.addItem({ description: 'Analyzers...', subitems: modes, icon: 'hand-point-right' }); } - ContextMenu.Instance?.addItem({ description: "Options...", subitems: funcs, icon: "asterisk" }); + ContextMenu.Instance?.addItem({ description: 'Options...', subitems: funcs, icon: 'asterisk' }); } - } + }; extractFaces = () => { const converter = (results: any) => { return results.map((face: CognitiveServices.Image.Face) => Doc.Get.FromJson({ data: face, title: `Face: ${face.faceId}` })!); }; - this.url && CognitiveServices.Image.Appliers.ProcessImage(this.dataDoc, [this.fieldKey + "-faces"], this.url, Service.Face, converter); - } + this.url && CognitiveServices.Image.Appliers.ProcessImage(this.dataDoc, [this.fieldKey + '-faces'], this.url, Service.Face, converter); + }; generateMetadata = (threshold: Confidence = Confidence.Excellent) => { const converter = (results: any) => { - const tagDoc = new Doc; + const tagDoc = new Doc(); const tagsList = new List(); results.tags.map((tag: Tag) => { tagsList.push(tag.name); - const sanitized = tag.name.replace(" ", "_"); + const sanitized = tag.name.replace(' ', '_'); tagDoc[sanitized] = ComputedField.MakeFunction(`(${tag.confidence} >= this.confidence) ? ${tag.confidence} : "${ComputedField.undefined}"`); }); - this.dataDoc[this.fieldKey + "-generatedTags"] = tagsList; - tagDoc.title = "Generated Tags Doc"; + this.dataDoc[this.fieldKey + '-generatedTags'] = tagsList; + tagDoc.title = 'Generated Tags Doc'; tagDoc.confidence = threshold; return tagDoc; }; - this.url && CognitiveServices.Image.Appliers.ProcessImage(this.dataDoc, [this.fieldKey + "-generatedTagsDoc"], this.url, Service.ComputerVision, converter); - } + this.url && CognitiveServices.Image.Appliers.ProcessImage(this.dataDoc, [this.fieldKey + '-generatedTagsDoc'], this.url, Service.ComputerVision, converter); + }; @computed private get url() { const data = Cast(this.dataDoc[this.fieldKey], ImageField); @@ -225,9 +228,9 @@ export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp choosePath(url: URL) { const lower = url.href.toLowerCase(); - if (url.protocol === "data") return url.href; + if (url.protocol === 'data') return url.href; if (url.href.indexOf(window.location.origin) === -1) return Utils.CorsProxy(url.href); - if (!/\.(png|jpg|jpeg|gif|webp)$/.test(lower)) return url.href; //Why is this here + if (!/\.(png|jpg|jpeg|gif|webp)$/.test(lower)) return url.href; //Why is this here const ext = extname(url.href); return url.href.replace(ext, this._curSuffix + ext); @@ -235,40 +238,36 @@ export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp considerGooglePhotosLink = () => { const remoteUrl = this.dataDoc.googlePhotosUrl; - return !remoteUrl ? (null) : (<img draggable={false} - style={{ transformOrigin: "bottom right" }} - id={"google-photos"} - src={"/assets/google_photos.png"} - onClick={() => window.open(remoteUrl)} - />); - } + return !remoteUrl ? null : <img draggable={false} style={{ transformOrigin: 'bottom right' }} id={'google-photos'} src={'/assets/google_photos.png'} onClick={() => window.open(remoteUrl)} />; + }; considerGooglePhotosTags = () => { const tags = this.dataDoc.googlePhotosTags; - return !tags ? (null) : (<img id={"google-tags"} src={"/assets/google_tags.png"} />); - } + return !tags ? null : <img id={'google-tags'} src={'/assets/google_tags.png'} />; + }; @computed private get considerDownloadIcon() { const data = this.dataDoc[this.fieldKey]; if (!(data instanceof ImageField)) { - return (null); + return null; } const primary = data.url.href; if (primary.includes(window.location.origin)) { - return (null); + return null; } return ( <img - id={"upload-icon"} draggable={false} - style={{ transformOrigin: "bottom right" }} + id={'upload-icon'} + draggable={false} + style={{ transformOrigin: 'bottom right' }} src={`/assets/${this._uploadIcon}`} onClick={async () => { const { dataDoc } = this; const { success, failure, idle, loading } = uploadIcons; - runInAction(() => this._uploadIcon = loading); - const [{ accessPaths }] = await Networking.PostToServer("/uploadRemoteImage", { sources: [primary] }); - dataDoc[this.props.fieldKey + "-originalUrl"] = primary; + runInAction(() => (this._uploadIcon = loading)); + const [{ accessPaths }] = await Networking.PostToServer('/uploadRemoteImage', { sources: [primary] }); + dataDoc[this.props.fieldKey + '-originalUrl'] = primary; let succeeded = true; let data: ImageField | undefined; try { @@ -276,13 +275,16 @@ export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp } catch { succeeded = false; } - runInAction(() => this._uploadIcon = succeeded ? success : failure); - setTimeout(action(() => { - this._uploadIcon = idle; - if (data) { - dataDoc[this.fieldKey] = data; - } - }), 2000); + runInAction(() => (this._uploadIcon = succeeded ? success : failure)); + setTimeout( + action(() => { + this._uploadIcon = idle; + if (data) { + dataDoc[this.fieldKey] = data; + } + }), + 2000 + ); }} /> ); @@ -290,18 +292,21 @@ export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp @computed get nativeSize() { TraceMobx(); - const nativeWidth = NumCast(this.dataDoc[this.fieldKey + "-nativeWidth"], NumCast(this.layoutDoc[this.fieldKey + "-nativeWidth"], 500)); - const nativeHeight = NumCast(this.dataDoc[this.fieldKey + "-nativeHeight"], NumCast(this.layoutDoc[this.fieldKey + "-nativeHeight"], 1)); - const nativeOrientation = NumCast(this.dataDoc[this.fieldKey + "-nativeOrientation"], 1); + const nativeWidth = NumCast(this.dataDoc[this.fieldKey + '-nativeWidth'], NumCast(this.layoutDoc[this.fieldKey + '-nativeWidth'], 500)); + const nativeHeight = NumCast(this.dataDoc[this.fieldKey + '-nativeHeight'], NumCast(this.layoutDoc[this.fieldKey + '-nativeHeight'], 1)); + const nativeOrientation = NumCast(this.dataDoc[this.fieldKey + '-nativeOrientation'], 1); return { nativeWidth, nativeHeight, nativeOrientation }; } @computed get paths() { const field = Cast(this.dataDoc[this.fieldKey], ImageField, null); // retrieve the primary image URL that is being rendered from the data doc - const alts = DocListCast(this.dataDoc[this.fieldKey + "-alternates"]); // retrieve alternate documents that may be rendered as alternate images - const altpaths = alts.map(doc => Cast(doc[Doc.LayoutFieldKey(doc)], ImageField, null)?.url).filter(url => url).map(url => this.choosePath(url)); // access the primary layout data of the alternate documents + const alts = DocListCast(this.dataDoc[this.fieldKey + '-alternates']); // retrieve alternate documents that may be rendered as alternate images + const altpaths = alts + .map(doc => Cast(doc[Doc.LayoutFieldKey(doc)], ImageField, null)?.url) + .filter(url => url) + .map(url => this.choosePath(url)); // access the primary layout data of the alternate documents const paths = field ? [this.choosePath(field.url), ...altpaths] : altpaths; - return paths.length ? paths : [Utils.CorsProxy("http://www.cs.brown.edu/~bcz/noImage.png")]; + return paths.length ? paths : [Utils.CorsProxy('http://www.cs.brown.edu/~bcz/noImage.png')]; } @computed get content() { @@ -310,39 +315,35 @@ export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp const srcpath = this.paths[0]; const fadepath = this.paths[Math.min(1, this.paths.length - 1)]; const { nativeWidth, nativeHeight, nativeOrientation } = this.nativeSize; - const rotation = NumCast(this.dataDoc[this.fieldKey + "-rotation"]); + const rotation = NumCast(this.dataDoc[this.fieldKey + '-rotation']); const aspect = rotation % 180 ? nativeHeight / nativeWidth : 1; - let transformOrigin = "center center"; + let transformOrigin = 'center center'; let transform = `translate(0%, 0%) rotate(${rotation}deg) scale(${aspect})`; if (rotation === 90 || rotation === -270) { - transformOrigin = "top left"; + transformOrigin = 'top left'; transform = `translate(100%, 0%) rotate(${rotation}deg) scale(${aspect})`; } else if (rotation === 180) { transform = `rotate(${rotation}deg) scale(${aspect})`; } else if (rotation === 270 || rotation === -90) { - transformOrigin = "right top"; + transformOrigin = 'right top'; transform = `translate(-100%, 0%) rotate(${rotation}deg) scale(${aspect})`; } - return <div className="imageBox-cont" key={this.layoutDoc[Id]} ref={this.createDropTarget} onPointerDown={this.marqueeDown}> - <div className="imageBox-fader" style={{ overflow: Array.from(this.props.docViewPath?.()).slice(-1)[0].fitWidth ? "auto" : undefined }} > - <img key="paths" - src={srcpath} - style={{ transform, transformOrigin }} - draggable={false} - width={nativeWidth} /> - {fadepath === srcpath ? (null) : - <div className={`imageBox-fadeBlocker${this.props.isHovering?.() ? "-hover" : ""}`}> - <img className="imageBox-fadeaway" key={"fadeaway"} - src={fadepath} - style={{ transform, transformOrigin }} draggable={false} - width={nativeWidth} /> - </div>} + return ( + <div className="imageBox-cont" key={this.layoutDoc[Id]} ref={this.createDropTarget} onPointerDown={this.marqueeDown}> + <div className="imageBox-fader" style={{ overflow: Array.from(this.props.docViewPath?.()).slice(-1)[0].fitWidth ? 'auto' : undefined }}> + <img key="paths" src={srcpath} style={{ transform, transformOrigin }} draggable={false} width={nativeWidth} /> + {fadepath === srcpath ? null : ( + <div className={`imageBox-fadeBlocker${this.props.isHovering?.() ? '-hover' : ''}`}> + <img className="imageBox-fadeaway" key={'fadeaway'} src={fadepath} style={{ transform, transformOrigin }} draggable={false} width={nativeWidth} /> + </div> + )} + </div> + {this.considerDownloadIcon} + {this.considerGooglePhotosLink()} + <FaceRectangles document={this.dataDoc} color={'#0000FF'} backgroundColor={'#0000FF'} /> </div> - {this.considerDownloadIcon} - {this.considerGooglePhotosLink()} - <FaceRectangles document={this.dataDoc} color={"#0000FF"} backgroundColor={"#0000FF"} /> - </div>; + ); } screenToLocalTransform = this.props.ScreenToLocalTransform; @@ -357,66 +358,79 @@ export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp return <div className="imageBox-annotationLayer" style={{ height: this.props.PanelHeight() }} ref={this._annotationLayer} />; } marqueeDown = (e: React.PointerEvent) => { - if (!e.altKey && e.button === 0 && NumCast(this.rootDoc._viewScale,1) <= NumCast(this.rootDoc.viewScaleMin,1) && this.props.isContentActive(true) && ![InkTool.Highlighter, InkTool.Pen, InkTool.Write].includes(CurrentUserUtils.ActiveTool)) { - setupMoveUpEvents(this, e, action(e => { - MarqueeAnnotator.clearAnnotations(this._savedAnnotations); - this._marqueeing = [e.clientX, e.clientY]; - return true; - }), returnFalse, () => MarqueeAnnotator.clearAnnotations(this._savedAnnotations), false); + if (!e.altKey && e.button === 0 && NumCast(this.rootDoc._viewScale, 1) <= NumCast(this.rootDoc.viewScaleMin, 1) && this.props.isContentActive(true) && ![InkTool.Highlighter, InkTool.Pen, InkTool.Write].includes(Doc.ActiveTool)) { + setupMoveUpEvents( + this, + e, + action(e => { + MarqueeAnnotator.clearAnnotations(this._savedAnnotations); + this._marqueeing = [e.clientX, e.clientY]; + return true; + }), + returnFalse, + () => MarqueeAnnotator.clearAnnotations(this._savedAnnotations), + false + ); } - } + }; @action finishMarquee = () => { this._getAnchor = AnchorMenu.Instance?.GetAnchor; this._marqueeing = undefined; this.props.select(false); - } + }; savedAnnotations = () => this._savedAnnotations; render() { TraceMobx(); const borderRad = this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.BorderRounding); - const borderRadius = borderRad?.includes("px") ? `${Number(borderRad.split("px")[0]) / (this.props.scaling?.() || 1)}px` : borderRad; - return (<div className="imageBox" onContextMenu={this.specificContextMenu} ref={this._mainCont} - style={{ - width: this.props.PanelWidth() ? undefined : `100%`, - height: this.props.PanelWidth() ? undefined : `100%`, - pointerEvents: this.layoutDoc._lockedPosition ? "none" : undefined, - borderRadius - }} > - <CollectionFreeFormView {...OmitKeys(this.props, ["NativeWidth", "NativeHeight", "setContentView"]).omit} - renderDepth={this.props.renderDepth + 1} - fieldKey={this.annotationKey} - CollectionView={undefined} - isAnnotationOverlay={true} - annotationLayerHostsContent={true} - PanelWidth={this.props.PanelWidth} - PanelHeight={this.props.PanelHeight} - ScreenToLocalTransform={this.screenToLocalTransform} - select={emptyFunction} - scaling={returnOne} - whenChildContentsActiveChanged={this.whenChildContentsActiveChanged} - removeDocument={this.removeDocument} - moveDocument={this.moveDocument} - addDocument={this.addDocument}> - {this.contentFunc} - </CollectionFreeFormView> - {this.annotationLayer} - {!this._marqueeing || !this._mainCont.current || !this._annotationLayer.current ? (null) : - <MarqueeAnnotator - rootDoc={this.rootDoc} - scrollTop={0} - down={this._marqueeing} - scaling={this.props.scaling} - docView={this.props.docViewPath().slice(-1)[0]} - addDocument={this.addDocument} - finishMarquee={this.finishMarquee} - savedAnnotations={this.savedAnnotations} - annotationLayer={this._annotationLayer.current} - mainCont={this._mainCont.current} - anchorMenuCrop={this.crop} - />} - </div >); + const borderRadius = borderRad?.includes('px') ? `${Number(borderRad.split('px')[0]) / (this.props.scaling?.() || 1)}px` : borderRad; + return ( + <div + className="imageBox" + onContextMenu={this.specificContextMenu} + ref={this._mainCont} + style={{ + width: this.props.PanelWidth() ? undefined : `100%`, + height: this.props.PanelWidth() ? undefined : `100%`, + pointerEvents: this.layoutDoc._lockedPosition ? 'none' : undefined, + borderRadius, + }}> + <CollectionFreeFormView + {...OmitKeys(this.props, ['NativeWidth', 'NativeHeight', 'setContentView']).omit} + renderDepth={this.props.renderDepth + 1} + fieldKey={this.annotationKey} + CollectionView={undefined} + isAnnotationOverlay={true} + annotationLayerHostsContent={true} + PanelWidth={this.props.PanelWidth} + PanelHeight={this.props.PanelHeight} + ScreenToLocalTransform={this.screenToLocalTransform} + select={emptyFunction} + scaling={returnOne} + whenChildContentsActiveChanged={this.whenChildContentsActiveChanged} + removeDocument={this.removeDocument} + moveDocument={this.moveDocument} + addDocument={this.addDocument}> + {this.contentFunc} + </CollectionFreeFormView> + {this.annotationLayer} + {!this._marqueeing || !this._mainCont.current || !this._annotationLayer.current ? null : ( + <MarqueeAnnotator + rootDoc={this.rootDoc} + scrollTop={0} + down={this._marqueeing} + scaling={this.props.scaling} + docView={this.props.docViewPath().slice(-1)[0]} + addDocument={this.addDocument} + finishMarquee={this.finishMarquee} + savedAnnotations={this.savedAnnotations} + annotationLayer={this._annotationLayer.current} + mainCont={this._mainCont.current} + anchorMenuCrop={this.crop} + /> + )} + </div> + ); } - } diff --git a/src/client/views/nodes/LinkAnchorBox.tsx b/src/client/views/nodes/LinkAnchorBox.tsx index 7fd289a97..85a8622ec 100644 --- a/src/client/views/nodes/LinkAnchorBox.tsx +++ b/src/client/views/nodes/LinkAnchorBox.tsx @@ -1,30 +1,31 @@ -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { action, observable } from "mobx"; -import { observer } from "mobx-react"; -import { Doc } from "../../../fields/Doc"; -import { Cast, NumCast, StrCast } from "../../../fields/Types"; -import { TraceMobx } from "../../../fields/util"; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { action, observable } from 'mobx'; +import { observer } from 'mobx-react'; +import { Doc } from '../../../fields/Doc'; +import { Cast, NumCast, StrCast } from '../../../fields/Types'; +import { TraceMobx } from '../../../fields/util'; import { emptyFunction, setupMoveUpEvents, Utils } from '../../../Utils'; -import { DragManager } from "../../util/DragManager"; -import { LinkManager } from "../../util/LinkManager"; -import { SelectionManager } from "../../util/SelectionManager"; -import { ContextMenu } from "../ContextMenu"; -import { ContextMenuProps } from "../ContextMenuItem"; -import { ViewBoxBaseComponent } from "../DocComponent"; -import { LinkEditor } from "../linking/LinkEditor"; -import { StyleProp } from "../StyleProvider"; -import { FieldView, FieldViewProps } from "./FieldView"; -import "./LinkAnchorBox.scss"; -import { LinkDocPreview } from "./LinkDocPreview"; -import React = require("react"); -const higflyout = require("@hig/flyout"); +import { DragManager } from '../../util/DragManager'; +import { LinkFollower } from '../../util/LinkFollower'; +import { SelectionManager } from '../../util/SelectionManager'; +import { ContextMenu } from '../ContextMenu'; +import { ContextMenuProps } from '../ContextMenuItem'; +import { ViewBoxBaseComponent } from '../DocComponent'; +import { LinkEditor } from '../linking/LinkEditor'; +import { StyleProp } from '../StyleProvider'; +import { FieldView, FieldViewProps } from './FieldView'; +import './LinkAnchorBox.scss'; +import { LinkDocPreview } from './LinkDocPreview'; +import React = require('react'); +const higflyout = require('@hig/flyout'); export const { anchorPoints } = higflyout; export const Flyout = higflyout.default; - @observer export class LinkAnchorBox extends ViewBoxBaseComponent<FieldViewProps>() { - public static LayoutString(fieldKey: string) { return FieldView.LayoutString(LinkAnchorBox, fieldKey); } + public static LayoutString(fieldKey: string) { + return FieldView.LayoutString(LinkAnchorBox, fieldKey); + } _doubleTap = false; _lastTap: number = 0; _ref = React.createRef<HTMLDivElement>(); @@ -38,7 +39,7 @@ export class LinkAnchorBox extends ViewBoxBaseComponent<FieldViewProps>() { onPointerDown = (e: React.PointerEvent) => { setupMoveUpEvents(this, e, this.onPointerMove, emptyFunction, emptyFunction, false); - } + }; onPointerMove = action((e: PointerEvent, down: number[], delta: number[]) => { const cdiv = this._ref && this._ref.current && this._ref.current.parentElement; if (!this._isOpen && cdiv) { @@ -47,20 +48,20 @@ export class LinkAnchorBox extends ViewBoxBaseComponent<FieldViewProps>() { const separation = Math.sqrt((pt[0] - e.clientX) * (pt[0] - e.clientX) + (pt[1] - e.clientY) * (pt[1] - e.clientY)); if (separation > 100) { const dragData = new DragManager.DocumentDragData([this.rootDoc]); - dragData.dropAction = "alias"; - dragData.removeDropProperties = ["anchor1_x", "anchor1_y", "anchor2_x", "anchor2_y", "isLinkButton"]; + dragData.dropAction = 'alias'; + dragData.removeDropProperties = ['anchor1_x', 'anchor1_y', 'anchor2_x', 'anchor2_y', 'isLinkButton']; DragManager.StartDocumentDrag([this._ref.current!], dragData, pt[0], pt[1]); return true; } else { - this.rootDoc[this.fieldKey + "_x"] = (pt[0] - bounds.left) / bounds.width * 100; - this.rootDoc[this.fieldKey + "_y"] = (pt[1] - bounds.top) / bounds.height * 100; + this.rootDoc[this.fieldKey + '_x'] = ((pt[0] - bounds.left) / bounds.width) * 100; + this.rootDoc[this.fieldKey + '_y'] = ((pt[1] - bounds.top) / bounds.height) * 100; } } return false; }); @action onClick = (e: React.MouseEvent) => { - if ((e.button === 2 || e.ctrlKey || !this.layoutDoc.isLinkButton)) { + if (e.button === 2 || e.ctrlKey || !this.layoutDoc.isLinkButton) { this.props.select(false); } if (!this._doubleTap && !e.ctrlKey && e.button < 2) { @@ -68,10 +69,13 @@ export class LinkAnchorBox extends ViewBoxBaseComponent<FieldViewProps>() { this._editing = true; anchorContainerDoc && this.props.bringToFront(anchorContainerDoc, false); if (anchorContainerDoc && !this.layoutDoc.onClick && !this._isOpen) { - this._timeout = setTimeout(action(() => { - LinkManager.FollowLink(this.rootDoc, anchorContainerDoc, this.props, false); - this._editing = false; - }), 300 - (Date.now() - this._lastTap)); + this._timeout = setTimeout( + action(() => { + LinkFollower.FollowLink(this.rootDoc, anchorContainerDoc, this.props, false); + this._editing = false; + }), + 300 - (Date.now() - this._lastTap) + ); e.stopPropagation(); } } else { @@ -81,17 +85,17 @@ export class LinkAnchorBox extends ViewBoxBaseComponent<FieldViewProps>() { this.openLinkEditor(e); e.stopPropagation(); } - } + }; openLinkDocOnRight = (e: React.MouseEvent) => { - this.props.addDocTab(this.rootDoc, "add:right"); - } + this.props.addDocTab(this.rootDoc, 'add:right'); + }; openLinkTargetOnRight = (e: React.MouseEvent) => { const alias = Doc.MakeAlias(Cast(this.layoutDoc[this.fieldKey], Doc, null)); alias._isLinkButton = undefined; - alias.layoutKey = "layout"; - this.props.addDocTab(alias, "add:right"); - } + alias.layoutKey = 'layout'; + this.props.addDocTab(alias, 'add:right'); + }; @action openLinkEditor = action((e: React.MouseEvent) => { SelectionManager.DeselectAll(); @@ -100,56 +104,67 @@ export class LinkAnchorBox extends ViewBoxBaseComponent<FieldViewProps>() { specificContextMenu = (e: React.MouseEvent): void => { const funcs: ContextMenuProps[] = []; - funcs.push({ description: "Open Link Target on Right", event: () => this.openLinkTargetOnRight(e), icon: "eye" }); - funcs.push({ description: "Open Link on Right", event: () => this.openLinkDocOnRight(e), icon: "eye" }); - funcs.push({ description: "Open Link Editor", event: () => this.openLinkEditor(e), icon: "eye" }); - funcs.push({ description: "Toggle Always Show Link", event: () => this.props.Document.linkDisplay = !this.props.Document.linkDisplay, icon: "eye" }); + funcs.push({ description: 'Open Link Target on Right', event: () => this.openLinkTargetOnRight(e), icon: 'eye' }); + funcs.push({ description: 'Open Link on Right', event: () => this.openLinkDocOnRight(e), icon: 'eye' }); + funcs.push({ description: 'Open Link Editor', event: () => this.openLinkEditor(e), icon: 'eye' }); + funcs.push({ description: 'Toggle Always Show Link', event: () => (this.props.Document.linkDisplay = !this.props.Document.linkDisplay), icon: 'eye' }); - ContextMenu.Instance.addItem({ description: "Options...", subitems: funcs, icon: "asterisk" }); - } + ContextMenu.Instance.addItem({ description: 'Options...', subitems: funcs, icon: 'asterisk' }); + }; render() { TraceMobx(); const small = this.props.PanelWidth() <= 1; // this happens when rendered in a treeView - const x = NumCast(this.rootDoc[this.fieldKey + "_x"], 100); - const y = NumCast(this.rootDoc[this.fieldKey + "_y"], 100); + const x = NumCast(this.rootDoc[this.fieldKey + '_x'], 100); + const y = NumCast(this.rootDoc[this.fieldKey + '_y'], 100); const linkSource = this.props.styleProvider?.(this.dataDoc, this.props, StyleProp.LinkSource); - const background = this.props.styleProvider?.(this.dataDoc, this.props, StyleProp.BackgroundColor + ":anchor"); - const anchor = this.fieldKey === "anchor1" ? "anchor2" : "anchor1"; - const anchorScale = !this.dataDoc[this.fieldKey + "-useLinkSmallAnchor"] && (x === 0 || x === 100 || y === 0 || y === 100) ? 1 : .25; + const background = this.props.styleProvider?.(this.dataDoc, this.props, StyleProp.BackgroundColor + ':anchor'); + const anchor = this.fieldKey === 'anchor1' ? 'anchor2' : 'anchor1'; + const anchorScale = !this.dataDoc[this.fieldKey + '-useLinkSmallAnchor'] && (x === 0 || x === 100 || y === 0 || y === 100) ? 1 : 0.25; const targetTitle = StrCast((this.dataDoc[anchor] as Doc)?.title); const flyout = ( <div className="linkAnchorBoxBox-flyout" title=" " onPointerOver={() => Doc.UnBrushDoc(this.rootDoc)}> - <LinkEditor sourceDoc={Cast(this.dataDoc[this.fieldKey], Doc, null)} hideback={true} linkDoc={this.rootDoc} showLinks={action(() => { })} /> - {!this._forceOpen ? (null) : <div className="linkAnchorBox-linkCloser" onPointerDown={action(() => this._isOpen = this._editing = this._forceOpen = false)}> - <FontAwesomeIcon color="dimgray" icon={"times"} size={"sm"} /> - </div>} + <LinkEditor sourceDoc={Cast(this.dataDoc[this.fieldKey], Doc, null)} hideback={true} linkDoc={this.rootDoc} showLinks={action(() => {})} /> + {!this._forceOpen ? null : ( + <div className="linkAnchorBox-linkCloser" onPointerDown={action(() => (this._isOpen = this._editing = this._forceOpen = false))}> + <FontAwesomeIcon color="dimgray" icon={'times'} size={'sm'} /> + </div> + )} + </div> + ); + return ( + <div + className={`linkAnchorBox-cont${small ? '-small' : ''}`} + onPointerLeave={LinkDocPreview.Clear} + onPointerEnter={e => + LinkDocPreview.SetLinkInfo({ + docProps: this.props, + linkSrc: linkSource, + linkDoc: this.rootDoc, + showHeader: true, + location: [e.clientX, e.clientY + 20], + }) + } + onPointerDown={this.onPointerDown} + onClick={this.onClick} + title={targetTitle} + onContextMenu={this.specificContextMenu} + ref={this._ref} + style={{ + background, + left: `calc(${x}% - ${small ? 2.5 : 7.5}px)`, + top: `calc(${y}% - ${small ? 2.5 : 7.5}px)`, + transform: `scale(${anchorScale})`, + }}> + {!this._editing && !this._forceOpen ? null : ( + <Flyout anchorPoint={anchorPoints.LEFT_TOP} content={flyout} open={this._forceOpen ? true : undefined} onOpen={() => (this._isOpen = true)} onClose={action(() => (this._isOpen = this._forceOpen = this._editing = false))}> + <span className="linkAnchorBox-button"> + <FontAwesomeIcon icon={'eye'} size={'lg'} /> + </span> + </Flyout> + )} </div> ); - return <div className={`linkAnchorBox-cont${small ? "-small" : ""}`} - onPointerLeave={LinkDocPreview.Clear} - onPointerEnter={e => LinkDocPreview.SetLinkInfo({ - docProps: this.props, - linkSrc: linkSource, - linkDoc: this.rootDoc, - showHeader: true, - location: [e.clientX, e.clientY + 20] - })} - onPointerDown={this.onPointerDown} onClick={this.onClick} title={targetTitle} onContextMenu={this.specificContextMenu} - ref={this._ref} - style={{ - background, - left: `calc(${x}% - ${small ? 2.5 : 7.5}px)`, - top: `calc(${y}% - ${small ? 2.5 : 7.5}px)`, - transform: `scale(${anchorScale})` - }} > - {!this._editing && !this._forceOpen ? (null) : - <Flyout anchorPoint={anchorPoints.LEFT_TOP} content={flyout} open={this._forceOpen ? true : undefined} onOpen={() => this._isOpen = true} onClose={action(() => this._isOpen = this._forceOpen = this._editing = false)}> - <span className="linkAnchorBox-button" > - <FontAwesomeIcon icon={"eye"} size={"lg"} /> - </span> - </Flyout>} - </div>; } } diff --git a/src/client/views/nodes/LinkDocPreview.tsx b/src/client/views/nodes/LinkDocPreview.tsx index 47d5c662e..c2fb5d4ec 100644 --- a/src/client/views/nodes/LinkDocPreview.tsx +++ b/src/client/views/nodes/LinkDocPreview.tsx @@ -16,6 +16,7 @@ import './LinkDocPreview.scss'; import React = require('react'); import { DocumentType } from '../../documents/DocumentTypes'; import { DragManager } from '../../util/DragManager'; +import { LinkFollower } from '../../util/LinkFollower'; interface LinkDocPreviewProps { linkDoc?: Doc; @@ -145,7 +146,7 @@ export class LinkDocPreview extends React.Component<LinkDocPreviewProps> { followLink = (e: React.PointerEvent) => { if (this._linkDoc && this._linkSrc) { LinkDocPreview.Clear(); - LinkManager.FollowLink(this._linkDoc, this._linkSrc, this.props.docProps, false); + LinkFollower.FollowLink(this._linkDoc, this._linkSrc, this.props.docProps, false); } else if (this.props.hrefs?.length) { this.props.docProps?.addDocTab(Docs.Create.WebDocument(this.props.hrefs[0], { title: this.props.hrefs[0], _nativeWidth: 850, _width: 200, _height: 400, useCors: true }), 'add:right'); } diff --git a/src/client/views/nodes/MapBox/MapBox.tsx b/src/client/views/nodes/MapBox/MapBox.tsx index 18a6b5453..bf4c029b2 100644 --- a/src/client/views/nodes/MapBox/MapBox.tsx +++ b/src/client/views/nodes/MapBox/MapBox.tsx @@ -10,7 +10,6 @@ import { NumCast, StrCast } from '../../../../fields/Types'; import { TraceMobx } from '../../../../fields/util'; import { emptyFunction, OmitKeys, returnFalse, returnOne, setupMoveUpEvents, Utils } from '../../../../Utils'; import { Docs } from '../../../documents/Documents'; -import { CurrentUserUtils } from '../../../util/CurrentUserUtils'; import { DragManager } from '../../../util/DragManager'; import { SnappingManager } from '../../../util/SnappingManager'; import { MarqueeOptionsMenu } from '../../collections/collectionFreeForm'; @@ -491,7 +490,7 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps @action onMarqueeDown = (e: React.PointerEvent) => { - if (!e.altKey && e.button === 0 && this.props.isContentActive(true) && ![InkTool.Highlighter, InkTool.Pen, InkTool.Write].includes(CurrentUserUtils.ActiveTool)) { + if (!e.altKey && e.button === 0 && this.props.isContentActive(true) && ![InkTool.Highlighter, InkTool.Pen, InkTool.Write].includes(Doc.ActiveTool)) { setupMoveUpEvents( this, e, @@ -591,7 +590,7 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps // moveDocument={this.moveDocument} // addDocument={this.sidebarAddDocument} // childPointerEvents={true} - // pointerEvents={CurrentUserUtils.ActiveTool !== InkTool.None || this._isAnnotating || SnappingManager.GetIsDragging() ? "all" : "none"} />; + // pointerEvents={Doc.ActiveTool !== InkTool.None || this._isAnnotating || SnappingManager.GetIsDragging() ? "all" : "none"} />; return ( <div className="mapBox" ref={this._ref}> {/*console.log(apiKey)*/} diff --git a/src/client/views/nodes/MapBox/MapBoxInfoWindow.tsx b/src/client/views/nodes/MapBox/MapBoxInfoWindow.tsx index bdf0f9d64..72569135b 100644 --- a/src/client/views/nodes/MapBox/MapBoxInfoWindow.tsx +++ b/src/client/views/nodes/MapBox/MapBoxInfoWindow.tsx @@ -1,19 +1,17 @@ import { InfoWindow } from '@react-google-maps/api'; import { action, computed } from 'mobx'; -import { observer } from "mobx-react"; -import * as React from "react"; +import { observer } from 'mobx-react'; +import * as React from 'react'; import { Doc } from '../../../../fields/Doc'; import { Id } from '../../../../fields/FieldSymbols'; import { emptyFunction, OmitKeys, returnAll, returnEmptyFilter, returnFalse, returnOne, returnTrue, returnZero, setupMoveUpEvents } from '../../../../Utils'; import { Docs } from '../../../documents/Documents'; -import { DocumentType } from '../../../documents/DocumentTypes'; +import { CollectionViewType, DocumentType } from '../../../documents/DocumentTypes'; import { CollectionStackingView } from '../../collections/CollectionStackingView'; -import { CollectionViewType } from '../../collections/CollectionView'; import { ViewBoxAnnotatableProps } from '../../DocComponent'; import { FieldViewProps } from '../FieldView'; import { FormattedTextBox } from '../formattedText/FormattedTextBox'; -import "./MapBox.scss"; - +import './MapBox.scss'; interface MapBoxInfoWindowProps { place: Doc; @@ -22,69 +20,74 @@ interface MapBoxInfoWindowProps { isAnyChildContentActive: () => boolean; } @observer -export class MapBoxInfoWindow extends React.Component<MapBoxInfoWindowProps & ViewBoxAnnotatableProps & FieldViewProps>{ - +export class MapBoxInfoWindow extends React.Component<MapBoxInfoWindowProps & ViewBoxAnnotatableProps & FieldViewProps> { @action private handleInfoWindowClose = () => { if (this.props.place.infoWindowOpen) { this.props.place.infoWindowOpen = false; } this.props.place.infoWindowOpen = false; - } + }; addNoteClick = (e: React.PointerEvent) => { setupMoveUpEvents(this, e, returnFalse, emptyFunction, e => { - const newBox = Docs.Create.TextDocument("Note", { _autoHeight: true }); - FormattedTextBox.SelectOnLoad = newBox[Id];// track the new text box so we can give it a prop that tells it to focus itself when it's displayed - Doc.AddDocToList(this.props.place, "data", newBox); + const newBox = Docs.Create.TextDocument('Note', { _autoHeight: true }); + FormattedTextBox.SelectOnLoad = newBox[Id]; // track the new text box so we can give it a prop that tells it to focus itself when it's displayed + Doc.AddDocToList(this.props.place, 'data', newBox); this._stack?.scrollToBottom(); e.stopPropagation(); e.preventDefault(); }); - } - + }; _stack: CollectionStackingView | null | undefined; - childFitWidth = (doc:Doc) => doc.type === DocumentType.RTF; - addDoc = (doc: Doc | Doc[]) => (doc instanceof Doc ? [doc] : doc).reduce((p, d) => p && Doc.AddDocToList(this.props.place, "data", d), true as boolean); - removeDoc = (doc: Doc | Doc[]) => (doc instanceof Doc ? [doc] : doc).reduce((p, d) => p && Doc.RemoveDocFromList(this.props.place, "data", d), true as boolean); + childFitWidth = (doc: Doc) => doc.type === DocumentType.RTF; + addDoc = (doc: Doc | Doc[]) => (doc instanceof Doc ? [doc] : doc).reduce((p, d) => p && Doc.AddDocToList(this.props.place, 'data', d), true as boolean); + removeDoc = (doc: Doc | Doc[]) => (doc instanceof Doc ? [doc] : doc).reduce((p, d) => p && Doc.RemoveDocFromList(this.props.place, 'data', d), true as boolean); render() { - return <InfoWindow anchor={this.props.markerMap[this.props.place[Id]]} onCloseClick={this.handleInfoWindowClose} > - <div className="mapbox-infowindow"> - <div style={{ width: this.props.PanelWidth(), height: this.props.PanelHeight() }}> - <CollectionStackingView - ref={r => this._stack = r} - {...OmitKeys(this.props, ["NativeWidth", "NativeHeight", "setContentView"]).omit} - Document={this.props.place} - DataDoc={undefined} - fieldKey="data" - CollectionView={undefined} - NativeWidth={returnZero} - NativeHeight={returnZero} - docFilters={returnEmptyFilter} - setHeight={emptyFunction} - isAnnotationOverlay={false} - select={emptyFunction} - scaling={returnOne} - isContentActive={returnTrue} - chromeHidden={true} - rootSelected={returnFalse} - childHideResizeHandles={returnTrue} - childHideDecorationTitle={returnTrue} - childFitWidth={this.childFitWidth} - // childDocumentsActive={returnFalse} - removeDocument={this.removeDoc} - addDocument={this.addDoc} - renderDepth={this.props.renderDepth + 1} - viewType={CollectionViewType.Stacking} - pointerEvents={returnAll} - /> - </div> - <hr /> - <div onPointerDown={this.addNoteClick} onClick={e => { e.stopPropagation(); e.preventDefault(); }} > - Add Note + return ( + <InfoWindow anchor={this.props.markerMap[this.props.place[Id]]} onCloseClick={this.handleInfoWindowClose}> + <div className="mapbox-infowindow"> + <div style={{ width: this.props.PanelWidth(), height: this.props.PanelHeight() }}> + <CollectionStackingView + ref={r => (this._stack = r)} + {...OmitKeys(this.props, ['NativeWidth', 'NativeHeight', 'setContentView']).omit} + Document={this.props.place} + DataDoc={undefined} + fieldKey="data" + CollectionView={undefined} + NativeWidth={returnZero} + NativeHeight={returnZero} + docFilters={returnEmptyFilter} + setHeight={emptyFunction} + isAnnotationOverlay={false} + select={emptyFunction} + scaling={returnOne} + isContentActive={returnTrue} + chromeHidden={true} + rootSelected={returnFalse} + childHideResizeHandles={returnTrue} + childHideDecorationTitle={returnTrue} + childFitWidth={this.childFitWidth} + // childDocumentsActive={returnFalse} + removeDocument={this.removeDoc} + addDocument={this.addDoc} + renderDepth={this.props.renderDepth + 1} + viewType={CollectionViewType.Stacking} + pointerEvents={returnAll} + /> + </div> + <hr /> + <div + onPointerDown={this.addNoteClick} + onClick={e => { + e.stopPropagation(); + e.preventDefault(); + }}> + Add Note + </div> </div> - </div> - </InfoWindow>; + </InfoWindow> + ); } -}
\ No newline at end of file +} diff --git a/src/client/views/nodes/ScreenshotBox.tsx b/src/client/views/nodes/ScreenshotBox.tsx index 0c7d7dc31..942072524 100644 --- a/src/client/views/nodes/ScreenshotBox.tsx +++ b/src/client/views/nodes/ScreenshotBox.tsx @@ -1,32 +1,31 @@ -import React = require("react"); -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import React = require('react'); +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; // import { Canvas } from '@react-three/fiber'; -import { computed, observable, runInAction } from "mobx"; -import { observer } from "mobx-react"; +import { computed, observable, runInAction } from 'mobx'; +import { observer } from 'mobx-react'; // import { BufferAttribute, Camera, Vector2, Vector3 } from 'three'; -import { DateField } from "../../../fields/DateField"; -import { Doc, HeightSym, WidthSym } from "../../../fields/Doc"; -import { Id } from "../../../fields/FieldSymbols"; -import { ComputedField } from "../../../fields/ScriptField"; -import { Cast, NumCast } from "../../../fields/Types"; -import { AudioField, VideoField } from "../../../fields/URLField"; -import { TraceMobx } from "../../../fields/util"; -import { emptyFunction, OmitKeys, returnFalse, returnOne } from "../../../Utils"; -import { DocUtils } from "../../documents/Documents"; -import { DocumentType } from "../../documents/DocumentTypes"; -import { Networking } from "../../Network"; -import { CaptureManager } from "../../util/CaptureManager"; -import { CurrentUserUtils } from "../../util/CurrentUserUtils"; -import { CollectionFreeFormView } from "../collections/collectionFreeForm/CollectionFreeFormView"; -import { CollectionStackedTimeline } from "../collections/CollectionStackedTimeline"; -import { ContextMenu } from "../ContextMenu"; -import { ViewBoxAnnotatableComponent, ViewBoxAnnotatableProps } from "../DocComponent"; +import { DateField } from '../../../fields/DateField'; +import { Doc, HeightSym, WidthSym } from '../../../fields/Doc'; +import { Id } from '../../../fields/FieldSymbols'; +import { ComputedField } from '../../../fields/ScriptField'; +import { Cast, NumCast } from '../../../fields/Types'; +import { AudioField, VideoField } from '../../../fields/URLField'; +import { TraceMobx } from '../../../fields/util'; +import { emptyFunction, OmitKeys, returnFalse, returnOne } from '../../../Utils'; +import { DocUtils } from '../../documents/Documents'; +import { DocumentType } from '../../documents/DocumentTypes'; +import { Networking } from '../../Network'; +import { CaptureManager } from '../../util/CaptureManager'; +import { CollectionFreeFormView } from '../collections/collectionFreeForm/CollectionFreeFormView'; +import { CollectionStackedTimeline } from '../collections/CollectionStackedTimeline'; +import { ContextMenu } from '../ContextMenu'; +import { ViewBoxAnnotatableComponent, ViewBoxAnnotatableProps } from '../DocComponent'; import { FieldView, FieldViewProps } from './FieldView'; -import { FormattedTextBox } from "./formattedText/FormattedTextBox"; -import "./ScreenshotBox.scss"; -import { VideoBox } from "./VideoBox"; +import { FormattedTextBox } from './formattedText/FormattedTextBox'; +import './ScreenshotBox.scss'; +import { VideoBox } from './VideoBox'; declare class MediaRecorder { - constructor(e: any, options?: any); // whatever MediaRecorder has + constructor(e: any, options?: any); // whatever MediaRecorder has } // interface VideoTileProps { @@ -107,226 +106,238 @@ declare class MediaRecorder { @observer export class ScreenshotBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps & FieldViewProps>() { - public static LayoutString(fieldKey: string) { return FieldView.LayoutString(ScreenshotBox, fieldKey); } - private _audioRec: any; - private _videoRec: any; - @observable private _videoRef: HTMLVideoElement | null = null; - @observable _screenCapture = false; - @computed get recordingStart() { return Cast(this.dataDoc[this.props.fieldKey + "-recordingStart"], DateField)?.date.getTime(); } + public static LayoutString(fieldKey: string) { + return FieldView.LayoutString(ScreenshotBox, fieldKey); + } + private _audioRec: any; + private _videoRec: any; + @observable private _videoRef: HTMLVideoElement | null = null; + @observable _screenCapture = false; + @computed get recordingStart() { + return Cast(this.dataDoc[this.props.fieldKey + '-recordingStart'], DateField)?.date.getTime(); + } - constructor(props: any) { - super(props); - if (this.rootDoc.videoWall) { - this.rootDoc.nativeWidth = undefined; - this.rootDoc.nativeHeight = undefined; - this.layoutDoc.popOff = 0; - this.layoutDoc.popOut = 1; - } else { - this.setupDictation(); - } - } - getAnchor = () => { - const startTime = Cast(this.layoutDoc._currentTimecode, "number", null) || (this._videoRec ? (Date.now() - (this.recordingStart || 0)) / 1000 : undefined); - return CollectionStackedTimeline.createAnchor(this.rootDoc, this.dataDoc, this.annotationKey, "_timecodeToShow", "_timecodeToHide", - startTime, startTime === undefined ? undefined : startTime + 3) - || this.rootDoc; - } + constructor(props: any) { + super(props); + if (this.rootDoc.videoWall) { + this.rootDoc.nativeWidth = undefined; + this.rootDoc.nativeHeight = undefined; + this.layoutDoc.popOff = 0; + this.layoutDoc.popOut = 1; + } else { + this.setupDictation(); + } + } + getAnchor = () => { + const startTime = Cast(this.layoutDoc._currentTimecode, 'number', null) || (this._videoRec ? (Date.now() - (this.recordingStart || 0)) / 1000 : undefined); + return CollectionStackedTimeline.createAnchor(this.rootDoc, this.dataDoc, this.annotationKey, '_timecodeToShow', '_timecodeToHide', startTime, startTime === undefined ? undefined : startTime + 3) || this.rootDoc; + }; - videoLoad = () => { - const aspect = this._videoRef!.videoWidth / this._videoRef!.videoHeight; - const nativeWidth = Doc.NativeWidth(this.layoutDoc); - const nativeHeight = Doc.NativeHeight(this.layoutDoc); - if (!nativeWidth || !nativeHeight) { - if (!nativeWidth) Doc.SetNativeWidth(this.dataDoc, 1200); - Doc.SetNativeHeight(this.dataDoc, (nativeWidth || 1200) / aspect); - this.layoutDoc._height = (this.layoutDoc[WidthSym]() || 0) / aspect; - } - } + videoLoad = () => { + const aspect = this._videoRef!.videoWidth / this._videoRef!.videoHeight; + const nativeWidth = Doc.NativeWidth(this.layoutDoc); + const nativeHeight = Doc.NativeHeight(this.layoutDoc); + if (!nativeWidth || !nativeHeight) { + if (!nativeWidth) Doc.SetNativeWidth(this.dataDoc, 1200); + Doc.SetNativeHeight(this.dataDoc, (nativeWidth || 1200) / aspect); + this.layoutDoc._height = (this.layoutDoc[WidthSym]() || 0) / aspect; + } + }; - componentDidMount() { - this.dataDoc.nativeWidth = this.dataDoc.nativeHeight = 0; - this.props.setContentView?.(this); // this tells the DocumentView that this ScreenshotBox is the "content" of the document. this allows the DocumentView to indirectly call getAnchor() on the AudioBox when making a link. - // this.rootDoc.videoWall && reaction(() => ({ width: this.props.PanelWidth(), height: this.props.PanelHeight() }), - // ({ width, height }) => { - // if (this._camera) { - // const angle = -Math.abs(1 - width / height); - // const xz = [0, (this._numScreens - 2) / Math.abs(1 + angle)]; - // this._camera.position.set(this._numScreens / 2 + xz[1] * Math.sin(angle), this._numScreens / 2, xz[1] * Math.cos(angle)); - // this._camera.lookAt(this._numScreens / 2, this._numScreens / 2, 0); - // (this._camera as any).updateProjectionMatrix(); - // } - // }); - } - componentWillUnmount() { - const ind = DocUtils.ActiveRecordings.indexOf(this); - ind !== -1 && (DocUtils.ActiveRecordings.splice(ind, 1)); - } - - specificContextMenu = (e: React.MouseEvent): void => { - const subitems = [{ description: "Screen Capture", event: this.toggleRecording, icon: "expand-arrows-alt" as any }]; - ContextMenu.Instance.addItem({ description: "Options...", subitems, icon: "video" }); - } + componentDidMount() { + this.dataDoc.nativeWidth = this.dataDoc.nativeHeight = 0; + this.props.setContentView?.(this); // this tells the DocumentView that this ScreenshotBox is the "content" of the document. this allows the DocumentView to indirectly call getAnchor() on the AudioBox when making a link. + // this.rootDoc.videoWall && reaction(() => ({ width: this.props.PanelWidth(), height: this.props.PanelHeight() }), + // ({ width, height }) => { + // if (this._camera) { + // const angle = -Math.abs(1 - width / height); + // const xz = [0, (this._numScreens - 2) / Math.abs(1 + angle)]; + // this._camera.position.set(this._numScreens / 2 + xz[1] * Math.sin(angle), this._numScreens / 2, xz[1] * Math.cos(angle)); + // this._camera.lookAt(this._numScreens / 2, this._numScreens / 2, 0); + // (this._camera as any).updateProjectionMatrix(); + // } + // }); + } + componentWillUnmount() { + const ind = DocUtils.ActiveRecordings.indexOf(this); + ind !== -1 && DocUtils.ActiveRecordings.splice(ind, 1); + } - @computed get content() { - if (this.rootDoc.videoWall) return (null); - return <video className={"videoBox-content"} key="video" - ref={r => { - this._videoRef = r; - setTimeout(() => { - if (this.rootDoc.mediaState === "pendingRecording" && this._videoRef) { - this.toggleRecording(); - } - }, 100); - }} - autoPlay={this._screenCapture} - style={{ width: this._screenCapture ? "100%" : undefined, height: this._screenCapture ? "100%" : undefined }} - onCanPlay={this.videoLoad} - controls={true} - onClick={e => e.preventDefault()}> - <source type="video/mp4" /> - Not supported. - </video>; - } + specificContextMenu = (e: React.MouseEvent): void => { + const subitems = [{ description: 'Screen Capture', event: this.toggleRecording, icon: 'expand-arrows-alt' as any }]; + ContextMenu.Instance.addItem({ description: 'Options...', subitems, icon: 'video' }); + }; - // _numScreens = 5; - // _camera: Camera | undefined; - // @observable _raised = [] as { coord: Vector2, off: Vector3 }[]; - // @action setRaised = (r: { coord: Vector2, off: Vector3 }[]) => this._raised = r; - @computed get threed() { - // if (this.rootDoc.videoWall) { - // const screens: any[] = []; - // const colors = ["yellow", "red", "orange", "brown", "maroon", "gray"]; - // let count = 0; - // numberRange(this._numScreens).forEach(x => numberRange(this._numScreens).forEach(y => screens.push( - // <VideoTile rootDoc={this.rootDoc} color={colors[count++ % colors.length]} x={x} y={y} raised={this._raised} setRaised={this.setRaised} />))); - // return <Canvas key="canvas" id="CANCAN" style={{ width: this.props.PanelWidth(), height: this.props.PanelHeight() }} gl={{ antialias: false }} colorManagement={false} onCreated={props => { - // this._camera = props.camera; - // props.camera.position.set(this._numScreens / 2, this._numScreens / 2, this._numScreens - 2); - // props.camera.lookAt(this._numScreens / 2, this._numScreens / 2, 0); - // }}> - // {/* <ambientLight />*/} - // <pointLight position={[10, 10, 10]} intensity={1} /> - // {screens} - // </ Canvas>; - // } - return (null); - } - toggleRecording = async () => { - if (!this._screenCapture) { - this._audioRec = new MediaRecorder(await navigator.mediaDevices.getUserMedia({ audio: true })); - const aud_chunks: any = []; - this._audioRec.ondataavailable = (e: any) => aud_chunks.push(e.data); - this._audioRec.onstop = async (e: any) => { - const [{ result }] = await Networking.UploadFilesToServer(aud_chunks); - if (!(result instanceof Error)) { - this.dataDoc[this.props.fieldKey + "-audio"] = new AudioField(result.accessPaths.agnostic.client); + @computed get content() { + if (this.rootDoc.videoWall) return null; + return ( + <video + className={'videoBox-content'} + key="video" + ref={r => { + this._videoRef = r; + setTimeout(() => { + if (this.rootDoc.mediaState === 'pendingRecording' && this._videoRef) { + this.toggleRecording(); } - }; - 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.ondataavailable = (e: any) => vid_chunks.push(e.data); - this._videoRec.onstop = async (e: any) => { - console.log("screenshotbox: upload") - 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; - if (!(result instanceof Error)) { // convert this screenshotBox into normal videoBox - this.dataDoc.type = DocumentType.VID; - this.layoutDoc.layout = VideoBox.LayoutString(this.fieldKey); - this.dataDoc.nativeWidth = this.dataDoc.nativeHeight = undefined; - this.layoutDoc._fitWidth = undefined; - this.dataDoc[this.props.fieldKey] = new VideoField(result.accessPaths.agnostic.client); - } else alert("video conversion failed"); - }; - this._audioRec.start(); - this._videoRec.start(); - runInAction(() => { - this._screenCapture = true; - this.dataDoc.mediaState = "recording"; - }); - DocUtils.ActiveRecordings.push(this); - } else { - this._audioRec?.stop(); - this._videoRec?.stop(); - runInAction(() => { - this._screenCapture = false; - this.dataDoc.mediaState = "paused"; - }); - const ind = DocUtils.ActiveRecordings.indexOf(this); - ind !== -1 && (DocUtils.ActiveRecordings.splice(ind, 1)); + }, 100); + }} + autoPlay={this._screenCapture} + style={{ width: this._screenCapture ? '100%' : undefined, height: this._screenCapture ? '100%' : undefined }} + onCanPlay={this.videoLoad} + controls={true} + onClick={e => e.preventDefault()}> + <source type="video/mp4" /> + Not supported. + </video> + ); + } + + // _numScreens = 5; + // _camera: Camera | undefined; + // @observable _raised = [] as { coord: Vector2, off: Vector3 }[]; + // @action setRaised = (r: { coord: Vector2, off: Vector3 }[]) => this._raised = r; + @computed get threed() { + // if (this.rootDoc.videoWall) { + // const screens: any[] = []; + // const colors = ["yellow", "red", "orange", "brown", "maroon", "gray"]; + // let count = 0; + // numberRange(this._numScreens).forEach(x => numberRange(this._numScreens).forEach(y => screens.push( + // <VideoTile rootDoc={this.rootDoc} color={colors[count++ % colors.length]} x={x} y={y} raised={this._raised} setRaised={this.setRaised} />))); + // return <Canvas key="canvas" id="CANCAN" style={{ width: this.props.PanelWidth(), height: this.props.PanelHeight() }} gl={{ antialias: false }} colorManagement={false} onCreated={props => { + // this._camera = props.camera; + // props.camera.position.set(this._numScreens / 2, this._numScreens / 2, this._numScreens - 2); + // props.camera.lookAt(this._numScreens / 2, this._numScreens / 2, 0); + // }}> + // {/* <ambientLight />*/} + // <pointLight position={[10, 10, 10]} intensity={1} /> + // {screens} + // </ Canvas>; + // } + return null; + } + toggleRecording = async () => { + if (!this._screenCapture) { + this._audioRec = new MediaRecorder(await navigator.mediaDevices.getUserMedia({ audio: true })); + const aud_chunks: any = []; + this._audioRec.ondataavailable = (e: any) => aud_chunks.push(e.data); + this._audioRec.onstop = async (e: any) => { + const [{ result }] = await Networking.UploadFilesToServer(aud_chunks); + if (!(result instanceof Error)) { + this.dataDoc[this.props.fieldKey + '-audio'] = new AudioField(result.accessPaths.agnostic.client); + } + }; + this._videoRef!.srcObject = await (navigator.mediaDevices as any).getDisplayMedia({ video: true }); + this._videoRec = new MediaRecorder(this._videoRef!.srcObject); + const vid_chunks: any = []; + this._videoRec.onstart = () => (this.dataDoc[this.props.fieldKey + '-recordingStart'] = new DateField(new Date())); + this._videoRec.ondataavailable = (e: any) => vid_chunks.push(e.data); + this._videoRec.onstop = async (e: any) => { + console.log('screenshotbox: upload'); + const 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; + if (!(result instanceof Error)) { + // convert this screenshotBox into normal videoBox + this.dataDoc.type = DocumentType.VID; + this.layoutDoc.layout = VideoBox.LayoutString(this.fieldKey); + this.dataDoc.nativeWidth = this.dataDoc.nativeHeight = undefined; + this.layoutDoc._fitWidth = undefined; + this.dataDoc[this.props.fieldKey] = new VideoField(result.accessPaths.agnostic.client); + } else alert('video conversion failed'); + }; + this._audioRec.start(); + this._videoRec.start(); + runInAction(() => { + this._screenCapture = true; + this.dataDoc.mediaState = 'recording'; + }); + DocUtils.ActiveRecordings.push(this); + } else { + this._audioRec?.stop(); + this._videoRec?.stop(); + runInAction(() => { + this._screenCapture = false; + this.dataDoc.mediaState = 'paused'; + }); + const ind = DocUtils.ActiveRecordings.indexOf(this); + ind !== -1 && DocUtils.ActiveRecordings.splice(ind, 1); - CaptureManager.Instance.open(this.rootDoc); - } - } + CaptureManager.Instance.open(this.rootDoc); + } + }; - setupDictation = () => { - if (this.dataDoc[this.fieldKey + "-dictation"]) return; - const dictationText = CurrentUserUtils.GetNewTextDoc("dictation", - NumCast(this.rootDoc.x), NumCast(this.rootDoc.y) + NumCast(this.layoutDoc._height) + 10, - NumCast(this.layoutDoc._width), 2 * NumCast(this.layoutDoc._height)); - dictationText._autoHeight = false; - const dictationTextProto = Doc.GetProto(dictationText); - dictationTextProto.recordingSource = this.dataDoc; - dictationTextProto.recordingStart = ComputedField.MakeFunction(`self.recordingSource["${this.props.fieldKey}-recordingStart"]`); - dictationTextProto.mediaState = ComputedField.MakeFunction("self.recordingSource.mediaState"); - this.dataDoc[this.fieldKey + "-dictation"] = dictationText; - } - contentFunc = () => [this.threed, this.content]; - videoPanelHeight = () => NumCast(this.dataDoc[this.fieldKey + "-nativeHeight"], this.layoutDoc[HeightSym]()) / NumCast(this.dataDoc[this.fieldKey + "-nativeWidth"], this.layoutDoc[WidthSym]()) * this.props.PanelWidth(); - formattedPanelHeight = () => Math.max(0, this.props.PanelHeight() - this.videoPanelHeight()); - render() { - TraceMobx(); - return <div className="videoBox" onContextMenu={this.specificContextMenu} style={{ width: "100%", height: "100%" }} > - <div className="videoBox-viewer" > - <div style={{ position: "relative", height: this.videoPanelHeight() }}> - <CollectionFreeFormView {...OmitKeys(this.props, ["NativeWidth", "NativeHeight"]).omit} - PanelHeight={this.videoPanelHeight} - PanelWidth={this.props.PanelWidth} - focus={this.props.focus} - isSelected={this.props.isSelected} - isAnnotationOverlay={true} - select={emptyFunction} - isContentActive={returnFalse} - scaling={returnOne} - whenChildContentsActiveChanged={emptyFunction} - removeDocument={returnFalse} - moveDocument={returnFalse} - addDocument={returnFalse} - CollectionView={undefined} - ScreenToLocalTransform={this.props.ScreenToLocalTransform} - renderDepth={this.props.renderDepth + 1} - ContainingCollectionDoc={this.props.ContainingCollectionDoc}> - {this.contentFunc} - </CollectionFreeFormView></div> - <div style={{ position: "relative", height: this.formattedPanelHeight() }}> - {!(this.dataDoc[this.fieldKey + "-dictation"] instanceof Doc) ? (null) : - <FormattedTextBox {...OmitKeys(this.props, ["NativeWidth", "NativeHeight"]).omit} - Document={this.dataDoc[this.fieldKey + "-dictation"]} - fieldKey={"text"} - PanelHeight={this.formattedPanelHeight} - isAnnotationOverlay={true} - select={emptyFunction} - isContentActive={emptyFunction} - scaling={returnOne} - xPadding={25} - yPadding={10} - whenChildContentsActiveChanged={emptyFunction} - removeDocument={returnFalse} - moveDocument={returnFalse} - addDocument={returnFalse} - CollectionView={undefined} - renderDepth={this.props.renderDepth + 1} - ContainingCollectionDoc={this.props.ContainingCollectionDoc}> - </FormattedTextBox>} - </div> - </div> - {!this.props.isSelected() ? (null) : <div className="screenshotBox-uiButtons"> - <div className="screenshotBox-recorder" key="snap" onPointerDown={this.toggleRecording} > - <FontAwesomeIcon icon="file" size="lg" /> + setupDictation = () => { + if (this.dataDoc[this.fieldKey + '-dictation']) return; + const dictationText = DocUtils.GetNewTextDoc('dictation', NumCast(this.rootDoc.x), NumCast(this.rootDoc.y) + NumCast(this.layoutDoc._height) + 10, NumCast(this.layoutDoc._width), 2 * NumCast(this.layoutDoc._height)); + dictationText._autoHeight = false; + const dictationTextProto = Doc.GetProto(dictationText); + dictationTextProto.recordingSource = this.dataDoc; + dictationTextProto.recordingStart = ComputedField.MakeFunction(`self.recordingSource["${this.props.fieldKey}-recordingStart"]`); + dictationTextProto.mediaState = ComputedField.MakeFunction('self.recordingSource.mediaState'); + this.dataDoc[this.fieldKey + '-dictation'] = dictationText; + }; + contentFunc = () => [this.threed, this.content]; + videoPanelHeight = () => (NumCast(this.dataDoc[this.fieldKey + '-nativeHeight'], this.layoutDoc[HeightSym]()) / NumCast(this.dataDoc[this.fieldKey + '-nativeWidth'], this.layoutDoc[WidthSym]())) * this.props.PanelWidth(); + formattedPanelHeight = () => Math.max(0, this.props.PanelHeight() - this.videoPanelHeight()); + render() { + TraceMobx(); + return ( + <div className="videoBox" onContextMenu={this.specificContextMenu} style={{ width: '100%', height: '100%' }}> + <div className="videoBox-viewer"> + <div style={{ position: 'relative', height: this.videoPanelHeight() }}> + <CollectionFreeFormView + {...OmitKeys(this.props, ['NativeWidth', 'NativeHeight']).omit} + PanelHeight={this.videoPanelHeight} + PanelWidth={this.props.PanelWidth} + focus={this.props.focus} + isSelected={this.props.isSelected} + isAnnotationOverlay={true} + select={emptyFunction} + isContentActive={returnFalse} + scaling={returnOne} + whenChildContentsActiveChanged={emptyFunction} + removeDocument={returnFalse} + moveDocument={returnFalse} + addDocument={returnFalse} + CollectionView={undefined} + ScreenToLocalTransform={this.props.ScreenToLocalTransform} + renderDepth={this.props.renderDepth + 1} + ContainingCollectionDoc={this.props.ContainingCollectionDoc}> + {this.contentFunc} + </CollectionFreeFormView> + </div> + <div style={{ position: 'relative', height: this.formattedPanelHeight() }}> + {!(this.dataDoc[this.fieldKey + '-dictation'] instanceof Doc) ? null : ( + <FormattedTextBox + {...OmitKeys(this.props, ['NativeWidth', 'NativeHeight']).omit} + Document={this.dataDoc[this.fieldKey + '-dictation']} + fieldKey={'text'} + PanelHeight={this.formattedPanelHeight} + isAnnotationOverlay={true} + select={emptyFunction} + isContentActive={emptyFunction} + scaling={returnOne} + xPadding={25} + yPadding={10} + whenChildContentsActiveChanged={emptyFunction} + removeDocument={returnFalse} + moveDocument={returnFalse} + addDocument={returnFalse} + CollectionView={undefined} + renderDepth={this.props.renderDepth + 1} + ContainingCollectionDoc={this.props.ContainingCollectionDoc}></FormattedTextBox> + )} + </div> + </div> + {!this.props.isSelected() ? null : ( + <div className="screenshotBox-uiButtons"> + <div className="screenshotBox-recorder" key="snap" onPointerDown={this.toggleRecording}> + <FontAwesomeIcon icon="file" size="lg" /> </div> - </div>} - </div >; - } -}
\ No newline at end of file + </div> + )} + </div> + ); + } +} diff --git a/src/client/views/nodes/VideoBox.tsx b/src/client/views/nodes/VideoBox.tsx index e833c7e30..b1f7d8023 100644 --- a/src/client/views/nodes/VideoBox.tsx +++ b/src/client/views/nodes/VideoBox.tsx @@ -1,44 +1,42 @@ -import React = require("react"); -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { action, computed, IReactionDisposer, observable, ObservableMap, reaction, runInAction, untracked } from "mobx"; -import { observer } from "mobx-react"; -import { basename } from "path"; +import React = require('react'); +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { action, computed, IReactionDisposer, observable, ObservableMap, reaction, runInAction, untracked } from 'mobx'; +import { observer } from 'mobx-react'; +import { basename } from 'path'; import * as rp from 'request-promise'; -import { Doc, DocListCast, HeightSym, WidthSym } from "../../../fields/Doc"; -import { InkTool } from "../../../fields/InkField"; -import { List } from "../../../fields/List"; -import { Cast, NumCast, StrCast } from "../../../fields/Types"; -import { AudioField, ImageField, VideoField } from "../../../fields/URLField"; -import { emptyFunction, formatTime, OmitKeys, returnFalse, returnOne, setupMoveUpEvents, Utils } from "../../../Utils"; -import { Docs, DocUtils } from "../../documents/Documents"; -import { DocumentType } from "../../documents/DocumentTypes"; -import { Networking } from "../../Network"; -import { CurrentUserUtils } from "../../util/CurrentUserUtils"; -import { DocumentManager } from "../../util/DocumentManager"; -import { SelectionManager } from "../../util/SelectionManager"; -import { SnappingManager } from "../../util/SnappingManager"; -import { undoBatch } from "../../util/UndoManager"; -import { CollectionFreeFormView } from "../collections/collectionFreeForm/CollectionFreeFormView"; -import { CollectionStackedTimeline, TrimScope } from "../collections/CollectionStackedTimeline"; -import { ContextMenu } from "../ContextMenu"; -import { ContextMenuProps } from "../ContextMenuItem"; -import { ViewBoxAnnotatableComponent, ViewBoxAnnotatableProps } from "../DocComponent"; -import { DocumentDecorations } from "../DocumentDecorations"; -import { MarqueeAnnotator } from "../MarqueeAnnotator"; -import { AnchorMenu } from "../pdf/AnchorMenu"; -import { StyleProp } from "../StyleProvider"; +import { Doc, DocListCast, HeightSym, WidthSym } from '../../../fields/Doc'; +import { InkTool } from '../../../fields/InkField'; +import { List } from '../../../fields/List'; +import { Cast, NumCast, StrCast } from '../../../fields/Types'; +import { AudioField, ImageField, VideoField } from '../../../fields/URLField'; +import { emptyFunction, formatTime, OmitKeys, returnFalse, returnOne, setupMoveUpEvents, Utils } from '../../../Utils'; +import { Docs, DocUtils } from '../../documents/Documents'; +import { DocumentType } from '../../documents/DocumentTypes'; +import { Networking } from '../../Network'; +import { DocumentManager } from '../../util/DocumentManager'; +import { ReplayMovements } from '../../util/ReplayMovements'; +import { SelectionManager } from '../../util/SelectionManager'; +import { SnappingManager } from '../../util/SnappingManager'; +import { undoBatch } from '../../util/UndoManager'; +import { CollectionFreeFormView } from '../collections/collectionFreeForm/CollectionFreeFormView'; +import { CollectionStackedTimeline, TrimScope } from '../collections/CollectionStackedTimeline'; +import { ContextMenu } from '../ContextMenu'; +import { ContextMenuProps } from '../ContextMenuItem'; +import { ViewBoxAnnotatableComponent, ViewBoxAnnotatableProps } from '../DocComponent'; +import { DocumentDecorations } from '../DocumentDecorations'; +import { MarqueeAnnotator } from '../MarqueeAnnotator'; +import { AnchorMenu } from '../pdf/AnchorMenu'; +import { StyleProp } from '../StyleProvider'; import { FieldView, FieldViewProps } from './FieldView'; -import "./VideoBox.scss"; -import { Presentation } from "../../util/TrackMovements"; -import { RecordingBox } from "./RecordingBox"; -import { ReplayMovements } from "../../util/ReplayMovements"; +import { RecordingBox } from './RecordingBox'; +import './VideoBox.scss'; const path = require('path'); /** * VideoBox * Main component: VideoBox.tsx * Supporting Components: CollectionStackedTimeline - * + * * VideoBox is a node that supports the playback of video files in Dash. * When a video file or YouTube video is importeed into Dash, it is immediately rendered as a VideoBox document. * CollectionStackedTimline handles AudioBox and VideoBox shared behavior, but VideoBox handles playing, pausing, etc because it contains <video> element @@ -48,7 +46,9 @@ const path = require('path'); @observer export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps & FieldViewProps>() { - public static LayoutString(fieldKey: string) { return FieldView.LayoutString(VideoBox, fieldKey); } + public static LayoutString(fieldKey: string) { + return FieldView.LayoutString(VideoBox, fieldKey); + } /** * Uploads an image buffer to the server and stores with specified filename. by default the image * is stored at multiple resolutions each retrieved by using the filename appended with _o, _s, _m, _l (indicating original, small, medium, or large) @@ -58,20 +58,19 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp */ public static async convertDataUri(imageUri: string, returnedFilename: string, nosuffix = false, replaceRootFilename?: string) { try { - const posting = Utils.prepend("/uploadURI"); + const posting = Utils.prepend('/uploadURI'); const returnedUri = await rp.post(posting, { body: { uri: imageUri, name: returnedFilename, nosuffix, - replaceRootFilename + replaceRootFilename, }, json: true, }); return returnedUri; - } catch (e) { - console.log("VideoBox :" + e); + console.log('VideoBox :' + e); } } @@ -103,27 +102,29 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp @observable _finished: boolean = false; // has playback reached end of clip @observable _volume: number = 1; @observable _muted: boolean = false; - @observable _controlsTransform?: { X: number, Y: number }; + @observable _controlsTransform?: { X: number; Y: number }; @observable _controlsVisible: boolean = true; @observable _scrubbing: boolean = false; - @computed get links() { return DocListCast(this.dataDoc.links); } - @computed get heightPercent() { return NumCast(this.layoutDoc._timelineHeightPercent, 100); } // current percent of video relative to VideoBox height + @computed get links() { + return DocListCast(this.dataDoc.links); + } + @computed get heightPercent() { + return NumCast(this.layoutDoc._timelineHeightPercent, 100); + } // current percent of video relative to VideoBox height // @computed get rawDuration() { return NumCast(this.dataDoc[this.fieldKey + "-duration"]); } @observable rawDuration: number = 0; - @computed get youtubeVideoId() { const field = Cast(this.dataDoc[this.props.fieldKey], VideoField); - return field && field.url.href.indexOf("youtube") !== -1 ? ((arr: string[]) => arr[arr.length - 1])(field.url.href.split("/")) : ""; + return field && field.url.href.indexOf('youtube') !== -1 ? ((arr: string[]) => arr[arr.length - 1])(field.url.href.split('/')) : ''; } - // returns the path of the audio file @computed get audiopath() { const field = Cast(this.props.Document[this.props.fieldKey + '-audio'], AudioField, null); const vfield = Cast(this.dataDoc[this.fieldKey], VideoField, null); - return field?.url.href ?? vfield?.url.href ?? ""; + return field?.url.href ?? vfield?.url.href ?? ''; } // returns the presentation data if it exists, null otherwise @@ -132,10 +133,15 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp return data ? JSON.parse(data) : null; } - @computed private get timeline() { return this._stackedTimeline; } - private get transition() { return this._clicking ? "left 0.5s, width 0.5s, height 0.5s" : ""; } // css transition for hiding/showing timeline - public get player(): HTMLVideoElement | null { return this._videoRef; } - + @computed private get timeline() { + return this._stackedTimeline; + } + private get transition() { + return this._clicking ? 'left 0.5s, width 0.5s, height 0.5s' : ''; + } // css transition for hiding/showing timeline + public get player(): HTMLVideoElement | null { + return this._videoRef; + } componentDidMount() { 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. @@ -150,7 +156,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp } } this.player && this.setPlayheadTime(0); - document.addEventListener("keydown", this.keyEvents, true); + document.addEventListener('keydown', this.keyEvents, true); if (this.presentation) { ReplayMovements.Instance.setVideoBox(this); @@ -161,9 +167,9 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp this.removeCurrentlyPlaying(); this.Pause(); Object.keys(this._disposers).forEach(d => this._disposers[d]?.()); - document.removeEventListener("keydown", this.keyEvents, true); + document.removeEventListener('keydown', this.keyEvents, true); - if (this.presentation) { + if (this.presentation) { ReplayMovements.Instance.removeVideoBox(); } } @@ -173,20 +179,23 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp keyEvents = (e: KeyboardEvent) => { if ( // need to include range inputs because after dragging time slider it becomes target element - !(e.target instanceof HTMLInputElement && !(e.target.type === "range")) && + !(e.target instanceof HTMLInputElement && !(e.target.type === 'range')) && this.props.isSelected(true) ) { switch (e.key) { - case "ArrowLeft": - case "ArrowRight": + case 'ArrowLeft': + case 'ArrowRight': clearTimeout(this._controlsFadeTimer); this._scrubbing = true; - this._controlsFadeTimer = setTimeout(action(() => this._scrubbing = false), 500); + this._controlsFadeTimer = setTimeout( + action(() => (this._scrubbing = false)), + 500 + ); e.stopPropagation(); break; } } - } + }; // plays video @action public Play = (update: boolean = true) => { @@ -210,18 +219,18 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp update && this._youtubePlayer?.playVideo(); this._youtubePlayer && !this._playTimer && (this._playTimer = setInterval(this.updateTimecode, 5)); } catch (e) { - console.log("Video Play Exception:", e); + console.log('Video Play Exception:', e); } } this.updateTimecode(); - } + }; // goes to time @action public Seek(time: number) { try { this._youtubePlayer?.seekTo(Math.round(time), true); } catch (e) { - console.log("Video Seek Exception:", e); + console.log('Video Seek Exception:', e); } this.player && (this.player.currentTime = time); this._audioPlayer && (this._audioPlayer.currentTime = time); @@ -242,7 +251,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp this._youtubePlayer && this._playTimer && clearInterval(this._playTimer); this._youtubePlayer?.seekTo(this._youtubePlayer?.getCurrentTime(), true); } catch (e) { - console.log("Video Pause Exception:", e); + console.log('Video Pause Exception:', e); } this._youtubePlayer && SelectionManager.DeselectAll(); // if we don't deselect the player, then we get an annoying YouTube spinner I guess telling us we're paused. this._playTimer = undefined; @@ -251,24 +260,23 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp clearTimeout(this._playRegionTimer); // if paused in the middle of playback, prevents restart on next play } this._playRegionTimer = undefined; - } + }; // toggles video full screen @action public FullScreen = () => { if (document.fullscreenElement === this._contentRef) { this._fullScreen = false; this.player && this._contentRef && document.exitFullscreen(); - } - else { + } else { this._fullScreen = true; this.player && this._contentRef && this._contentRef.requestFullscreen(); } try { - this._youtubePlayer && this.props.addDocTab(this.rootDoc, "add"); + this._youtubePlayer && this.props.addDocTab(this.rootDoc, 'add'); } catch (e) { - console.log("Video FullScreen Exception:", e); + console.log('Video FullScreen Exception:', e); } - } + }; // fades out controls in fullscreen after mouse stops moving @action controlsFade = (e: PointerEvent) => { @@ -276,10 +284,12 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp if (!this._scrubbing) { clearTimeout(this._controlsFadeTimer); this._controlsVisible = true; - this._controlsFadeTimer = setTimeout(action(() => this._controlsVisible = false), 3000); + this._controlsFadeTimer = setTimeout( + action(() => (this._controlsVisible = false)), + 3000 + ); } - } - + }; // drag controls around window in fulls screen @action controlsDrag = (e: React.PointerEvent) => { @@ -288,7 +298,8 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp const eleStyle = getComputedStyle(e.target as Element); this._controlsTransform = { X: parseInt(eleStyle.left), Y: parseInt(eleStyle.top) }; - setupMoveUpEvents(e.target, + setupMoveUpEvents( + e.target, e, action((e, down, delta) => { if (this._controlsTransform) { @@ -298,32 +309,35 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp return false; }), emptyFunction, - emptyFunction) - } - + emptyFunction + ); + }; // creates and links snapshot photo of current video frame @action public Snapshot = (downX?: number, downY?: number, cb?: (filename: string, x: number | undefined, y: number | undefined) => void) => { const width = NumCast(this.layoutDoc._width); const canvas = document.createElement('canvas'); canvas.width = 640; - canvas.height = 640 * Doc.NativeHeight(this.layoutDoc) / (Doc.NativeWidth(this.layoutDoc) || 1); - const ctx = canvas.getContext('2d');//draw image to canvas. scale to target dimensions + canvas.height = (640 * Doc.NativeHeight(this.layoutDoc)) / (Doc.NativeWidth(this.layoutDoc) || 1); + const ctx = canvas.getContext('2d'); //draw image to canvas. scale to target dimensions if (ctx) { this._videoRef && ctx.drawImage(this._videoRef, 0, 0, canvas.width, canvas.height); } if (!this._videoRef) { const b = Docs.Create.LabelDocument({ - x: NumCast(this.layoutDoc.x) + width, y: NumCast(this.layoutDoc.y, 1), - _width: 150, _height: 50, title: (this.layoutDoc._currentTimecode || 0).toString(), - _isLinkButton: true + x: NumCast(this.layoutDoc.x) + width, + y: NumCast(this.layoutDoc.y, 1), + _width: 150, + _height: 50, + title: (this.layoutDoc._currentTimecode || 0).toString(), + _isLinkButton: true, }); this.props.addDocument?.(b); - DocUtils.MakeLink({ doc: b }, { doc: this.rootDoc }, "video snapshot"); - Networking.PostToServer("/youtubeScreenshot", { + DocUtils.MakeLink({ doc: b }, { doc: this.rootDoc }, 'video snapshot'); + Networking.PostToServer('/youtubeScreenshot', { id: this.youtubeVideoId, - timecode: this.layoutDoc._currentTimecode + timecode: this.layoutDoc._currentTimecode, }).then(response => { const resolved = response?.accessPaths?.agnostic?.client; if (resolved) { @@ -335,49 +349,50 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp //convert to desired file format const dataUrl = canvas.toDataURL('image/png'); // can also use 'image/png' // if you want to preview the captured image, - const retitled = StrCast(this.rootDoc.title).replace(/[ -\.:]/g, ""); - const encodedFilename = encodeURIComponent("snapshot" + retitled + "_" + (this.layoutDoc._currentTimecode || 0).toString().replace(/\./, "_")); + const retitled = StrCast(this.rootDoc.title).replace(/[ -\.:]/g, ''); + const encodedFilename = encodeURIComponent('snapshot' + retitled + '_' + (this.layoutDoc._currentTimecode || 0).toString().replace(/\./, '_')); const filename = basename(encodedFilename); - VideoBox.convertDataUri(dataUrl, filename).then((returnedFilename: string) => - returnedFilename && (cb ?? this.createRealSummaryLink)(returnedFilename, downX, downY)); + VideoBox.convertDataUri(dataUrl, filename).then((returnedFilename: string) => returnedFilename && (cb ?? this.createRealSummaryLink)(returnedFilename, downX, downY)); } - } + }; updateIcon = () => { const makeIcon = (returnedfilename: string) => { this.dataDoc.icon = new ImageField(returnedfilename); - this.dataDoc["icon-nativeWidth"] = this.layoutDoc[WidthSym](); - this.dataDoc["icon-nativeHeight"] = this.layoutDoc[HeightSym](); + this.dataDoc['icon-nativeWidth'] = this.layoutDoc[WidthSym](); + this.dataDoc['icon-nativeHeight'] = this.layoutDoc[HeightSym](); }; this.Snapshot(undefined, undefined, makeIcon); - } + }; // creates link for snapshot createRealSummaryLink = (imagePath: string, downX?: number, downY?: number) => { - const url = !imagePath.startsWith("/") ? Utils.CorsProxy(imagePath) : imagePath; + const url = !imagePath.startsWith('/') ? Utils.CorsProxy(imagePath) : imagePath; const width = NumCast(this.layoutDoc._width) || 1; const height = NumCast(this.layoutDoc._height); const imageSummary = Docs.Create.ImageDocument(url, { - _nativeWidth: Doc.NativeWidth(this.layoutDoc), _nativeHeight: Doc.NativeHeight(this.layoutDoc), - x: NumCast(this.layoutDoc.x) + width, y: NumCast(this.layoutDoc.y), _isLinkButton: true, - _width: 150, _height: height / width * 150, title: "--snapshot" + NumCast(this.layoutDoc._currentTimecode) + " image-" + _nativeWidth: Doc.NativeWidth(this.layoutDoc), + _nativeHeight: Doc.NativeHeight(this.layoutDoc), + x: NumCast(this.layoutDoc.x) + width, + y: NumCast(this.layoutDoc.y), + _isLinkButton: true, + _width: 150, + _height: (height / width) * 150, + title: '--snapshot' + NumCast(this.layoutDoc._currentTimecode) + ' image-', }); Doc.SetNativeWidth(Doc.GetProto(imageSummary), Doc.NativeWidth(this.layoutDoc)); Doc.SetNativeHeight(Doc.GetProto(imageSummary), Doc.NativeHeight(this.layoutDoc)); this.props.addDocument?.(imageSummary); - const link = DocUtils.MakeLink({ doc: imageSummary }, { doc: this.getAnchor() }, "video snapshot"); + const link = DocUtils.MakeLink({ doc: imageSummary }, { doc: this.getAnchor() }, 'video snapshot'); link && (Doc.GetProto(link.anchor2 as Doc).timecodeToHide = NumCast((link.anchor2 as Doc).timecodeToShow) + 3); - setTimeout(() => - (downX !== undefined && downY !== undefined) && DocumentManager.Instance.getFirstDocumentView(imageSummary)?.startDragging(downX, downY, "move", true)); - } - + setTimeout(() => downX !== undefined && downY !== undefined && DocumentManager.Instance.getFirstDocumentView(imageSummary)?.startDragging(downX, downY, 'move', true)); + }; getAnchor = () => { - const timecode = Cast(this.layoutDoc._currentTimecode, "number", null); + const timecode = Cast(this.layoutDoc._currentTimecode, 'number', null); const marquee = AnchorMenu.Instance.GetAnchor?.(); - return CollectionStackedTimeline.createAnchor(this.rootDoc, this.dataDoc, this.annotationKey, "_timecodeToShow"/* videoStart */, "_timecodeToHide" /* videoEnd */, timecode ? timecode : undefined, undefined, marquee) || this.rootDoc; - } - + return CollectionStackedTimeline.createAnchor(this.rootDoc, this.dataDoc, this.annotationKey, '_timecodeToShow' /* videoStart */, '_timecodeToHide' /* videoEnd */, timecode ? timecode : undefined, undefined, marquee) || this.rootDoc; + }; // sets video info on load videoLoad = action(() => { @@ -387,10 +402,9 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp this.layoutDoc._height = NumCast(this.layoutDoc._width) / aspect; if (Number.isFinite(this.player!.duration)) { this.rawDuration = this.player!.duration; - } else this.rawDuration = NumCast(this.dataDoc[this.fieldKey + "-duration"]); + } else this.rawDuration = NumCast(this.dataDoc[this.fieldKey + '-duration']); }); - // updates video time @action updateTimecode = () => { @@ -398,10 +412,9 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp try { this._youtubePlayer && (this.layoutDoc._currentTimecode = this._youtubePlayer.getCurrentTime?.()); } catch (e) { - console.log("Video Timecode Exception:", e); + console.log('Video Timecode Exception:', e); } - } - + }; // extracts video thumbnails and saves them as field of doc getVideoThumbnails = () => { @@ -419,23 +432,23 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp const ctx = canvas.getContext('2d'); ctx?.drawImage(video, 0, 0, canvas.width, canvas.height); const imgUrl = canvas.toDataURL(); - const retitled = StrCast(this.rootDoc.title).replace(/[ -\.:]/g, ""); - const encodedFilename = encodeURIComponent("thumbnail" + retitled + "_" + video.currentTime.toString().replace(/\./, "_")); + const retitled = StrCast(this.rootDoc.title).replace(/[ -\.:]/g, ''); + const encodedFilename = encodeURIComponent('thumbnail' + retitled + '_' + video.currentTime.toString().replace(/\./, '_')); const filename = basename(encodedFilename); thumbnailPromises.push(VideoBox.convertDataUri(imgUrl, filename)); const newTime = video.currentTime + video.duration / (VideoBox.numThumbnails - 1); if (newTime < video.duration) { video.currentTime = newTime; + } else { + Promise.all(thumbnailPromises).then(thumbnails => { + this.dataDoc.thumbnails = new List<string>(thumbnails); + }); } - else { - Promise.all(thumbnailPromises).then(thumbnails => { this.dataDoc.thumbnails = new List<string>(thumbnails); }); - } - } + }; const field = Cast(this.dataDoc[this.fieldKey], VideoField); field && (video.src = field.url.href); - } - + }; // sets video element ref @action @@ -446,33 +459,34 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp // @ts-ignore // vref.onfullscreenchange = action((e) => this._fullScreen = vref.webkitDisplayingFullscreen); this._disposers.reactionDisposer?.(); - this._disposers.reactionDisposer = reaction(() => NumCast(this.layoutDoc._currentTimecode), - time => !this._playing && (vref.currentTime = time), { fireImmediately: true }); + this._disposers.reactionDisposer = reaction( + () => NumCast(this.layoutDoc._currentTimecode), + time => !this._playing && (vref.currentTime = time), + { fireImmediately: true } + ); (!this.dataDoc.thumbnails || this.dataDoc.thumbnails.length != VideoBox.numThumbnails) && this.getVideoThumbnails(); } - } + }; // set ref for div that wraps video and controls for fullscreen @action setContentRef = (cref: HTMLDivElement | null) => { this._contentRef = cref; if (cref) { - cref.onfullscreenchange = action((e) => { - this._fullScreen = (document.fullscreenElement === cref); + cref.onfullscreenchange = action(e => { + this._fullScreen = document.fullscreenElement === cref; this._controlsVisible = true; this._scrubbing = false; clearTimeout(this._controlsFadeTimer); if (this._fullScreen) { document.addEventListener('pointermove', this.controlsFade); - } - else { + } else { document.removeEventListener('pointermove', this.controlsFade); } }); } - } - + }; // context menu specificContextMenu = (e: React.MouseEvent): void => { @@ -480,143 +494,185 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp if (field) { const url = field.url.href; const subitems: ContextMenuProps[] = []; - subitems.push({ description: "Full Screen", event: this.FullScreen, icon: "expand" }); - subitems.push({ description: "Take Snapshot", event: this.Snapshot, icon: "expand-arrows-alt" }); - this.rootDoc.type === DocumentType.SCREENSHOT && subitems.push({ - description: "Screen Capture", event: (async () => { - runInAction(() => this._screenCapture = !this._screenCapture); - this._videoRef!.srcObject = !this._screenCapture ? undefined : await (navigator.mediaDevices as any).getDisplayMedia({ video: true }); - }), icon: "expand-arrows-alt" + subitems.push({ description: 'Full Screen', event: this.FullScreen, icon: 'expand' }); + subitems.push({ description: 'Take Snapshot', event: this.Snapshot, icon: 'expand-arrows-alt' }); + this.rootDoc.type === DocumentType.SCREENSHOT && + subitems.push({ + description: 'Screen Capture', + event: async () => { + runInAction(() => (this._screenCapture = !this._screenCapture)); + this._videoRef!.srcObject = !this._screenCapture ? undefined : await (navigator.mediaDevices as any).getDisplayMedia({ video: true }); + }, + icon: 'expand-arrows-alt', + }); + subitems.push({ description: (this.layoutDoc.dontAutoFollowLinks ? '' : "Don't") + ' follow links when encountered', event: () => (this.layoutDoc.dontAutoFollowLinks = !this.layoutDoc.dontAutoFollowLinks), icon: 'expand-arrows-alt' }); + subitems.push({ + description: (this.layoutDoc.dontAutoPlayFollowedLinks ? '' : "Don't") + ' play when link is selected', + event: () => (this.layoutDoc.dontAutoPlayFollowedLinks = !this.layoutDoc.dontAutoPlayFollowedLinks), + icon: 'expand-arrows-alt', }); - subitems.push({ description: (this.layoutDoc.dontAutoFollowLinks ? "" : "Don't") + " follow links when encountered", event: () => this.layoutDoc.dontAutoFollowLinks = !this.layoutDoc.dontAutoFollowLinks, icon: "expand-arrows-alt" }); - subitems.push({ description: (this.layoutDoc.dontAutoPlayFollowedLinks ? "" : "Don't") + " play when link is selected", event: () => this.layoutDoc.dontAutoPlayFollowedLinks = !this.layoutDoc.dontAutoPlayFollowedLinks, icon: "expand-arrows-alt" }); - subitems.push({ description: (this.layoutDoc.autoPlayAnchors ? "Don't auto play" : "Auto play") + " anchors onClick", event: () => this.layoutDoc.autoPlayAnchors = !this.layoutDoc.autoPlayAnchors, icon: "expand-arrows-alt" }); + subitems.push({ description: (this.layoutDoc.autoPlayAnchors ? "Don't auto play" : 'Auto play') + ' anchors onClick', event: () => (this.layoutDoc.autoPlayAnchors = !this.layoutDoc.autoPlayAnchors), icon: 'expand-arrows-alt' }); // subitems.push({ description: "Toggle Native Controls", event: action(() => VideoBox._nativeControls = !VideoBox._nativeControls), icon: "expand-arrows-alt" }); // subitems.push({ description: "Start Trim All", event: () => this.startTrim(TrimScope.All), icon: "expand-arrows-alt" }); // subitems.push({ description: "Start Trim Clip", event: () => this.startTrim(TrimScope.Clip), icon: "expand-arrows-alt" }); // subitems.push({ description: "Stop Trim", event: () => this.finishTrim(), icon: "expand-arrows-alt" }); - subitems.push({ description: "Copy path", event: () => { Utils.CopyText(url); }, icon: "expand-arrows-alt" }); + subitems.push({ + description: 'Copy path', + event: () => { + Utils.CopyText(url); + }, + icon: 'expand-arrows-alt', + }); // if the videobox was turned from a recording box - if (this.dataDoc[this.fieldKey + "-recorded"] === true) { + if (this.dataDoc[this.fieldKey + '-recorded'] === true) { subitems.push({ - description: "Recreate recording", event: () => { - this.dataDoc.layout = RecordingBox.LayoutString(this.fieldKey); - // delete assoicated video data - this.dataDoc[this.props.fieldKey] = ""; - this.dataDoc[this.fieldKey + "-duration"] = ""; - // delete assoicated presentation data - this.dataDoc[this.fieldKey + "-presentation"] = ""; - }, icon: "expand-arrows-alt" + description: 'Recreate recording', + event: () => { + this.dataDoc.layout = RecordingBox.LayoutString(this.fieldKey); + // delete assoicated video data + this.dataDoc[this.props.fieldKey] = ''; + this.dataDoc[this.fieldKey + '-duration'] = ''; + // delete assoicated presentation data + this.dataDoc[this.fieldKey + '-presentation'] = ''; + }, + icon: 'expand-arrows-alt', }); } - ContextMenu.Instance.addItem({ description: "Options...", subitems: subitems, icon: "video" }); + ContextMenu.Instance.addItem({ description: 'Options...', subitems: subitems, icon: 'video' }); } - } - + }; // ref for updating time - setAudioRef = (e: HTMLAudioElement | null) => this._audioPlayer = e; + setAudioRef = (e: HTMLAudioElement | null) => (this._audioPlayer = e); // renders the video and audio @computed get content() { const field = Cast(this.dataDoc[this.fieldKey], VideoField); - const interactive = CurrentUserUtils.ActiveTool !== InkTool.None || !this.props.isSelected() ? "" : "-interactive"; - const classname = "videoBox-content" + (this._fullScreen ? "-fullScreen" : "") + interactive; - const opacity = this._scrubbing ? 0.3 : (this._controlsVisible ? 1 : 0); - return !field ? <div key="loading">Loading</div> : - <div className="videoBox-contentContainer" key="container" style={{ mixBlendMode: "multiply", cursor: this._fullScreen && !this._controlsVisible ? 'none' : 'pointer' }}> - <div className={classname} ref={this.setContentRef} onPointerDown={(e) => this._fullScreen && e.stopPropagation()}> - {this._fullScreen && <div className="videoBox-ui" onPointerDown={this.controlsDrag} - style={{ left: this._controlsTransform && this._controlsTransform.X, top: this._controlsTransform && this._controlsTransform.Y, visibility: this._controlsVisible || this._scrubbing ? 'visible' : 'hidden', opacity: opacity }}> - {this.UIButtons} - </div>} - <video key="video" autoPlay={this._screenCapture} ref={this.setVideoRef} style={this._fullScreen ? this.fullScreenSize() : {}} + const interactive = Doc.ActiveTool !== InkTool.None || !this.props.isSelected() ? '' : '-interactive'; + const classname = 'videoBox-content' + (this._fullScreen ? '-fullScreen' : '') + interactive; + const opacity = this._scrubbing ? 0.3 : this._controlsVisible ? 1 : 0; + return !field ? ( + <div key="loading">Loading</div> + ) : ( + <div className="videoBox-contentContainer" key="container" style={{ mixBlendMode: 'multiply', cursor: this._fullScreen && !this._controlsVisible ? 'none' : 'pointer' }}> + <div className={classname} ref={this.setContentRef} onPointerDown={e => this._fullScreen && e.stopPropagation()}> + {this._fullScreen && ( + <div + className="videoBox-ui" + onPointerDown={this.controlsDrag} + style={{ + left: this._controlsTransform && this._controlsTransform.X, + top: this._controlsTransform && this._controlsTransform.Y, + visibility: this._controlsVisible || this._scrubbing ? 'visible' : 'hidden', + opacity: opacity, + }}> + {this.UIButtons} + </div> + )} + <video + key="video" + autoPlay={this._screenCapture} + ref={this.setVideoRef} + style={this._fullScreen ? this.fullScreenSize() : {}} onCanPlay={this.videoLoad} controls={VideoBox._nativeControls} onPlay={() => this.Play()} onSeeked={this.updateTimecode} onPause={() => this.Pause()} - onClick={this._fullScreen ? () => this.playing() ? this.Pause() : this.Play() : e => e.preventDefault()}> + onClick={this._fullScreen ? () => (this.playing() ? this.Pause() : this.Play()) : e => e.preventDefault()}> <source src={field.url.href} type="video/mp4" /> Not supported. </video> - {!this.audiopath || this.audiopath === field.url.href ? (null) : - <audio ref={this.setAudioRef} className={`audiobox-control${this.props.isContentActive() ? "-interactive" : ""}`}> + {!this.audiopath || this.audiopath === field.url.href ? null : ( + <audio ref={this.setAudioRef} className={`audiobox-control${this.props.isContentActive() ? '-interactive' : ''}`}> <source src={this.audiopath} type="audio/mpeg" /> Not supported. - </audio>} + </audio> + )} </div> - </div>; + </div> + ); } - @action youtubeIframeLoaded = (e: any) => { if (!this._youtubeContentCreated) { this._forceCreateYouTubeIFrame = !this._forceCreateYouTubeIFrame; return; - } - else this._youtubeContentCreated = false; + } else this._youtubeContentCreated = false; this.loadYouTube(e.target); - } + }; loadYouTube = (iframe: any) => { let started = true; - const onYoutubePlayerStateChange = (event: any) => runInAction(() => { - if (started && event.data === YT.PlayerState.PLAYING) { - started = false; - this._youtubePlayer?.unMute(); - //this.Pause(); - return; - } - if (event.data === YT.PlayerState.PLAYING && !this._playing) this.Play(false); - if (event.data === YT.PlayerState.PAUSED && this._playing) this.Pause(false); - }); + const onYoutubePlayerStateChange = (event: any) => + runInAction(() => { + if (started && event.data === YT.PlayerState.PLAYING) { + started = false; + this._youtubePlayer?.unMute(); + //this.Pause(); + return; + } + if (event.data === YT.PlayerState.PLAYING && !this._playing) this.Play(false); + if (event.data === YT.PlayerState.PAUSED && this._playing) this.Pause(false); + }); const onYoutubePlayerReady = (event: any) => { this._disposers.reactionDisposer?.(); this._disposers.youtubeReactionDisposer?.(); - this._disposers.reactionDisposer = reaction(() => this.layoutDoc._currentTimecode, () => !this._playing && this.Seek(NumCast(this.layoutDoc._currentTimecode))); + this._disposers.reactionDisposer = reaction( + () => this.layoutDoc._currentTimecode, + () => !this._playing && this.Seek(NumCast(this.layoutDoc._currentTimecode)) + ); this._disposers.youtubeReactionDisposer = reaction( - () => CurrentUserUtils.ActiveTool === InkTool.None && this.props.isSelected(true) && !SnappingManager.GetIsDragging() && !DocumentDecorations.Instance.Interacting, - (interactive) => iframe.style.pointerEvents = interactive ? "all" : "none", { fireImmediately: true }); + () => Doc.ActiveTool === InkTool.None && this.props.isSelected(true) && !SnappingManager.GetIsDragging() && !DocumentDecorations.Instance.Interacting, + interactive => (iframe.style.pointerEvents = interactive ? 'all' : 'none'), + { fireImmediately: true } + ); }; - if (typeof (YT) === undefined) setTimeout(() => this.loadYouTube(iframe), 100); + if (typeof YT === undefined) setTimeout(() => this.loadYouTube(iframe), 100); else { (YT as any)?.ready(() => { this._youtubePlayer = new YT.Player(`${this.youtubeVideoId + this._youtubeIframeId}-player`, { events: { - 'onReady': this.props.dontRegisterView ? undefined : onYoutubePlayerReady, - 'onStateChange': this.props.dontRegisterView ? undefined : onYoutubePlayerStateChange, - } + onReady: this.props.dontRegisterView ? undefined : onYoutubePlayerReady, + onStateChange: this.props.dontRegisterView ? undefined : onYoutubePlayerStateChange, + }, }); }); } - } - + }; // for play button - onPlayDown = () => this._playing ? this.Pause() : this.Play(); + onPlayDown = () => (this._playing ? this.Pause() : this.Play()); // for fullscreen button onFullDown = (e: React.PointerEvent) => { this.FullScreen(); e.stopPropagation(); e.preventDefault(); - } + }; // for snapshot button onSnapshotDown = (e: React.PointerEvent) => { - setupMoveUpEvents(this, e, (e) => { - this.Snapshot(e.clientX, e.clientY); - return true; - }, emptyFunction, () => this.Snapshot()); - } + setupMoveUpEvents( + this, + e, + e => { + this.Snapshot(e.clientX, e.clientY); + return true; + }, + emptyFunction, + () => this.Snapshot() + ); + }; // for show/hide timeline button, transitions between show/hide @action onTimelineHdlDown = (e: React.PointerEvent) => { this._clicking = true; - setupMoveUpEvents(this, e, + setupMoveUpEvents( + this, + e, action(encodeURIComponent => { this._clicking = false; if (this.props.isContentActive()) { @@ -626,13 +682,19 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp this.layoutDoc._timelineHeightPercent = 80; } return false; - }), emptyFunction, + }), + emptyFunction, () => { this.layoutDoc._timelineHeightPercent = this.heightPercent !== 100 ? 100 : VideoBox.heightPercent; - setTimeout(action(() => this._clicking = false), 500); - }, this.props.isContentActive(), this.props.isContentActive()); - } - + setTimeout( + action(() => (this._clicking = false)), + 500 + ); + }, + this.props.isContentActive(), + this.props.isContentActive() + ); + }; // removes video from currently playing display @action @@ -641,7 +703,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp const index = CollectionStackedTimeline.CurrentlyPlaying.indexOf(this.layoutDoc); index !== -1 && CollectionStackedTimeline.CurrentlyPlaying.splice(index, 1); } - } + }; // adds video to currently playing display @action @@ -652,31 +714,36 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp if (CollectionStackedTimeline.CurrentlyPlaying.indexOf(this.layoutDoc) === -1) { CollectionStackedTimeline.CurrentlyPlaying.push(this.layoutDoc); } - } - + }; @computed get youtubeContent() { this._youtubeIframeId = VideoBox._youtubeIframeCounter++; this._youtubeContentCreated = this._forceCreateYouTubeIFrame ? true : true; - const classname = "videoBox-content-YouTube" + (this._fullScreen ? "-fullScreen" : ""); + const classname = 'videoBox-content-YouTube' + (this._fullScreen ? '-fullScreen' : ''); const start = untracked(() => Math.round(NumCast(this.layoutDoc._currentTimecode))); - return <iframe key={this._youtubeIframeId} id={`${this.youtubeVideoId + this._youtubeIframeId}-player`} - onPointerLeave={this.updateTimecode} - onLoad={this.youtubeIframeLoaded} className={classname} width={Doc.NativeWidth(this.layoutDoc) || 640} height={Doc.NativeHeight(this.layoutDoc) || 390} - src={`https://www.youtube.com/embed/${this.youtubeVideoId}?enablejsapi=1&rel=0&showinfo=1&autoplay=0&mute=1&start=${start}&modestbranding=1&controls=${VideoBox._nativeControls ? 1 : 0}`} />; + return ( + <iframe + key={this._youtubeIframeId} + id={`${this.youtubeVideoId + this._youtubeIframeId}-player`} + onPointerLeave={this.updateTimecode} + onLoad={this.youtubeIframeLoaded} + className={classname} + width={Doc.NativeWidth(this.layoutDoc) || 640} + height={Doc.NativeHeight(this.layoutDoc) || 390} + src={`https://www.youtube.com/embed/${this.youtubeVideoId}?enablejsapi=1&rel=0&showinfo=1&autoplay=0&mute=1&start=${start}&modestbranding=1&controls=${VideoBox._nativeControls ? 1 : 0}`} + /> + ); } - // for annotating, adds doc with time info @action.bound addDocWithTimecode(doc: Doc | Doc[]): boolean { const docs = doc instanceof Doc ? [doc] : doc; const curTime = NumCast(this.layoutDoc._currentTimecode); - docs.forEach(doc => doc._timecodeToHide = (doc._timecodeToShow = curTime) + 1); + docs.forEach(doc => (doc._timecodeToHide = (doc._timecodeToShow = curTime) + 1)); return this.addDocument(doc); } - // play back the audio from seekTimeInSeconds, fullPlay tells whether clip is being played to end vs link range @action playFrom = (seekTimeInSeconds: number, endTime?: number, fullPlay: boolean = false) => { @@ -684,8 +751,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp this._playRegionTimer = undefined; if (Number.isNaN(this.player?.duration)) { setTimeout(() => this.playFrom(seekTimeInSeconds, endTime), 500); - } - else if (this.player) { + } else if (this.player) { // trimBounds override requested playback bounds const end = Math.min(this.timeline?.trimEnd ?? this.rawDuration, endTime ?? this.timeline?.trimEnd ?? this.rawDuration); const start = Math.max(this.timeline?.trimStart ?? 0, seekTimeInSeconds); @@ -698,20 +764,18 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp this._audioPlayer?.play(); this._playing = true; this.addCurrentlyPlaying(); - this._playRegionTimer = setTimeout( - () => { - // need to keep track of if end of clip is reached so on next play, clip restarts - if (fullPlay) this._finished = true; - // removes from currently playing if playback has reached end of range marker - else this.removeCurrentlyPlaying(); - this.Pause(); - }, playRegionDuration * 1000); + this._playRegionTimer = setTimeout(() => { + // need to keep track of if end of clip is reached so on next play, clip restarts + if (fullPlay) this._finished = true; + // removes from currently playing if playback has reached end of range marker + else this.removeCurrentlyPlaying(); + this.Pause(); + }, playRegionDuration * 1000); } else { this.Pause(); } } - } - + }; // ends trim, hides trim controls and displays new clip @undoBatch @@ -725,22 +789,28 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp startTrim = (scope: TrimScope) => { this.Pause(); this.timeline?.StartTrimming(scope); - } + }; // for trim button, double click displays full clip, single displays curr trim bounds onClipPointerDown = (e: React.PointerEvent) => { // if timeline isn't shown, show first then trim this.heightPercent >= 100 && this.onTimelineHdlDown(e); - this.timeline && setupMoveUpEvents(this, e, returnFalse, returnFalse, action((e: PointerEvent, doubleTap?: boolean) => { - if (doubleTap) { - this.startTrim(TrimScope.All); - } else if (this.timeline) { - this.Pause(); - this.timeline.IsTrimming !== TrimScope.None ? this.finishTrim() : this.startTrim(TrimScope.Clip); - } - })); - } - + this.timeline && + setupMoveUpEvents( + this, + e, + returnFalse, + returnFalse, + action((e: PointerEvent, doubleTap?: boolean) => { + if (doubleTap) { + this.startTrim(TrimScope.All); + } else if (this.timeline) { + this.Pause(); + this.timeline.IsTrimming !== TrimScope.None ? this.finishTrim() : this.startTrim(TrimScope.Clip); + } + }) + ); + }; // for volume slider sets volume @action @@ -752,7 +822,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp this.toggleMute(); } } - } + }; // toggles video mute @action @@ -761,62 +831,68 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp this._muted = !this._muted; this.player.muted = this._muted; } - } - + }; // stretches vertically or horizontally depending on video orientation so video fits full screen fullScreenSize() { if (this._videoRef && this._videoRef.videoHeight / this._videoRef.videoWidth > 1) { - return { height: "100%" }; - } - else { - return { width: "100%" }; + return { height: '100%' }; + } else { + return { width: '100%' }; } } - // for zoom slider, sets timeline waveform zoom zoom = (zoom: number) => { this.timeline?.setZoom(zoom); - } - + }; // plays link playLink = (doc: Doc) => { - const startTime = Math.max(0, (this._stackedTimeline?.anchorStart(doc) || 0)); + const startTime = Math.max(0, 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); else this.Seek(startTime); } - } - + }; // starts marquee selection marqueeDown = (e: React.PointerEvent) => { - if (!e.altKey && e.button === 0 && this.layoutDoc._viewScale === 1 && this.props.isContentActive(true) && ![InkTool.Highlighter, InkTool.Pen].includes(CurrentUserUtils.ActiveTool)) { - setupMoveUpEvents(this, e, action(e => { - MarqueeAnnotator.clearAnnotations(this._savedAnnotations); - this._marqueeing = [e.clientX, e.clientY]; - return true; - }), returnFalse, () => MarqueeAnnotator.clearAnnotations(this._savedAnnotations), false); + if (!e.altKey && e.button === 0 && this.layoutDoc._viewScale === 1 && this.props.isContentActive(true) && ![InkTool.Highlighter, InkTool.Pen].includes(Doc.ActiveTool)) { + setupMoveUpEvents( + this, + e, + action(e => { + MarqueeAnnotator.clearAnnotations(this._savedAnnotations); + this._marqueeing = [e.clientX, e.clientY]; + return true; + }), + returnFalse, + () => MarqueeAnnotator.clearAnnotations(this._savedAnnotations), + false + ); } - } + }; // ends marquee selection @action finishMarquee = () => { this._marqueeing = undefined; this.props.select(true); - } + }; - timelineWhenChildContentsActiveChanged = action((isActive: boolean) => this.props.whenChildContentsActiveChanged(this._isAnyChildContentActive = isActive)); + timelineWhenChildContentsActiveChanged = action((isActive: boolean) => this.props.whenChildContentsActiveChanged((this._isAnyChildContentActive = isActive))); - timelineScreenToLocal = () => this.props.ScreenToLocalTransform().scale(this.scaling()).translate(0, -this.heightPercent / 100 * this.props.PanelHeight()); + timelineScreenToLocal = () => + this.props + .ScreenToLocalTransform() + .scale(this.scaling()) + .translate(0, (-this.heightPercent / 100) * this.props.PanelHeight()); - setPlayheadTime = (time: number) => this.player!.currentTime = this.layoutDoc._currentTimecode = time; + setPlayheadTime = (time: number) => (this.player!.currentTime = this.layoutDoc._currentTimecode = time); - timelineHeight = () => this.props.PanelHeight() * (100 - this.heightPercent) / 100; + timelineHeight = () => (this.props.PanelHeight() * (100 - this.heightPercent)) / 100; playing = () => this._playing; @@ -824,20 +900,22 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp scaling = () => this.props.scaling?.() || 1; - panelWidth = () => this.props.PanelWidth() * this.heightPercent / 100; - panelHeight = () => this.layoutDoc._fitWidth ? this.panelWidth() / (Doc.NativeAspect(this.rootDoc) || 1) : this.props.PanelHeight() * this.heightPercent / 100; + panelWidth = () => (this.props.PanelWidth() * this.heightPercent) / 100; + panelHeight = () => (this.layoutDoc._fitWidth ? this.panelWidth() / (Doc.NativeAspect(this.rootDoc) || 1) : (this.props.PanelHeight() * this.heightPercent) / 100); screenToLocalTransform = () => { const offset = (this.props.PanelWidth() - this.panelWidth()) / 2 / this.scaling(); - return this.props.ScreenToLocalTransform().translate(-offset, 0).scale(100 / this.heightPercent); - } + return this.props + .ScreenToLocalTransform() + .translate(-offset, 0) + .scale(100 / this.heightPercent); + }; - marqueeFitScaling = () => (this.props.scaling?.() || 1) * this.heightPercent / 100; - marqueeOffset = () => [this.panelWidth() / 2 * (1 - this.heightPercent / 100) / (this.heightPercent / 100), 0]; + marqueeFitScaling = () => ((this.props.scaling?.() || 1) * this.heightPercent) / 100; + marqueeOffset = () => [((this.panelWidth() / 2) * (1 - this.heightPercent / 100)) / (this.heightPercent / 100), 0]; timelineDocFilter = () => [`_timelineLabel:true,${Utils.noRecursionHack}:x`]; - // renders video controls componentUI = (boundsLeft: number, boundsTop: number) => { const bounds = this.props.docViewPath().lastElement().getBounds(); @@ -848,130 +926,157 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp const width = Math.max(right - left, 100); const uiHeight = Math.max(25, Math.min(50, height / 10)); const uiMargin = Math.min(10, height / 20); - const vidHeight = height * this.heightPercent / 100; + const vidHeight = (height * this.heightPercent) / 100; const yPos = top + vidHeight - uiHeight - uiMargin; const xPos = uiHeight / vidHeight > 0.4 ? right + 10 : left + 10; - const opacity = this._scrubbing ? 0.3 : (this._controlsVisible ? 1 : 0); - return this._fullScreen || (right - left) < 50 ? null : <div className="videoBox-ui-wrapper" style={{ clip: `rect(${boundsTop}px, 10000px, 10000px, ${boundsLeft}px)` }}> - <div className="videoBox-ui" style={{ left: xPos, top: yPos, height: uiHeight, width: width - 20, transition: this._clicking ? "top 0.5s" : "", opacity: opacity}}> - {this.UIButtons} + const opacity = this._scrubbing ? 0.3 : this._controlsVisible ? 1 : 0; + return this._fullScreen || right - left < 50 ? null : ( + <div className="videoBox-ui-wrapper" style={{ clip: `rect(${boundsTop}px, 10000px, 10000px, ${boundsLeft}px)` }}> + <div className="videoBox-ui" style={{ left: xPos, top: yPos, height: uiHeight, width: width - 20, transition: this._clicking ? 'top 0.5s' : '', opacity: opacity }}> + {this.UIButtons} + </div> </div> - </div> - } + ); + }; @computed get UIButtons() { const bounds = this.props.docViewPath().lastElement().getBounds(); const width = (bounds?.right || 0) - (bounds?.left || 0); const curTime = NumCast(this.layoutDoc._currentTimecode) - (this.timeline?.clipStart || 0); - return <> - <div className="videobox-button" - title={this._playing ? "play" : "pause"} - onPointerDown={this.onPlayDown}> - <FontAwesomeIcon icon={this._playing ? "pause" : "play"} /> - </div> - - {this.timeline && width > 150 && <div className="timecode-controls"> - <div className="timecode-current"> - {formatTime(curTime)} + return ( + <> + <div className="videobox-button" title={this._playing ? 'play' : 'pause'} onPointerDown={this.onPlayDown}> + <FontAwesomeIcon icon={this._playing ? 'pause' : 'play'} /> </div> - {this._fullScreen || (this.heightPercent === 100 && width > 200) ? - <div className="timeline-slider"> - <input type="range" step="0.1" min={this.timeline.clipStart} max={this.timeline.clipEnd} value={curTime} - className="toolbar-slider time-progress" - onPointerDown={action((e: React.PointerEvent) => { e.stopPropagation(); this._scrubbing = true;})} - onChange={(e: React.ChangeEvent<HTMLInputElement>) => this.setPlayheadTime(Number(e.target.value))} - onPointerUp={action((e: React.PointerEvent) => {e.stopPropagation(); this._scrubbing = false;})} - /> + {this.timeline && width > 150 && ( + <div className="timecode-controls"> + <div className="timecode-current">{formatTime(curTime)}</div> + + {this._fullScreen || (this.heightPercent === 100 && width > 200) ? ( + <div className="timeline-slider"> + <input + type="range" + step="0.1" + min={this.timeline.clipStart} + max={this.timeline.clipEnd} + value={curTime} + className="toolbar-slider time-progress" + onPointerDown={action((e: React.PointerEvent) => { + e.stopPropagation(); + this._scrubbing = true; + })} + onChange={(e: React.ChangeEvent<HTMLInputElement>) => this.setPlayheadTime(Number(e.target.value))} + onPointerUp={action((e: React.PointerEvent) => { + e.stopPropagation(); + this._scrubbing = false; + })} + /> + </div> + ) : ( + <div>/</div> + )} + + <div className="timecode-end">{formatTime(this.timeline.clipDuration)}</div> </div> - : - <div>/</div>} - - <div className="timecode-end"> - {formatTime(this.timeline.clipDuration)} - </div> - </div> - } - - <div className="videobox-button" - title={"full screen"} - onPointerDown={this.onFullDown}> - <FontAwesomeIcon icon="expand" /> - </div> - - { - !this._fullScreen && width > 300 && <div className="videobox-button" - title={"show timeline"} - onPointerDown={this.onTimelineHdlDown}> - <FontAwesomeIcon icon="eye" /> - </div> - } + )} - { - !this._fullScreen && width > 300 && <div className="videobox-button" - title={this.timeline?.IsTrimming !== TrimScope.None ? "finish trimming" : "start trim"} - onPointerDown={this.onClipPointerDown}> - <FontAwesomeIcon icon={this.timeline?.IsTrimming !== TrimScope.None ? "check" : "cut"} /> + <div className="videobox-button" title={'full screen'} onPointerDown={this.onFullDown}> + <FontAwesomeIcon icon="expand" /> </div> - } - <div className="videobox-button" - title={this._muted ? "unmute" : "mute"} - onPointerDown={(e) => { e.stopPropagation(); this.toggleMute(); }}> - <FontAwesomeIcon icon={this._muted ? "volume-mute" : "volume-up"} /> - </div> - { - width > 300 && <input type="range" style={{ width: `min(25%, 50px)` }} step="0.1" min="0" max="1" value={this._muted ? 0 : this._volume} - className="toolbar-slider volume" - onPointerDown={(e: React.PointerEvent) => e.stopPropagation()} - onChange={(e: React.ChangeEvent<HTMLInputElement>) => this.setVolume(Number(e.target.value))} - /> - } + {!this._fullScreen && width > 300 && ( + <div className="videobox-button" title={'show timeline'} onPointerDown={this.onTimelineHdlDown}> + <FontAwesomeIcon icon="eye" /> + </div> + )} - { - !this._fullScreen && this.heightPercent !== 100 && width > 300 && - <> - <div className="videobox-button" title="zoom"> - <FontAwesomeIcon icon="search-plus" /> + {!this._fullScreen && width > 300 && ( + <div className="videobox-button" title={this.timeline?.IsTrimming !== TrimScope.None ? 'finish trimming' : 'start trim'} onPointerDown={this.onClipPointerDown}> + <FontAwesomeIcon icon={this.timeline?.IsTrimming !== TrimScope.None ? 'check' : 'cut'} /> </div> - <input type="range" step="0.1" min="1" max="5" value={this.timeline?._zoomFactor} - className="toolbar-slider zoom" - onPointerDown={(e: React.PointerEvent) => { e.stopPropagation(); }} - onChange={(e: React.ChangeEvent<HTMLInputElement>) => { this.zoom(Number(e.target.value)); }} + )} + + <div + className="videobox-button" + title={this._muted ? 'unmute' : 'mute'} + onPointerDown={e => { + e.stopPropagation(); + this.toggleMute(); + }}> + <FontAwesomeIcon icon={this._muted ? 'volume-mute' : 'volume-up'} /> + </div> + {width > 300 && ( + <input + type="range" + style={{ width: `min(25%, 50px)` }} + step="0.1" + min="0" + max="1" + value={this._muted ? 0 : this._volume} + className="toolbar-slider volume" + onPointerDown={(e: React.PointerEvent) => e.stopPropagation()} + onChange={(e: React.ChangeEvent<HTMLInputElement>) => this.setVolume(Number(e.target.value))} /> - </> - } - </> + )} + + {!this._fullScreen && this.heightPercent !== 100 && width > 300 && ( + <> + <div className="videobox-button" title="zoom"> + <FontAwesomeIcon icon="search-plus" /> + </div> + <input + type="range" + step="0.1" + min="1" + max="5" + value={this.timeline?._zoomFactor} + className="toolbar-slider zoom" + onPointerDown={(e: React.PointerEvent) => { + e.stopPropagation(); + }} + onChange={(e: React.ChangeEvent<HTMLInputElement>) => { + this.zoom(Number(e.target.value)); + }} + /> + </> + )} + </> + ); } // renders CollectionStackedTimeline @computed get renderTimeline() { - return <div className="videoBox-stackPanel" style={{ transition: this.transition, height: `${100 - this.heightPercent}%` }}> - <CollectionStackedTimeline ref={action((r: any) => this._stackedTimeline = r)} {...this.props} - fieldKey={this.annotationKey} - dictationKey={this.fieldKey + "-dictation"} - mediaPath={this.audiopath} - renderDepth={this.props.renderDepth + 1} - startTag={"_timecodeToShow" /* videoStart */} - endTag={"_timecodeToHide" /* videoEnd */} - bringToFront={emptyFunction} - CollectionView={undefined} - playFrom={this.playFrom} - setTime={this.setPlayheadTime} - playing={this.playing} - isAnyChildContentActive={this.isAnyChildContentActive} - whenChildContentsActiveChanged={this.timelineWhenChildContentsActiveChanged} - moveDocument={this.moveDocument} - addDocument={this.addDocument} - removeDocument={this.removeDocument} - ScreenToLocalTransform={this.timelineScreenToLocal} - Play={this.Play} - Pause={this.Pause} - playLink={this.playLink} - PanelHeight={this.timelineHeight} - rawDuration={this.rawDuration} - /> - </div>; + return ( + <div className="videoBox-stackPanel" style={{ transition: this.transition, height: `${100 - this.heightPercent}%` }}> + <CollectionStackedTimeline + ref={action((r: any) => (this._stackedTimeline = r))} + {...this.props} + fieldKey={this.annotationKey} + dictationKey={this.fieldKey + '-dictation'} + mediaPath={this.audiopath} + renderDepth={this.props.renderDepth + 1} + startTag={'_timecodeToShow' /* videoStart */} + endTag={'_timecodeToHide' /* videoEnd */} + bringToFront={emptyFunction} + CollectionView={undefined} + playFrom={this.playFrom} + setTime={this.setPlayheadTime} + playing={this.playing} + isAnyChildContentActive={this.isAnyChildContentActive} + whenChildContentsActiveChanged={this.timelineWhenChildContentsActiveChanged} + moveDocument={this.moveDocument} + addDocument={this.addDocument} + removeDocument={this.removeDocument} + ScreenToLocalTransform={this.timelineScreenToLocal} + Play={this.Play} + Pause={this.Pause} + playLink={this.playLink} + PanelHeight={this.timelineHeight} + rawDuration={this.rawDuration} + /> + </div> + ); } // renders annotation layer @@ -982,59 +1087,72 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp savedAnnotations = () => this._savedAnnotations; render() { const borderRad = this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.BorderRounding); - const borderRadius = borderRad?.includes("px") ? `${Number(borderRad.split("px")[0]) / this.scaling()}px` : borderRad; - return (<div className="videoBox" onContextMenu={this.specificContextMenu} ref={this._mainCont} - style={{ - pointerEvents: this.layoutDoc._lockedPosition ? "none" : undefined, - borderRadius, - overflow: this.props.docViewPath?.().slice(-1)[0].fitWidth ? "auto" : undefined - }} onWheel={e => { e.stopPropagation(); e.preventDefault(); }}> - <div className="videoBox-viewer" onPointerDown={this.marqueeDown} > - <div style={{ - position: "absolute", transition: this.transition, - width: this.panelWidth(), - height: this.panelHeight(), - top: 0, - left: (this.props.PanelWidth() - this.panelWidth()) / 2 + const borderRadius = borderRad?.includes('px') ? `${Number(borderRad.split('px')[0]) / this.scaling()}px` : borderRad; + return ( + <div + className="videoBox" + onContextMenu={this.specificContextMenu} + ref={this._mainCont} + style={{ + pointerEvents: this.layoutDoc._lockedPosition ? 'none' : undefined, + borderRadius, + overflow: this.props.docViewPath?.().slice(-1)[0].fitWidth ? 'auto' : undefined, + }} + onWheel={e => { + e.stopPropagation(); + e.preventDefault(); }}> - <CollectionFreeFormView {...OmitKeys(this.props, ["NativeWidth", "NativeHeight", "setContentView"]).omit} - renderDepth={this.props.renderDepth + 1} - fieldKey={this.annotationKey} - CollectionView={undefined} - isAnnotationOverlay={true} - annotationLayerHostsContent={true} - PanelWidth={this.panelWidth} - PanelHeight={this.panelHeight} - ScreenToLocalTransform={this.screenToLocalTransform} - docFilters={this.timelineDocFilter} - select={emptyFunction} - scaling={returnOne} - whenChildContentsActiveChanged={this.whenChildContentsActiveChanged} - removeDocument={this.removeDocument} - moveDocument={this.moveDocument} - addDocument={this.addDocWithTimecode}> - {this.contentFunc} - </CollectionFreeFormView> + <div className="videoBox-viewer" onPointerDown={this.marqueeDown}> + <div + style={{ + position: 'absolute', + transition: this.transition, + width: this.panelWidth(), + height: this.panelHeight(), + top: 0, + left: (this.props.PanelWidth() - this.panelWidth()) / 2, + }}> + <CollectionFreeFormView + {...OmitKeys(this.props, ['NativeWidth', 'NativeHeight', 'setContentView']).omit} + renderDepth={this.props.renderDepth + 1} + fieldKey={this.annotationKey} + CollectionView={undefined} + isAnnotationOverlay={true} + annotationLayerHostsContent={true} + PanelWidth={this.panelWidth} + PanelHeight={this.panelHeight} + ScreenToLocalTransform={this.screenToLocalTransform} + docFilters={this.timelineDocFilter} + select={emptyFunction} + scaling={returnOne} + whenChildContentsActiveChanged={this.whenChildContentsActiveChanged} + removeDocument={this.removeDocument} + moveDocument={this.moveDocument} + addDocument={this.addDocWithTimecode}> + {this.contentFunc} + </CollectionFreeFormView> + </div> + {this.annotationLayer} + {!this._marqueeing || !this._mainCont.current || !this._annotationLayer.current ? null : ( + <MarqueeAnnotator + rootDoc={this.rootDoc} + scrollTop={0} + down={this._marqueeing} + scaling={this.marqueeFitScaling} + docView={this.props.docViewPath().slice(-1)[0]} + containerOffset={this.marqueeOffset} + addDocument={this.addDocWithTimecode} + finishMarquee={this.finishMarquee} + savedAnnotations={this.savedAnnotations} + annotationLayer={this._annotationLayer.current} + mainCont={this._mainCont.current} + /> + )} + {this.renderTimeline} </div> - {this.annotationLayer} - {!this._marqueeing || !this._mainCont.current || !this._annotationLayer.current ? (null) : - <MarqueeAnnotator - rootDoc={this.rootDoc} - scrollTop={0} - down={this._marqueeing} - scaling={this.marqueeFitScaling} - docView={this.props.docViewPath().slice(-1)[0]} - containerOffset={this.marqueeOffset} - addDocument={this.addDocWithTimecode} - finishMarquee={this.finishMarquee} - savedAnnotations={this.savedAnnotations} - annotationLayer={this._annotationLayer.current} - mainCont={this._mainCont.current} - />} - {this.renderTimeline} </div> - </div >); + ); } } -VideoBox._nativeControls = false;
\ No newline at end of file +VideoBox._nativeControls = false; diff --git a/src/client/views/nodes/WebBox.tsx b/src/client/views/nodes/WebBox.tsx index d14af49ea..d97277c2b 100644 --- a/src/client/views/nodes/WebBox.tsx +++ b/src/client/views/nodes/WebBox.tsx @@ -1,48 +1,48 @@ -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { action, computed, IReactionDisposer, observable, ObservableMap, reaction, runInAction } from "mobx"; -import { observer } from "mobx-react"; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { action, computed, IReactionDisposer, observable, ObservableMap, reaction, runInAction } from 'mobx'; +import { observer } from 'mobx-react'; import * as WebRequest from 'web-request'; -import { Doc, DocListCast, HeightSym, Opt, WidthSym } from "../../../fields/Doc"; -import { Id } from "../../../fields/FieldSymbols"; -import { HtmlField } from "../../../fields/HtmlField"; -import { InkTool } from "../../../fields/InkField"; -import { List } from "../../../fields/List"; -import { listSpec } from "../../../fields/Schema"; -import { ComputedField } from "../../../fields/ScriptField"; -import { Cast, ImageCast, NumCast, StrCast } from "../../../fields/Types"; -import { ImageField, WebField } from "../../../fields/URLField"; -import { TraceMobx } from "../../../fields/util"; -import { emptyFunction, getWordAtPoint, OmitKeys, returnFalse, returnOne, setupMoveUpEvents, smoothScroll, Utils } from "../../../Utils"; -import { Docs } from "../../documents/Documents"; -import { CurrentUserUtils } from "../../util/CurrentUserUtils"; -import { ScriptingGlobals } from "../../util/ScriptingGlobals"; -import { SnappingManager } from "../../util/SnappingManager"; -import { undoBatch } from "../../util/UndoManager"; -import { MarqueeOptionsMenu } from "../collections/collectionFreeForm"; -import { CollectionFreeFormView } from "../collections/collectionFreeForm/CollectionFreeFormView"; -import { ContextMenu } from "../ContextMenu"; -import { ContextMenuProps } from "../ContextMenuItem"; -import { ViewBoxAnnotatableComponent, ViewBoxAnnotatableProps } from "../DocComponent"; -import { DocumentDecorations } from "../DocumentDecorations"; -import { Colors } from "../global/globalEnums"; -import { LightboxView } from "../LightboxView"; -import { MarqueeAnnotator } from "../MarqueeAnnotator"; -import { AnchorMenu } from "../pdf/AnchorMenu"; -import { Annotation } from "../pdf/Annotation"; -import { SidebarAnnos } from "../SidebarAnnos"; -import { StyleProp } from "../StyleProvider"; -import { DocumentViewProps } from "./DocumentView"; +import { Doc, DocListCast, HeightSym, Opt, WidthSym } from '../../../fields/Doc'; +import { Id } from '../../../fields/FieldSymbols'; +import { HtmlField } from '../../../fields/HtmlField'; +import { InkTool } from '../../../fields/InkField'; +import { List } from '../../../fields/List'; +import { listSpec } from '../../../fields/Schema'; +import { Cast, ImageCast, NumCast, StrCast } from '../../../fields/Types'; +import { ImageField, WebField } from '../../../fields/URLField'; +import { TraceMobx } from '../../../fields/util'; +import { emptyFunction, getWordAtPoint, OmitKeys, returnFalse, returnOne, setupMoveUpEvents, smoothScroll, Utils } from '../../../Utils'; +import { Docs, DocUtils } from '../../documents/Documents'; +import { ScriptingGlobals } from '../../util/ScriptingGlobals'; +import { SnappingManager } from '../../util/SnappingManager'; +import { undoBatch } from '../../util/UndoManager'; +import { MarqueeOptionsMenu } from '../collections/collectionFreeForm'; +import { CollectionFreeFormView } from '../collections/collectionFreeForm/CollectionFreeFormView'; +import { ContextMenu } from '../ContextMenu'; +import { ContextMenuProps } from '../ContextMenuItem'; +import { ViewBoxAnnotatableComponent, ViewBoxAnnotatableProps } from '../DocComponent'; +import { DocumentDecorations } from '../DocumentDecorations'; +import { Colors } from '../global/globalEnums'; +import { LightboxView } from '../LightboxView'; +import { MarqueeAnnotator } from '../MarqueeAnnotator'; +import { AnchorMenu } from '../pdf/AnchorMenu'; +import { Annotation } from '../pdf/Annotation'; +import { SidebarAnnos } from '../SidebarAnnos'; +import { StyleProp } from '../StyleProvider'; +import { DocumentViewProps } from './DocumentView'; import { FieldView, FieldViewProps } from './FieldView'; -import { LinkDocPreview } from "./LinkDocPreview"; -import { VideoBox } from "./VideoBox"; -import "./WebBox.scss"; -import React = require("react"); -const { CreateImage } = require("./WebBoxRenderer"); -const _global = (window /* browser */ || global /* node */) as any; -const htmlToText = require("html-to-text"); +import { LinkDocPreview } from './LinkDocPreview'; +import { VideoBox } from './VideoBox'; +import './WebBox.scss'; +import React = require('react'); +const { CreateImage } = require('./WebBoxRenderer'); +const _global = (window /* browser */ || global) /* node */ as any; +const htmlToText = require('html-to-text'); @observer export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps & FieldViewProps>() { - public static LayoutString(fieldKey: string) { return FieldView.LayoutString(WebBox, fieldKey); } + public static LayoutString(fieldKey: string) { + return FieldView.LayoutString(WebBox, fieldKey); + } public static openSidebarWidth = 250; public static sidebarResizerWidth = 5; private _setPreviewCursor: undefined | ((x: number, y: number, drag: boolean, hide: boolean) => void); @@ -55,9 +55,9 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps private _getAnchor: (savedAnnotations?: ObservableMap<number, HTMLDivElement[]>) => Opt<Doc> = () => undefined; private _sidebarRef = React.createRef<SidebarAnnos>(); private _searchRef = React.createRef<HTMLInputElement>(); - private _searchString = ""; - @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 wan 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 + private _searchString = ''; + @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 wan 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; @observable private _showSidebar = false; @observable private _scrollTimer: any; @@ -69,22 +69,41 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps @observable private _iframe: HTMLIFrameElement | null = null; @observable private _savedAnnotations = new ObservableMap<number, HTMLDivElement[]>(); @observable private _scrollHeight = NumCast(this.layoutDoc.scrollHeight); - @computed get _url() { return this.webField?.toString() || ""; } - @computed get _urlHash() { return this._url ? WebBox.urlHash(this._url) + "" : ""; } - @computed get scrollHeight() { return Math.max(this.layoutDoc[HeightSym](), this._scrollHeight); } - @computed get allAnnotations() { return DocListCast(this.dataDoc[this.annotationKey]); } - @computed get inlineTextAnnotations() { return this.allAnnotations.filter(a => a.textInlineAnnotations); } - @computed get webField() { return Cast(this.dataDoc[this.props.fieldKey], WebField)?.url; } + @computed get _url() { + return this.webField?.toString() || ''; + } + @computed get _urlHash() { + return this._url ? WebBox.urlHash(this._url) + '' : ''; + } + @computed get scrollHeight() { + return Math.max(this.layoutDoc[HeightSym](), this._scrollHeight); + } + @computed get allAnnotations() { + return DocListCast(this.dataDoc[this.annotationKey]); + } + @computed get inlineTextAnnotations() { + return this.allAnnotations.filter(a => a.textInlineAnnotations); + } + @computed get webField() { + return Cast(this.dataDoc[this.props.fieldKey], WebField)?.url; + } @computed get webThumb() { - return this.props.thumbShown?.() && - ImageCast(this.layoutDoc["thumb-frozen"], - ImageCast(this.layoutDoc.thumbScrollTop === this.layoutDoc._scrollTop && this.layoutDoc.thumbNativeWidth === NumCast(this.layoutDoc.nativeWidth) && - this.layoutDoc.thumbNativeHeight === NumCast(this.layoutDoc.nativeHeight) ? this.layoutDoc.thumb : undefined))?.url; + return ( + this.props.thumbShown?.() && + ImageCast( + this.layoutDoc['thumb-frozen'], + ImageCast( + this.layoutDoc.thumbScrollTop === this.layoutDoc._scrollTop && this.layoutDoc.thumbNativeWidth === NumCast(this.layoutDoc.nativeWidth) && this.layoutDoc.thumbNativeHeight === NumCast(this.layoutDoc.nativeHeight) + ? this.layoutDoc.thumb + : undefined + ) + )?.url + ); } constructor(props: any) { super(props); - runInAction(() => this._webUrl = this._url); // setting the weburl will change the src parameter of the embedded iframe and force a navigation to it. + runInAction(() => (this._webUrl = this._url)); // setting the weburl will change the src parameter of the embedded iframe and force a navigation to it. } @action @@ -104,7 +123,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps (this._iframe?.contentWindow as any)?.find(searchString, false, bwd, true); } return true; - } + }; @action setScrollPos = (pos: number) => { if (!this._outerRef.current || this._outerRef.current.scrollHeight < pos) { @@ -113,14 +132,14 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps this._outerRef.current.scrollTop = pos; this._initialScroll = undefined; } - } + }; lockout = false; updateThumb = async () => { - const imageBitmap = ImageCast(this.layoutDoc["thumb-frozen"])?.url.href; + const imageBitmap = ImageCast(this.layoutDoc['thumb-frozen'])?.url.href; const scrollTop = NumCast(this.layoutDoc._scrollTop); const nativeWidth = NumCast(this.layoutDoc.nativeWidth); - const nativeHeight = nativeWidth * this.props.PanelHeight() / this.props.PanelWidth(); + const nativeHeight = (nativeWidth * this.props.PanelHeight()) / this.props.PanelWidth(); if (!this.lockout && this._iframe && !imageBitmap && (scrollTop !== this.layoutDoc.thumbScrollTop || nativeWidth !== this.layoutDoc.thumbNativeWidth || nativeHeight !== this.layoutDoc.thumbNativeHeight)) { var htmlString = this._iframe.contentDocument && new XMLSerializer().serializeToString(this._iframe.contentDocument); if (!htmlString) { @@ -128,60 +147,65 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps } this.layoutDoc.thumb = undefined; this.lockout = true; // lock to prevent multiple thumb updates. - CreateImage( - this._webUrl.endsWith("/") ? this._webUrl.substring(0, this._webUrl.length - 1) : this._webUrl, - this._iframe.contentDocument?.styleSheets ?? [], - htmlString, - nativeWidth, - nativeHeight, - scrollTop - ).then - ((data_url: any) => { - VideoBox.convertDataUri(data_url, this.layoutDoc[Id] + "-icon" + (new Date()).getTime(), true, this.layoutDoc[Id] + "-icon").then( - returnedfilename => setTimeout(action(() => { - this.lockout = false; - this.layoutDoc.thumb = new ImageField(returnedfilename); - this.layoutDoc.thumbScrollTop = scrollTop; - this.layoutDoc.thumbNativeWidth = nativeWidth; - this.layoutDoc.thumbNativeHeight = nativeHeight; - }), 500)); + CreateImage(this._webUrl.endsWith('/') ? this._webUrl.substring(0, this._webUrl.length - 1) : this._webUrl, this._iframe.contentDocument?.styleSheets ?? [], htmlString, nativeWidth, nativeHeight, scrollTop) + .then((data_url: any) => { + VideoBox.convertDataUri(data_url, this.layoutDoc[Id] + '-icon' + new Date().getTime(), true, this.layoutDoc[Id] + '-icon').then(returnedfilename => + setTimeout( + action(() => { + this.lockout = false; + this.layoutDoc.thumb = new ImageField(returnedfilename); + this.layoutDoc.thumbScrollTop = scrollTop; + this.layoutDoc.thumbNativeWidth = nativeWidth; + this.layoutDoc.thumbNativeHeight = nativeHeight; + }), + 500 + ) + ); }) .catch(function (error: any) { console.error('oops, something went wrong!', error); }); } - } + }; async componentDidMount() { this.props.setContentView?.(this); // this tells the DocumentView that this WebBox is the "content" of the document. this allows the DocumentView to call WebBox relevant methods to configure the UI (eg, show back/forward buttons) runInAction(() => { - this._annotationKeySuffix = () => this._urlHash + "-annotations"; - const reqdFuncs:{[key:string]: string} = {}; + this._annotationKeySuffix = () => this._urlHash + '-annotations'; + const reqdFuncs: { [key: string]: string } = {}; // 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 + "-sidebar"] = `copyField(this["${this.fieldKey}-"+urlHash(this["${this.fieldKey}"]?.url?.toString())+"-sidebar"`; - CurrentUserUtils.AssignScripts(this.dataDoc, {}, reqdFuncs); + reqdFuncs[this.fieldKey + '-annotations'] = `copyField(this["${this.fieldKey}-"+urlHash(this["${this.fieldKey}"]?.url?.toString())+"-annotations"`; + reqdFuncs[this.fieldKey + '-sidebar'] = `copyField(this["${this.fieldKey}-"+urlHash(this["${this.fieldKey}"]?.url?.toString())+"-sidebar"`; + DocUtils.AssignScripts(this.dataDoc, {}, reqdFuncs); }); - reaction(() => this.props.isSelected(true) || this.isAnyChildContentActive() || Doc.isBrushedHighlightedDegree(this.props.Document), - async (selected) => { + reaction( + () => this.props.isSelected(true) || this.isAnyChildContentActive() || Doc.isBrushedHighlightedDegree(this.props.Document), + async selected => { if (selected) { this._webPageHasBeenRendered = true; - } else if ((!this.props.isContentActive(true) || SnappingManager.GetIsDragging()) && // update thumnail when unselected AND (no child annotation is active OR we've started dragging the document in which case no additional deselect will occur so this is the only chance to update the thumbnail) + } else if ( + (!this.props.isContentActive(true) || SnappingManager.GetIsDragging()) && // update thumnail when unselected AND (no child annotation is active OR we've started dragging the document in which case no additional deselect will occur so this is the only chance to update the thumbnail) !this.props.docViewPath().lastElement()?.docView?._pendingDoubleClick && // don't create a thumbnail when double-clicking to enter lightbox because thumbnail will be empty - LightboxView.LightboxDoc !== this.rootDoc) { // don't create a thumbnail if entering Lightbox from maximize either, since thumb will be empty. + LightboxView.LightboxDoc !== this.rootDoc + ) { + // don't create a thumbnail if entering Lightbox from maximize either, since thumb will be empty. this.updateThumb(); } - }, { fireImmediately: this.props.isSelected(true) || this.isAnyChildContentActive() || (Doc.isBrushedHighlightedDegreeUnmemoized(this.props.Document) ? true : false) }); + }, + { fireImmediately: this.props.isSelected(true) || this.isAnyChildContentActive() || (Doc.isBrushedHighlightedDegreeUnmemoized(this.props.Document) ? true : false) } + ); - this._disposers.autoHeight = reaction(() => this.layoutDoc._autoHeight, + this._disposers.autoHeight = reaction( + () => this.layoutDoc._autoHeight, autoHeight => { if (autoHeight) { - this.layoutDoc._nativeHeight = NumCast(this.props.Document[this.props.fieldKey + "-nativeHeight"]); - this.props.setHeight?.(NumCast(this.props.Document[this.props.fieldKey + "-nativeHeight"]) * (this.props.scaling?.() || 1)); + this.layoutDoc._nativeHeight = NumCast(this.props.Document[this.props.fieldKey + '-nativeHeight']); + this.props.setHeight?.(NumCast(this.props.Document[this.props.fieldKey + '-nativeHeight']) * (this.props.scaling?.() || 1)); } - }); + } + ); - if (this.webField?.href.indexOf("youtube") !== -1) { + if (this.webField?.href.indexOf('youtube') !== -1) { const youtubeaspect = 400 / 315; const nativeWidth = Doc.NativeWidth(this.layoutDoc); const nativeHeight = Doc.NativeHeight(this.layoutDoc); @@ -199,8 +223,9 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps } } - this._disposers.scrollReaction = reaction(() => NumCast(this.layoutDoc._scrollTop), - (scrollTop) => { + this._disposers.scrollReaction = reaction( + () => NumCast(this.layoutDoc._scrollTop), + scrollTop => { const viewTrans = StrCast(this.Document._viewTransition); const durationMiliStr = viewTrans.match(/([0-9]*)ms/); const durationSecStr = viewTrans.match(/([0-9.]*)s/); @@ -223,13 +248,13 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps for (let i = 0; i < clientRects.length; i++) { const rect = clientRects.item(i); if (rect && rect.width !== this._mainCont.current.clientWidth) { - const annoBox = document.createElement("div"); - annoBox.className = "marqueeAnnotator-annotationBox"; + const annoBox = document.createElement('div'); + annoBox.className = 'marqueeAnnotator-annotationBox'; // 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.left = rect.left.toString(); + annoBox.style.width = rect.width.toString(); + annoBox.style.height = rect.height.toString(); this._annotationLayer.current && MarqueeAnnotator.previewNewAnnotation(this._savedAnnotations, this._annotationLayer.current, annoBox, 1); } } @@ -237,23 +262,22 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps //this._selectionText = selRange.cloneContents().textContent || ""; // clear selection - if (sel.empty) sel.empty();// Chrome - else if (sel.removeAllRanges) sel.removeAllRanges(); // Firefox + if (sel.empty) sel.empty(); // Chrome + else if (sel.removeAllRanges) sel.removeAllRanges(); // Firefox return this._savedAnnotations; - } + }; menuControls = () => this.urlEditor; // controls to be added to the top bar when a document of this type is selected scrollFocus = (doc: Doc, smooth: boolean) => { if (StrCast(doc.webUrl) !== this._url) this.submitURL(StrCast(doc.webUrl), !smooth); - if (DocListCast(this.props.Document[this.fieldKey + "-sidebar"]).includes(doc) && !this.SidebarShown) { + if (DocListCast(this.props.Document[this.fieldKey + '-sidebar']).includes(doc) && !this.SidebarShown) { this.toggleSidebar(!smooth); } if (this._sidebarRef?.current?.makeDocUnfiltered(doc)) return 1; if (doc !== this.rootDoc && this._outerRef.current) { const windowHeight = this.props.PanelHeight() / (this.props.scaling?.() || 1); - const scrollTo = Utils.scrollIntoView(NumCast(doc.y), doc[HeightSym](), NumCast(this.layoutDoc._scrollTop), windowHeight, windowHeight * .1, - Math.max(NumCast(doc.y) + doc[HeightSym](), this.getScrollHeight())); + const scrollTo = Utils.scrollIntoView(NumCast(doc.y), doc[HeightSym](), NumCast(this.layoutDoc._scrollTop), windowHeight, windowHeight * 0.1, Math.max(NumCast(doc.y) + doc[HeightSym](), this.getScrollHeight())); if (scrollTo !== undefined && this._initialScroll === undefined) { const focusSpeed = smooth ? 500 : 0; this.goTo(scrollTo, focusSpeed); @@ -263,22 +287,22 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps } } return undefined; - } + }; getAnchor = () => { const anchor = this._getAnchor(this._savedAnnotations) ?? Docs.Create.WebanchorDocument(this._url, { - title: StrCast(this.rootDoc.title + " " + this.layoutDoc._scrollTop), + title: StrCast(this.rootDoc.title + ' ' + this.layoutDoc._scrollTop), y: NumCast(this.layoutDoc._scrollTop), - unrendered: true + unrendered: true, }); this.addDocumentWrapper(anchor); return anchor; - } + }; _textAnnotationCreator: (() => ObservableMap<number, HTMLDivElement[]>) | undefined; - savedAnnotationsCreator: (() => ObservableMap<number, HTMLDivElement[]>) = () => this._textAnnotationCreator?.() || this._savedAnnotations; + savedAnnotationsCreator: () => ObservableMap<number, HTMLDivElement[]> = () => this._textAnnotationCreator?.() || this._savedAnnotations; @action iframeUp = (e: PointerEvent) => { @@ -290,11 +314,10 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps const sel = this._iframe.contentWindow.getSelection(); if (sel) { 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._scrollTop) * scale); + AnchorMenu.Instance.jumpTo(e.clientX * scale + mainContBounds.translateX, e.clientY * scale + mainContBounds.translateY - NumCast(this.layoutDoc._scrollTop) * scale); } } - } + }; @action iframeDown = (e: PointerEvent) => { const sel = this._iframe?.contentWindow?.getSelection?.(); @@ -303,10 +326,12 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps const word = getWordAtPoint(e.target, e.clientX, e.clientY); this._setPreviewCursor?.(e.clientX, e.clientY, false, true); MarqueeAnnotator.clearAnnotations(this._savedAnnotations); - this._marqueeing = [e.clientX * scale + mainContBounds.translateX, - e.clientY * scale + mainContBounds.translateY - NumCast(this.layoutDoc._scrollTop) * scale]; - if (word || (e.target as any || "").className.includes("rangeslider") || (e.target as any)?.onclick || (e.target as any)?.parentNode?.onclick) { - setTimeout(action(() => this._marqueeing = undefined), 100); // bcz: hack .. anchor menu is setup within MarqueeAnnotator so we need to at least create the marqueeAnnotator even though we aren't using it. + this._marqueeing = [e.clientX * scale + mainContBounds.translateX, e.clientY * scale + mainContBounds.translateY - NumCast(this.layoutDoc._scrollTop) * scale]; + if (word || ((e.target as any) || '').className.includes('rangeslider') || (e.target as any)?.onclick || (e.target as any)?.parentNode?.onclick) { + setTimeout( + action(() => (this._marqueeing = undefined)), + 100 + ); // bcz: hack .. anchor menu is setup within MarqueeAnnotator so we need to at least create the marqueeAnnotator even though we aren't using it. } else { this._iframeClick = this._iframe ?? undefined; this._isAnnotating = true; @@ -322,13 +347,13 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps ContextMenu.Instance.closeMenu(); ContextMenu.Instance.setIgnoreEvents(true); } - } + }; getScrollHeight = () => this._scrollHeight; isFirefox = () => { - return "InstallTrigger" in window; // navigator.userAgent.indexOf("Chrome") !== -1; - } + return 'InstallTrigger' in window; // navigator.userAgent.indexOf("Chrome") !== -1; + }; iframeClick = () => this._iframeClick; iframeScaling = () => 1 / this.props.ScreenToLocalTransform().Scale; @@ -338,7 +363,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps if (this._initialScroll !== undefined) { this.setScrollPos(this._initialScroll); } - let requrlraw = decodeURIComponent(iframe?.contentWindow?.location.href.replace(Utils.prepend("") + "/corsProxy/", "") ?? this._url.toString()); + let requrlraw = decodeURIComponent(iframe?.contentWindow?.location.href.replace(Utils.prepend('') + '/corsProxy/', '') ?? this._url.toString()); if (requrlraw !== this._url.toString()) { if (requrlraw.match(/q=.*&/)?.length && this._url.toString().match(/q=.*&/)?.length) { const matches = requrlraw.match(/[^a-zA-z]q=[^&]*/g); @@ -346,63 +371,79 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps if (matches) { requrlraw = requrlraw.substring(0, requrlraw.indexOf(newsearch)); for (let i = 1; i < Array.from(matches)?.length; i++) { - requrlraw = requrlraw.replace(matches[i], ""); + requrlraw = requrlraw.replace(matches[i], ''); } } - requrlraw = requrlraw.replace(/q=[^&]*/, newsearch.substring(1)).replace("search&", "search?").replace("?gbv=1", ""); + requrlraw = requrlraw + .replace(/q=[^&]*/, newsearch.substring(1)) + .replace('search&', 'search?') + .replace('?gbv=1', ''); } this.submitURL(requrlraw, undefined, true); } if (iframe?.contentDocument) { - iframe.contentDocument.addEventListener("pointerup", this.iframeUp); - iframe.contentDocument.addEventListener("pointerdown", this.iframeDown); + iframe.contentDocument.addEventListener('pointerup', this.iframeUp); + iframe.contentDocument.addEventListener('pointerdown', this.iframeDown); this._scrollHeight = Math.max(this._scrollHeight, iframe?.contentDocument.body.scrollHeight); - setTimeout(action(() => this._scrollHeight = Math.max(this._scrollHeight, iframe?.contentDocument?.body.scrollHeight || 0)), 5000); - iframe.setAttribute("enable-annotation", "true"); - iframe.contentDocument.addEventListener("click", undoBatch(action((e: MouseEvent) => { - let href = ""; - for (let ele = e.target as any; ele; ele = ele.parentElement) { - href = (typeof (ele.href) === "string" ? ele.href : ele.href?.baseVal) || ele.parentElement?.href || href; - } - const origin = this.webField?.origin; - if (href && origin) { - e.stopPropagation(); - setTimeout(() => this.submitURL(href.replace(Utils.prepend(""), origin))); - if (this._outerRef.current) { - this._outerRef.current.scrollTop = NumCast(this.layoutDoc._scrollTop); - this._outerRef.current.scrollLeft = 0; - } - } - }))); + setTimeout( + action(() => (this._scrollHeight = Math.max(this._scrollHeight, iframe?.contentDocument?.body.scrollHeight || 0))), + 5000 + ); + iframe.setAttribute('enable-annotation', 'true'); + iframe.contentDocument.addEventListener( + 'click', + undoBatch( + action((e: MouseEvent) => { + let href = ''; + for (let ele = e.target as any; ele; ele = ele.parentElement) { + href = (typeof ele.href === 'string' ? ele.href : ele.href?.baseVal) || ele.parentElement?.href || href; + } + const origin = this.webField?.origin; + if (href && origin) { + e.stopPropagation(); + setTimeout(() => this.submitURL(href.replace(Utils.prepend(''), origin))); + if (this._outerRef.current) { + this._outerRef.current.scrollTop = NumCast(this.layoutDoc._scrollTop); + this._outerRef.current.scrollLeft = 0; + } + } + }) + ) + ); iframe.contentDocument.addEventListener('wheel', this.iframeWheel, false); //iframe.contentDocument.addEventListener('scroll', () => !this.active() && this._iframe && (this._iframe.scrollTop = NumCast(this.layoutDoc._scrollTop), false)); } - } + }; @action iframeWheel = (e: any) => { if (!this._scrollTimer) { - this._scrollTimer = setTimeout(action(() => this._scrollTimer = undefined), 250); // this turns events off on the iframe which allows scrolling to change direction smoothly + this._scrollTimer = setTimeout( + action(() => (this._scrollTimer = undefined)), + 250 + ); // this turns events off on the iframe which allows scrolling to change direction smoothly } - } + }; @action setDashScrollTop = (scrollTop: number, timeout: number = 250) => { const iframeHeight = Math.max(scrollTop, this._scrollHeight - this.panelHeight()); this._scrollTimer && clearTimeout(this._scrollTimer); - this._scrollTimer = setTimeout(action(() => { - this._scrollTimer = undefined; - const newScrollTop = scrollTop > iframeHeight ? iframeHeight : scrollTop; - if (!LinkDocPreview.LinkInfo && this._outerRef.current && newScrollTop !== this.layoutDoc.thumbScrollTop && - (!LightboxView.LightboxDoc || LightboxView.IsLightboxDocView(this.props.docViewPath()))) { - this.layoutDoc.thumb = undefined; - this.layoutDoc.thumbScrollTop = undefined; - this.layoutDoc.thumbNativeWidth = undefined; - this.layoutDoc.thumbNativeHeight = undefined; - this.layoutDoc.scrollTop = this._outerRef.current.scrollTop = newScrollTop; - } else if (this._outerRef.current) this._outerRef.current.scrollTop = newScrollTop; - }), timeout); - } + this._scrollTimer = setTimeout( + action(() => { + this._scrollTimer = undefined; + const newScrollTop = scrollTop > iframeHeight ? iframeHeight : scrollTop; + if (!LinkDocPreview.LinkInfo && this._outerRef.current && newScrollTop !== this.layoutDoc.thumbScrollTop && (!LightboxView.LightboxDoc || LightboxView.IsLightboxDocView(this.props.docViewPath()))) { + this.layoutDoc.thumb = undefined; + this.layoutDoc.thumbScrollTop = undefined; + this.layoutDoc.thumbNativeWidth = undefined; + this.layoutDoc.thumbNativeHeight = undefined; + this.layoutDoc.scrollTop = this._outerRef.current.scrollTop = newScrollTop; + } else if (this._outerRef.current) this._outerRef.current.scrollTop = newScrollTop; + }), + timeout + ); + }; goTo = (scrollTop: number, duration: number) => { if (this._outerRef.current) { @@ -414,20 +455,20 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps this.setDashScrollTop(scrollTop); } } else this._initialScroll = scrollTop; - } + }; forward = (checkAvailable?: boolean) => { - const future = Cast(this.dataDoc[this.fieldKey + "-future"], listSpec("string"), []); - const history = Cast(this.dataDoc[this.fieldKey + "-history"], listSpec("string"), []); + const future = Cast(this.dataDoc[this.fieldKey + '-future'], listSpec('string'), []); + const history = Cast(this.dataDoc[this.fieldKey + '-history'], listSpec('string'), []); if (checkAvailable) return future.length; runInAction(() => { if (future.length) { const curUrl = this._url; - this.dataDoc[this.fieldKey + "-history"] = new List<string>([...history, this._url]); + this.dataDoc[this.fieldKey + '-history'] = new List<string>([...history, this._url]); this.dataDoc[this.fieldKey] = new WebField(new URL(future.pop()!)); if (this._webUrl === this._url) { this._webUrl = curUrl; - setTimeout(action(() => this._webUrl = this._url)); + setTimeout(action(() => (this._webUrl = this._url))); } else { this._webUrl = this._url; } @@ -435,21 +476,21 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps } }); return false; - } + }; back = (checkAvailable?: boolean) => { - const future = Cast(this.dataDoc[this.fieldKey + "-future"], listSpec("string")); - const history = Cast(this.dataDoc[this.fieldKey + "-history"], listSpec("string"), []); + const future = Cast(this.dataDoc[this.fieldKey + '-future'], listSpec('string')); + const history = Cast(this.dataDoc[this.fieldKey + '-history'], listSpec('string'), []); if (checkAvailable) return history.length; runInAction(() => { if (history.length) { const curUrl = this._url; - if (future === undefined) this.dataDoc[this.fieldKey + "-future"] = new List<string>([this._url]); - else this.dataDoc[this.fieldKey + "-future"] = new List<string>([...future, this._url]); + if (future === undefined) this.dataDoc[this.fieldKey + '-future'] = new List<string>([this._url]); + else this.dataDoc[this.fieldKey + '-future'] = new List<string>([...future, this._url]); this.dataDoc[this.fieldKey] = new WebField(new URL(history.pop()!)); if (this._webUrl === this._url) { this._webUrl = curUrl; - setTimeout(action(() => this._webUrl = this._url)); + setTimeout(action(() => (this._webUrl = this._url))); } else { this._webUrl = this._url; } @@ -457,21 +498,26 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps } }); return false; - } + }; static urlHash = (s: string) => { - return Math.abs(s.split('').reduce((a: any, b: any) => { a = ((a << 5) - a) + b.charCodeAt(0); return a & a; }, 0)); - } + return Math.abs( + s.split('').reduce((a: any, b: any) => { + a = (a << 5) - a + b.charCodeAt(0); + return a & a; + }, 0) + ); + }; @action submitURL = (newUrl?: string, preview?: boolean, dontUpdateIframe?: boolean) => { if (!newUrl) return; - if (!newUrl.startsWith("http")) newUrl = "http://" + newUrl; + if (!newUrl.startsWith('http')) newUrl = 'http://' + newUrl; try { - const future = Cast(this.dataDoc[this.fieldKey + "-future"], listSpec("string")); - const history = Cast(this.dataDoc[this.fieldKey + "-history"], listSpec("string")); + const future = Cast(this.dataDoc[this.fieldKey + '-future'], listSpec('string')); + const history = Cast(this.dataDoc[this.fieldKey + '-history'], listSpec('string')); const url = this.webField?.toString(); if (url && !preview) { - this.dataDoc[this.fieldKey + "-history"] = new List<string>([...(history || []), url]); + this.dataDoc[this.fieldKey + '-history'] = new List<string>([...(history || []), url]); this.layoutDoc._scrollTop = 0; if (this._webPageHasBeenRendered) { this.layoutDoc.thumb = undefined; @@ -486,47 +532,54 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps !dontUpdateIframe && (this._webUrl = this._url); } } catch (e) { - console.log("WebBox URL error:" + this._url); + console.log('WebBox URL error:' + this._url); } return true; - } + }; onWebUrlDrop = (e: React.DragEvent) => { const { dataTransfer } = e; - const html = dataTransfer.getData("text/html"); - const uri = dataTransfer.getData("text/uri-list"); - const url = uri || html || this._url || ""; - const newurl = url.startsWith(window.location.origin) ? - url.replace(window.location.origin, this._url?.match(/http[s]?:\/\/[^\/]*/)?.[0] || "") : url; + const html = dataTransfer.getData('text/html'); + const uri = dataTransfer.getData('text/uri-list'); + const url = uri || html || this._url || ''; + const newurl = url.startsWith(window.location.origin) ? url.replace(window.location.origin, this._url?.match(/http[s]?:\/\/[^\/]*/)?.[0] || '') : url; this.submitURL(newurl); e.stopPropagation(); - } + }; onWebUrlValueKeyDown = (e: React.KeyboardEvent) => { - e.key === "Enter" && this.submitURL(this._keyInput.current!.value); + e.key === 'Enter' && this.submitURL(this._keyInput.current!.value); e.stopPropagation(); - } + }; @computed get urlEditor() { return ( - <div className="collectionMenu-webUrlButtons" onDrop={this.onWebUrlDrop} onDragOver={e => e.preventDefault()} > - <input className="collectionMenu-urlInput" key={this._url} + <div className="collectionMenu-webUrlButtons" onDrop={this.onWebUrlDrop} onDragOver={e => e.preventDefault()}> + <input + className="collectionMenu-urlInput" + key={this._url} placeholder="ENTER URL" defaultValue={this._url} onDrop={this.onWebUrlDrop} onDragOver={e => e.preventDefault()} onKeyDown={this.onWebUrlValueKeyDown} - onClick={(e) => { + onClick={e => { this._keyInput.current!.select(); e.stopPropagation(); }} ref={this._keyInput} /> - <div style={{ display: "flex", flexDirection: "row", justifyContent: "space-between", maxWidth: "250px", }}> + <div style={{ display: 'flex', flexDirection: 'row', justifyContent: 'space-between', maxWidth: '250px' }}> <button className="submitUrl" onClick={() => this.submitURL(this._keyInput.current!.value)} onDragOver={e => e.stopPropagation()} onDrop={this.onWebUrlDrop}> GO </button> - <button className="submitUrl" onClick={() => this.back}> <FontAwesomeIcon icon="caret-left" size="lg" /> </button> - <button className="submitUrl" onClick={() => this.forward}> <FontAwesomeIcon icon="caret-right" size="lg" /> </button> + <button className="submitUrl" onClick={() => this.back}> + {' '} + <FontAwesomeIcon icon="caret-left" size="lg" />{' '} + </button> + <button className="submitUrl" onClick={() => this.forward}> + {' '} + <FontAwesomeIcon icon="caret-right" size="lg" />{' '} + </button> </div> </div> ); @@ -535,48 +588,59 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps specificContextMenu = (e: React.MouseEvent | PointerEvent): void => { const cm = ContextMenu.Instance; const funcs: ContextMenuProps[] = []; - if (!cm.findByDescription("Options...")) { - !Doc.noviceMode && funcs.push({ description: (this.layoutDoc.useCors ? "Don't Use" : "Use") + " Cors", event: () => this.layoutDoc.useCors = !this.layoutDoc.useCors, icon: "snowflake" }); + if (!cm.findByDescription('Options...')) { + !Doc.noviceMode && funcs.push({ description: (this.layoutDoc.useCors ? "Don't Use" : 'Use') + ' Cors', event: () => (this.layoutDoc.useCors = !this.layoutDoc.useCors), icon: 'snowflake' }); funcs.push({ - description: (this.layoutDoc.allowScripts ? "Prevent" : "Allow") + " Scripts", event: () => { + description: (this.layoutDoc.allowScripts ? 'Prevent' : 'Allow') + ' Scripts', + event: () => { this.layoutDoc.allowScripts = !this.layoutDoc.allowScripts; if (this._iframe) { - runInAction(() => this._hackHide = true); - setTimeout(action(() => this._hackHide = false)); + runInAction(() => (this._hackHide = true)); + setTimeout(action(() => (this._hackHide = false))); } - }, icon: "snowflake" + }, + icon: 'snowflake', }); funcs.push({ - description: (!this.layoutDoc.forceReflow ? "Force" : "Prevent") + " Reflow", event: () => { + description: (!this.layoutDoc.forceReflow ? 'Force' : 'Prevent') + ' Reflow', + event: () => { const nw = !this.layoutDoc.forceReflow ? undefined : Doc.NativeWidth(this.layoutDoc) - this.sidebarWidth() / (this.props.scaling?.() || 1); this.layoutDoc.forceReflow = !nw; if (nw) { - Doc.SetInPlace(this.layoutDoc, this.fieldKey + "-nativeWidth", nw, true); + Doc.SetInPlace(this.layoutDoc, this.fieldKey + '-nativeWidth', nw, true); } - }, icon: "snowflake" + }, + icon: 'snowflake', }); - cm.addItem({ description: "Options...", subitems: funcs, icon: "asterisk" }); + cm.addItem({ description: 'Options...', subitems: funcs, icon: 'asterisk' }); } - } + }; @action onMarqueeDown = (e: React.PointerEvent) => { - if (!e.altKey && e.button === 0 && this.props.isContentActive(true) && ![InkTool.Highlighter, InkTool.Pen, InkTool.Write].includes(CurrentUserUtils.ActiveTool)) { - setupMoveUpEvents(this, e, action(e => { - MarqueeAnnotator.clearAnnotations(this._savedAnnotations); - this._marqueeing = [e.clientX, e.clientY]; - return true; - }), returnFalse, () => MarqueeAnnotator.clearAnnotations(this._savedAnnotations), false); + if (!e.altKey && e.button === 0 && this.props.isContentActive(true) && ![InkTool.Highlighter, InkTool.Pen, InkTool.Write].includes(Doc.ActiveTool)) { + setupMoveUpEvents( + this, + e, + action(e => { + MarqueeAnnotator.clearAnnotations(this._savedAnnotations); + this._marqueeing = [e.clientX, e.clientY]; + return true; + }), + returnFalse, + () => MarqueeAnnotator.clearAnnotations(this._savedAnnotations), + false + ); } - } + }; @action finishMarquee = (x?: number, y?: number, e?: PointerEvent) => { this._getAnchor = AnchorMenu.Instance?.GetAnchor; this._marqueeing = undefined; this._isAnnotating = false; this._iframeClick = undefined; const sel = this._iframe?.contentDocument?.getSelection(); - if (sel?.empty) sel.empty();// Chrome - else if (sel?.removeAllRanges) sel.removeAllRanges(); // Firefox + if (sel?.empty) sel.empty(); // Chrome + else if (sel?.removeAllRanges) sel.removeAllRanges(); // Firefox if (x !== undefined && y !== undefined) { this._setPreviewCursor?.(x, y, false, false); ContextMenu.Instance.closeMenu(); @@ -586,10 +650,10 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps this.props.docViewPath().lastElement().docView?.onContextMenu(undefined, x, y); } } - } + }; @computed get urlContent() { - if (this._hackHide || (this.webThumb && (!this._webPageHasBeenRendered && LightboxView.LightboxDoc !== this.rootDoc))) return (null); + if (this._hackHide || (this.webThumb && !this._webPageHasBeenRendered && LightboxView.LightboxDoc !== this.rootDoc)) return null; this.props.thumbShown?.(); const field = this.dataDoc[this.props.fieldKey]; let view; @@ -597,149 +661,194 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps view = <span className="webBox-htmlSpan" contentEditable onPointerDown={e => e.stopPropagation()} dangerouslySetInnerHTML={{ __html: field.html }} />; } else if (field instanceof WebField) { const url = this.layoutDoc.useCors ? Utils.CorsProxy(this._webUrl) : this._webUrl; - view = <iframe className="webBox-iframe" enable-annotation={"true"} - style={{ pointerEvents: this._scrollTimer ? "none" : undefined }} - ref={action((r: HTMLIFrameElement | null) => this._iframe = r)} src={url} onLoad={this.iframeLoaded} - // 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`} />; + view = ( + <iframe + className="webBox-iframe" + enable-annotation={'true'} + style={{ pointerEvents: this._scrollTimer ? 'none' : undefined }} + ref={action((r: HTMLIFrameElement | null) => (this._iframe = r))} + src={url} + onLoad={this.iframeLoaded} + // 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`} + /> + ); } else { - view = <iframe className="webBox-iframe" enable-annotation={"true"} - style={{ pointerEvents: this._scrollTimer ? "none" : undefined }} // if we allow pointer events when scrolling is on, then reversing direction does not work smoothly - ref={action((r: HTMLIFrameElement | null) => this._iframe = r)} src={"https://crossorigin.me/https://cs.brown.edu"} />; + view = ( + <iframe + className="webBox-iframe" + enable-annotation={'true'} + style={{ pointerEvents: this._scrollTimer ? 'none' : undefined }} // if we allow pointer events when scrolling is on, then reversing direction does not work smoothly + ref={action((r: HTMLIFrameElement | null) => (this._iframe = r))} + src={'https://crossorigin.me/https://cs.brown.edu'} + /> + ); } - setTimeout(action(() => { - this._scrollHeight = Math.max(this._scrollHeight, this._iframe && this._iframe.contentDocument && this._iframe.contentDocument.body ? this._iframe.contentDocument.body.scrollHeight : 0); - if (this._initialScroll === undefined && !this._webPageHasBeenRendered) { - this.setScrollPos(NumCast(this.layoutDoc.thumbScrollTop, NumCast(this.layoutDoc.scrollTop))); - } - this._webPageHasBeenRendered = true; - })); + setTimeout( + action(() => { + this._scrollHeight = Math.max(this._scrollHeight, this._iframe && this._iframe.contentDocument && this._iframe.contentDocument.body ? this._iframe.contentDocument.body.scrollHeight : 0); + if (this._initialScroll === undefined && !this._webPageHasBeenRendered) { + this.setScrollPos(NumCast(this.layoutDoc.thumbScrollTop, NumCast(this.layoutDoc.scrollTop))); + } + this._webPageHasBeenRendered = true; + }) + ); return view; } addDocumentWrapper = (doc: Doc | Doc[], annotationKey?: string) => { - (doc instanceof Doc ? [doc] : doc).forEach(doc => doc.webUrl = this._url); + (doc instanceof Doc ? [doc] : doc).forEach(doc => (doc.webUrl = this._url)); return this.addDocument(doc, annotationKey); - } + }; sidebarAddDocument = (doc: Doc | Doc[], sidebarKey?: string) => { if (!this.layoutDoc._showSidebar) this.toggleSidebar(); return this.addDocumentWrapper(doc, sidebarKey); - } + }; @observable _draggingSidebar = false; - sidebarBtnDown = (e: React.PointerEvent, onButton: boolean) => { // onButton determines whether the width of the pdf box changes, or just the ratio of the sidebar to the pdf - setupMoveUpEvents(this, e, action((e, down, delta) => { - this._draggingSidebar = true; - const localDelta = this.props.ScreenToLocalTransform().scale(this.props.scaling?.() || 1).transformDirection(delta[0], delta[1]); - const nativeWidth = NumCast(this.layoutDoc[this.fieldKey + "-nativeWidth"]); - const nativeHeight = NumCast(this.layoutDoc[this.fieldKey + "-nativeHeight"]); - const curNativeWidth = NumCast(this.layoutDoc.nativeWidth, nativeWidth); - const ratio = (curNativeWidth + (onButton ? 1 : -1) * localDelta[0] / (this.props.scaling?.() || 1)) / nativeWidth; - if (ratio >= 1) { - this.layoutDoc.nativeWidth = nativeWidth * ratio; - this.layoutDoc.nativeHeight = nativeHeight * (1 + ratio); - onButton && (this.layoutDoc._width = this.layoutDoc[WidthSym]() + localDelta[0]); - this.layoutDoc._showSidebar = nativeWidth !== this.layoutDoc._nativeWidth; - } - return false; - }), action(() => this._draggingSidebar = false), () => this.toggleSidebar()); - } + sidebarBtnDown = (e: React.PointerEvent, onButton: boolean) => { + // onButton determines whether the width of the pdf box changes, or just the ratio of the sidebar to the pdf + setupMoveUpEvents( + this, + e, + action((e, down, delta) => { + this._draggingSidebar = true; + const localDelta = this.props + .ScreenToLocalTransform() + .scale(this.props.scaling?.() || 1) + .transformDirection(delta[0], delta[1]); + const nativeWidth = NumCast(this.layoutDoc[this.fieldKey + '-nativeWidth']); + const nativeHeight = NumCast(this.layoutDoc[this.fieldKey + '-nativeHeight']); + const curNativeWidth = NumCast(this.layoutDoc.nativeWidth, nativeWidth); + const ratio = (curNativeWidth + ((onButton ? 1 : -1) * localDelta[0]) / (this.props.scaling?.() || 1)) / nativeWidth; + if (ratio >= 1) { + this.layoutDoc.nativeWidth = nativeWidth * ratio; + this.layoutDoc.nativeHeight = nativeHeight * (1 + ratio); + onButton && (this.layoutDoc._width = this.layoutDoc[WidthSym]() + localDelta[0]); + this.layoutDoc._showSidebar = nativeWidth !== this.layoutDoc._nativeWidth; + } + return false; + }), + action(() => (this._draggingSidebar = false)), + () => this.toggleSidebar() + ); + }; @observable _previewNativeWidth: Opt<number> = undefined; @observable _previewWidth: Opt<number> = undefined; toggleSidebar = action((preview: boolean = false) => { - var nativeWidth = NumCast(this.layoutDoc[this.fieldKey + "-nativeWidth"]); + var nativeWidth = NumCast(this.layoutDoc[this.fieldKey + '-nativeWidth']); if (!nativeWidth) { const defaultNativeWidth = this.dataDoc[this.fieldKey] instanceof WebField ? 850 : this.Document[WidthSym](); Doc.SetNativeWidth(this.dataDoc, Doc.NativeWidth(this.dataDoc) || defaultNativeWidth); - Doc.SetNativeHeight(this.dataDoc, Doc.NativeHeight(this.dataDoc) || this.Document[HeightSym]() / this.Document[WidthSym]() * defaultNativeWidth); - nativeWidth = NumCast(this.layoutDoc[this.fieldKey + "-nativeWidth"]); + Doc.SetNativeHeight(this.dataDoc, Doc.NativeHeight(this.dataDoc) || (this.Document[HeightSym]() / this.Document[WidthSym]()) * defaultNativeWidth); + nativeWidth = NumCast(this.layoutDoc[this.fieldKey + '-nativeWidth']); } const sideratio = ((!this.layoutDoc.nativeWidth || this.layoutDoc.nativeWidth === nativeWidth ? WebBox.openSidebarWidth : 0) + nativeWidth) / nativeWidth; const pdfratio = ((!this.layoutDoc.nativeWidth || this.layoutDoc.nativeWidth === nativeWidth ? WebBox.openSidebarWidth + WebBox.sidebarResizerWidth : 0) + nativeWidth) / nativeWidth; const curNativeWidth = NumCast(this.layoutDoc.nativeWidth, nativeWidth); if (preview) { this._previewNativeWidth = nativeWidth * sideratio; - this._previewWidth = this.layoutDoc[WidthSym]() * nativeWidth * sideratio / curNativeWidth; + this._previewWidth = (this.layoutDoc[WidthSym]() * nativeWidth * sideratio) / curNativeWidth; this._showSidebar = true; - } - else { + } else { this.layoutDoc._showSidebar = !this.layoutDoc._showSidebar; - this.layoutDoc._width = this.layoutDoc[WidthSym]() * nativeWidth * pdfratio / curNativeWidth; + this.layoutDoc._width = (this.layoutDoc[WidthSym]() * nativeWidth * pdfratio) / curNativeWidth; if (!this.layoutDoc._showSidebar && !(this.dataDoc[this.fieldKey] instanceof WebField)) { - this.layoutDoc.nativeWidth = this.dataDoc[this.fieldKey + "-nativeWidth"] = undefined; + this.layoutDoc.nativeWidth = this.dataDoc[this.fieldKey + '-nativeWidth'] = undefined; } else { this.layoutDoc.nativeWidth = nativeWidth * pdfratio; } } }); - sidebarWidth = () => !this.SidebarShown ? 0 : - WebBox.sidebarResizerWidth + (this._previewWidth ? WebBox.openSidebarWidth : - (NumCast(this.layoutDoc.nativeWidth) - Doc.NativeWidth(this.dataDoc)) * this.props.PanelWidth() / NumCast(this.layoutDoc.nativeWidth)) + sidebarWidth = () => + !this.SidebarShown ? 0 : WebBox.sidebarResizerWidth + (this._previewWidth ? WebBox.openSidebarWidth : ((NumCast(this.layoutDoc.nativeWidth) - Doc.NativeWidth(this.dataDoc)) * this.props.PanelWidth()) / NumCast(this.layoutDoc.nativeWidth)); @computed get content() { - const interactive = !this.props.docViewPath().lastElement()?.docView?._pendingDoubleClick && this.props.isContentActive() && this.props.pointerEvents?.() !== "none" && CurrentUserUtils.ActiveTool === InkTool.None && !DocumentDecorations.Instance?.Interacting; - return <div className={"webBox-cont" + (interactive ? "-interactive" : "")} - onKeyDown={e => e.stopPropagation()} - style={{ width: !this.layoutDoc.forceReflow ? NumCast(this.layoutDoc[this.fieldKey + "-nativeWidth"]) || `100%` : "100%", }}> - {this.urlContent} - </div>; + const interactive = + !this.props.docViewPath().lastElement()?.docView?._pendingDoubleClick && this.props.isContentActive() && this.props.pointerEvents?.() !== 'none' && Doc.ActiveTool === InkTool.None && !DocumentDecorations.Instance?.Interacting; + return ( + <div className={'webBox-cont' + (interactive ? '-interactive' : '')} onKeyDown={e => e.stopPropagation()} style={{ width: !this.layoutDoc.forceReflow ? NumCast(this.layoutDoc[this.fieldKey + '-nativeWidth']) || `100%` : '100%' }}> + {this.urlContent} + </div> + ); } @computed get annotationLayer() { TraceMobx(); - return <div className="webBox-annotationLayer" style={{ height: Doc.NativeHeight(this.Document) || undefined }} ref={this._annotationLayer}> - {this.inlineTextAnnotations.sort((a, b) => NumCast(a.y) - NumCast(b.y)).map(anno => - <Annotation {...this.props} fieldKey={this.annotationKey} pointerEvents={this.pointerEvents} showInfo={this.showInfo} dataDoc={this.dataDoc} anno={anno} key={`${anno[Id]}-annotation`} />)} - </div>; - + return ( + <div className="webBox-annotationLayer" style={{ height: Doc.NativeHeight(this.Document) || undefined }} ref={this._annotationLayer}> + {this.inlineTextAnnotations + .sort((a, b) => NumCast(a.y) - NumCast(b.y)) + .map(anno => ( + <Annotation {...this.props} fieldKey={this.annotationKey} pointerEvents={this.pointerEvents} showInfo={this.showInfo} dataDoc={this.dataDoc} anno={anno} key={`${anno[Id]}-annotation`} /> + ))} + </div> + ); + } + @computed get SidebarShown() { + return this._showSidebar || this.layoutDoc._showSidebar ? true : false; } - @computed get SidebarShown() { return this._showSidebar || this.layoutDoc._showSidebar ? true : false; } @computed get searchUI() { - return <div className="webBox-ui" - onPointerDown={e => e.stopPropagation()} style={{ display: this.props.isContentActive() ? "flex" : "none" }}> - <div className="webBox-overlayCont" onPointerDown={(e) => e.stopPropagation()} style={{ left: `${this._searching ? 0 : 100}%` }}> - <button className="webBox-overlayButton" title={"search"} /> - <input className="webBox-searchBar" placeholder="Search" ref={this._searchRef} onChange={this.searchStringChanged} - onKeyDown={e => { e.key === "Enter" && this.search(this._searchString, e.shiftKey); e.stopPropagation(); }} /> - <button className="webBox-search" title="Search" onClick={e => this.search(this._searchString, e.shiftKey)}> - <FontAwesomeIcon icon="search" size="sm" /> + return ( + <div className="webBox-ui" onPointerDown={e => e.stopPropagation()} style={{ display: this.props.isContentActive() ? 'flex' : 'none' }}> + <div className="webBox-overlayCont" onPointerDown={e => e.stopPropagation()} style={{ left: `${this._searching ? 0 : 100}%` }}> + <button className="webBox-overlayButton" title={'search'} /> + <input + className="webBox-searchBar" + placeholder="Search" + ref={this._searchRef} + onChange={this.searchStringChanged} + onKeyDown={e => { + e.key === 'Enter' && this.search(this._searchString, e.shiftKey); + e.stopPropagation(); + }} + /> + <button className="webBox-search" title="Search" onClick={e => this.search(this._searchString, e.shiftKey)}> + <FontAwesomeIcon icon="search" size="sm" /> + </button> + </div> + <button + className="webBox-overlayButton" + title={'search'} + onClick={action(() => { + this._searching = !this._searching; + this.search('', false, true); + })}> + <div className="webBox-overlayButton-arrow" onPointerDown={e => e.stopPropagation()} /> + <div className="webBox-overlayButton-iconCont" onPointerDown={e => e.stopPropagation()}> + <FontAwesomeIcon icon={this._searching ? 'times' : 'search'} size="lg" /> + </div> </button> </div> - <button className="webBox-overlayButton" title={"search"} - onClick={action(() => { this._searching = !this._searching; this.search("", false, true); })} > - <div className="webBox-overlayButton-arrow" onPointerDown={(e) => e.stopPropagation()} /> - <div className="webBox-overlayButton-iconCont" onPointerDown={(e) => e.stopPropagation()}> - <FontAwesomeIcon icon={this._searching ? "times" : "search"} size="lg" /> - </div> - </button> - </div>; + ); } - 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; + 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); panelWidth = () => this.props.PanelWidth() / (this.props.scaling?.() || 1) - this.sidebarWidth() + WebBox.sidebarResizerWidth; // (this.Document.scrollHeight || Doc.NativeHeight(this.Document) || 0); panelHeight = () => this.props.PanelHeight() / (this.props.scaling?.() || 1); // () => this._pageSizes.length && this._pageSizes[0] ? this._pageSizes[0].width : Doc.NativeWidth(this.Document); scrollXf = () => this.props.ScreenToLocalTransform().translate(0, NumCast(this.layoutDoc._scrollTop)); anchorMenuClick = () => this._sidebarRef.current?.anchorMenuClick; - basicFilter = () => [...this.props.docFilters(), Utils.PropUnsetFilter("textInlineAnnotations")]; + basicFilter = () => [...this.props.docFilters(), Utils.PropUnsetFilter('textInlineAnnotations')]; transparentFilter = () => [...this.props.docFilters(), Utils.IsTransparentFilter()]; opaqueFilter = () => [...this.props.docFilters(), Utils.IsOpaqueFilter()]; - childStyleProvider = (doc: (Doc | undefined), props: Opt<DocumentViewProps>, property: string): any => { + childStyleProvider = (doc: Doc | undefined, props: Opt<DocumentViewProps>, property: string): any => { if (doc instanceof Doc && property === StyleProp.PointerEvents) { - if (doc.textInlineAnnotations) return "none"; + if (doc.textInlineAnnotations) return 'none'; } return this.props.styleProvider?.(doc, props, property); - } - pointerEvents = () => !this._draggingSidebar && this.props.isContentActive() && this.props.pointerEvents?.() !== "none" && !MarqueeOptionsMenu.Instance.isShown() ? "all" : SnappingManager.GetIsDragging() ? undefined : "none"; - annotationPointerEvents = () => this._isAnnotating || SnappingManager.GetIsDragging() ? "all" : "none"; + }; + pointerEvents = () => (!this._draggingSidebar && this.props.isContentActive() && this.props.pointerEvents?.() !== 'none' && !MarqueeOptionsMenu.Instance.isShown() ? 'all' : SnappingManager.GetIsDragging() ? undefined : 'none'); + annotationPointerEvents = () => (this._isAnnotating || SnappingManager.GetIsDragging() ? 'all' : 'none'); render() { - const pointerEvents = this.layoutDoc._lockedPosition ? "none" : this.props.pointerEvents?.() as any; + const pointerEvents = this.layoutDoc._lockedPosition ? 'none' : (this.props.pointerEvents?.() as any); const previewScale = this._previewNativeWidth ? 1 - this.sidebarWidth() / this._previewNativeWidth : 1; const scale = previewScale * (this.props.scaling?.() || 1); - const renderAnnotations = (docFilters?: () => string[]) => - <CollectionFreeFormView {...OmitKeys(this.props, ["NativeWidth", "NativeHeight", "setContentView"]).omit} + const renderAnnotations = (docFilters?: () => string[]) => ( + <CollectionFreeFormView + {...OmitKeys(this.props, ['NativeWidth', 'NativeHeight', 'setContentView']).omit} renderDepth={this.props.renderDepth + 1} isAnnotationOverlay={true} fieldKey={this.annotationKey} @@ -749,7 +858,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps PanelHeight={this.panelHeight} ScreenToLocalTransform={this.scrollXf} scaling={returnOne} - dropAction={"alias"} + dropAction={'alias'} docFilters={docFilters || this.basicFilter} dontRenderDocuments={docFilters ? false : true} select={emptyFunction} @@ -760,61 +869,76 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps moveDocument={this.moveDocument} addDocument={this.addDocument} styleProvider={this.childStyleProvider} - childPointerEvents={this.props.isContentActive() ? "all" : undefined} - pointerEvents={this.annotationPointerEvents} />; + childPointerEvents={this.props.isContentActive() ? 'all' : undefined} + pointerEvents={this.annotationPointerEvents} + /> + ); return ( - <div className="webBox" ref={this._mainCont} - style={{ pointerEvents: this.pointerEvents(), display: this.props.thumbShown?.() ? "none" : undefined }} > + <div className="webBox" ref={this._mainCont} style={{ pointerEvents: this.pointerEvents(), display: this.props.thumbShown?.() ? 'none' : undefined }}> <div className="webBox-background" style={{ backgroundColor: this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.BackgroundColor) }} /> - <div className="webBox-container" style={{ - width: `calc(${100 / scale}% - ${!this.SidebarShown ? 0 : (this.sidebarWidth() - WebBox.sidebarResizerWidth) / scale * (this._previewWidth ? scale : 1)}px)`, - transform: `scale(${scale})`, - pointerEvents - }} onContextMenu={this.specificContextMenu}> - <div className={"webBox-outerContent"} ref={this._outerRef} + <div + className="webBox-container" + style={{ + width: `calc(${100 / scale}% - ${!this.SidebarShown ? 0 : ((this.sidebarWidth() - WebBox.sidebarResizerWidth) / scale) * (this._previewWidth ? scale : 1)}px)`, + transform: `scale(${scale})`, + pointerEvents, + }} + onContextMenu={this.specificContextMenu}> + <div + className={'webBox-outerContent'} + ref={this._outerRef} style={{ height: `${100 / scale}%`, - pointerEvents + pointerEvents, }} - onWheel={e => { e.stopPropagation(); e.preventDefault(); }} // block wheel events from propagating since they're handled by the iframe + onWheel={e => { + e.stopPropagation(); + e.preventDefault(); + }} // block wheel events from propagating since they're handled by the iframe onScroll={e => this.setDashScrollTop(this._outerRef.current?.scrollTop || 0)} - onPointerDown={this.onMarqueeDown} - > - <div className={"webBox-innerContent"} style={{ height: this._webPageHasBeenRendered ? NumCast(this.scrollHeight, 50) : "100%", pointerEvents }}> + onPointerDown={this.onMarqueeDown}> + <div className={'webBox-innerContent'} style={{ height: this._webPageHasBeenRendered ? NumCast(this.scrollHeight, 50) : '100%', pointerEvents }}> {this.content} - <div style={{ mixBlendMode: "multiply" }}> - {renderAnnotations(this.transparentFilter)} - </div> + <div style={{ mixBlendMode: 'multiply' }}>{renderAnnotations(this.transparentFilter)}</div> {renderAnnotations(this.opaqueFilter)} - {SnappingManager.GetIsDragging() ? (null) : renderAnnotations()} + {SnappingManager.GetIsDragging() ? null : renderAnnotations()} {this.annotationLayer} </div> </div> - {!this._marqueeing || !this._mainCont.current || !this._annotationLayer.current ? (null) : - <div style={{ transformOrigin: "top left", transform: `scale(${1 / scale})` }}> - <MarqueeAnnotator rootDoc={this.rootDoc} + {!this._marqueeing || !this._mainCont.current || !this._annotationLayer.current ? null : ( + <div style={{ transformOrigin: 'top left', transform: `scale(${1 / scale})` }}> + <MarqueeAnnotator + rootDoc={this.rootDoc} iframe={this.isFirefox() ? this.iframeClick : undefined} iframeScaling={this.isFirefox() ? this.iframeScaling : undefined} anchorMenuClick={this.anchorMenuClick} scrollTop={0} - down={this._marqueeing} scaling={returnOne} + down={this._marqueeing} + scaling={returnOne} addDocument={this.addDocumentWrapper} docView={this.props.docViewPath().lastElement()} finishMarquee={this.finishMarquee} savedAnnotations={this.savedAnnotationsCreator} annotationLayer={this._annotationLayer.current} - mainCont={this._mainCont.current} /> </div>} - </div > - <div className="webBox-sideResizer" style={{ - display: this.SidebarShown ? undefined : "none", - width: WebBox.sidebarResizerWidth, - left: `calc(100% - ${this.sidebarWidth() - WebBox.sidebarResizerWidth}px)` - }} - onPointerDown={e => this.sidebarBtnDown(e, false)} /> - <SidebarAnnos ref={this._sidebarRef} + mainCont={this._mainCont.current} + />{' '} + </div> + )} + </div> + <div + className="webBox-sideResizer" + style={{ + display: this.SidebarShown ? undefined : 'none', + width: WebBox.sidebarResizerWidth, + left: `calc(100% - ${this.sidebarWidth() - WebBox.sidebarResizerWidth}px)`, + }} + onPointerDown={e => this.sidebarBtnDown(e, false)} + /> + <SidebarAnnos + ref={this._sidebarRef} {...this.props} whenChildContentsActiveChanged={this.whenChildContentsActiveChanged} - fieldKey={this.fieldKey + "-" + this._urlHash} + fieldKey={this.fieldKey + '-' + this._urlHash} rootDoc={this.rootDoc} layoutDoc={this.layoutDoc} dataDoc={this.dataDoc} @@ -825,17 +949,23 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps moveDocument={this.moveDocument} removeDocument={this.removeDocument} /> - <div className="webBox-overlayButton-sidebar" key="sidebar" title="Toggle Sidebar" + <div + className="webBox-overlayButton-sidebar" + key="sidebar" + title="Toggle Sidebar" style={{ - display: !this.props.isContentActive() ? "none" : undefined, - top: StrCast(this.rootDoc._showTitle) === "title" ? 20 : 5, - backgroundColor: this.SidebarShown ? Colors.MEDIUM_BLUE : Colors.BLACK + display: !this.props.isContentActive() ? 'none' : undefined, + top: StrCast(this.rootDoc._showTitle) === 'title' ? 20 : 5, + backgroundColor: this.SidebarShown ? Colors.MEDIUM_BLUE : Colors.BLACK, }} - onPointerDown={e => this.sidebarBtnDown(e, true)} > - <FontAwesomeIcon style={{ color: Colors.WHITE }} icon={"comment-alt"} size="sm" /> + onPointerDown={e => this.sidebarBtnDown(e, true)}> + <FontAwesomeIcon style={{ color: Colors.WHITE }} icon={'comment-alt'} size="sm" /> </div> - {!this.props.isContentActive() ? (null) : this.searchUI} - </div>); + {!this.props.isContentActive() ? null : this.searchUI} + </div> + ); } } -ScriptingGlobals.add(function urlHash(url: string) { return WebBox.urlHash(url); });
\ No newline at end of file +ScriptingGlobals.add(function urlHash(url: string) { + return WebBox.urlHash(url); +}); diff --git a/src/client/views/nodes/button/FontIconBox.tsx b/src/client/views/nodes/button/FontIconBox.tsx index 539fb5c99..1ce97979e 100644 --- a/src/client/views/nodes/button/FontIconBox.tsx +++ b/src/client/views/nodes/button/FontIconBox.tsx @@ -1,7 +1,6 @@ import { IconProp } from '@fortawesome/fontawesome-svg-core'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { Tooltip } from '@material-ui/core'; -import { Fragment, Mark, Node, Slice } from 'prosemirror-model'; import { action, computed, observable } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; @@ -12,13 +11,11 @@ import { ScriptField } from '../../../../fields/ScriptField'; import { BoolCast, Cast, NumCast, ScriptCast, StrCast } from '../../../../fields/Types'; import { WebField } from '../../../../fields/URLField'; import { aggregateBounds, Utils } from '../../../../Utils'; -import { DocumentType } from '../../../documents/DocumentTypes'; -import { CurrentUserUtils } from '../../../util/CurrentUserUtils'; +import { CollectionViewType, DocumentType } from '../../../documents/DocumentTypes'; import { ScriptingGlobals } from '../../../util/ScriptingGlobals'; import { SelectionManager } from '../../../util/SelectionManager'; import { undoBatch, UndoManager } from '../../../util/UndoManager'; import { CollectionFreeFormView } from '../../collections/collectionFreeForm'; -import { CollectionViewType } from '../../collections/CollectionView'; import { ContextMenu } from '../../ContextMenu'; import { DocComponent } from '../../DocComponent'; import { EditableView } from '../../EditableView'; @@ -733,7 +730,7 @@ ScriptingGlobals.add(function toggleItalic(checkResult?: boolean) { export function checkInksToGroup() { // console.log("getting here to inks group"); - if (CurrentUserUtils.ActiveTool === InkTool.Write) { + if (Doc.ActiveTool === InkTool.Write) { CollectionFreeFormView.collectionsWithUnprocessedInk.forEach(ffView => { // TODO: nda - will probably want to go through ffView unprocessed docs and then see if any of the inksToGroup docs are in it and only use those // find all inkDocs in ffView.unprocessedDocs that are within 200 pixels of each other @@ -746,7 +743,7 @@ export function checkInksToGroup() { export function createInkGroup(inksToGroup?: Doc[], isSubGroup?: boolean) { // TODO nda - if document being added to is a inkGrouping then we can just add to that group - if (CurrentUserUtils.ActiveTool === InkTool.Write) { + if (Doc.ActiveTool === InkTool.Write) { CollectionFreeFormView.collectionsWithUnprocessedInk.forEach(ffView => { // TODO: nda - will probably want to go through ffView unprocessed docs and then see if any of the inksToGroup docs are in it and only use those const selected = ffView.unprocessedDocs; @@ -819,30 +816,30 @@ export function createInkGroup(inksToGroup?: Doc[], isSubGroup?: boolean) { ScriptingGlobals.add(function setActiveTool(tool: string, checkResult?: boolean) { InkTranscription.Instance?.createInkGroup(); if (checkResult) { - return (CurrentUserUtils.ActiveTool === tool && !GestureOverlay.Instance?.InkShape) || GestureOverlay.Instance?.InkShape === tool ? Colors.MEDIUM_BLUE : 'transparent'; + return (Doc.ActiveTool === tool && !GestureOverlay.Instance?.InkShape) || GestureOverlay.Instance?.InkShape === tool ? Colors.MEDIUM_BLUE : 'transparent'; } if (['circle', 'square', 'line'].includes(tool)) { if (GestureOverlay.Instance.InkShape === tool) { - CurrentUserUtils.ActiveTool = InkTool.None; + Doc.ActiveTool = InkTool.None; GestureOverlay.Instance.InkShape = InkTool.None; } else { - CurrentUserUtils.ActiveTool = InkTool.Pen; + Doc.ActiveTool = InkTool.Pen; GestureOverlay.Instance.InkShape = tool; } } else if (tool) { // pen or eraser - if (CurrentUserUtils.ActiveTool === tool && !GestureOverlay.Instance.InkShape) { - CurrentUserUtils.ActiveTool = InkTool.None; + if (Doc.ActiveTool === tool && !GestureOverlay.Instance.InkShape) { + Doc.ActiveTool = InkTool.None; } else if (tool == InkTool.Write) { // console.log("write mode selected - create groupDoc here!", tool) - CurrentUserUtils.ActiveTool = tool; + Doc.ActiveTool = tool; GestureOverlay.Instance.InkShape = ''; } else { - CurrentUserUtils.ActiveTool = tool as any; + Doc.ActiveTool = tool as any; GestureOverlay.Instance.InkShape = ''; } } else { - CurrentUserUtils.ActiveTool = InkTool.None; + Doc.ActiveTool = InkTool.None; } }); diff --git a/src/client/views/nodes/formattedText/DashDocView.tsx b/src/client/views/nodes/formattedText/DashDocView.tsx index 9d203b6cc..08f255cab 100644 --- a/src/client/views/nodes/formattedText/DashDocView.tsx +++ b/src/client/views/nodes/formattedText/DashDocView.tsx @@ -1,18 +1,17 @@ -import { IReactionDisposer, reaction, observable, action } from 'mobx'; +import { action, IReactionDisposer, observable, reaction } from 'mobx'; +import { observer } from 'mobx-react'; import { NodeSelection } from 'prosemirror-state'; +import * as ReactDOM from 'react-dom'; import { Doc, HeightSym, WidthSym } from '../../../../fields/Doc'; -import { Cast, StrCast, NumCast } from '../../../../fields/Types'; -import { emptyFunction, returnEmptyDoclist, returnEmptyFilter, returnEmptyString, returnFalse, Utils, returnTransparent } from '../../../../Utils'; +import { Cast, NumCast, StrCast } from '../../../../fields/Types'; +import { emptyFunction, returnFalse, Utils } from '../../../../Utils'; import { DocServer } from '../../../DocServer'; import { Docs, DocUtils } from '../../../documents/Documents'; -import { CurrentUserUtils } from '../../../util/CurrentUserUtils'; +import { ColorScheme } from '../../../util/SettingsManager'; import { Transform } from '../../../util/Transform'; import { DocumentView } from '../DocumentView'; import { FormattedTextBox } from './FormattedTextBox'; import React = require('react'); -import * as ReactDOM from 'react-dom'; -import { observer } from 'mobx-react'; -import { ColorScheme } from '../../../util/SettingsManager'; export class DashDocView { dom: HTMLSpanElement; // container for label and value @@ -21,7 +20,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, CurrentUserUtils.ActiveDashboard?.colorScheme === ColorScheme.Dark ? 'dimgray' : 'lightGray'); + this.dom.style.border = '1px solid ' + StrCast(tbox.layoutDoc.color, Doc.ActiveDashboard?.colorScheme === ColorScheme.Dark ? 'dimgray' : '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/DashFieldView.tsx b/src/client/views/nodes/formattedText/DashFieldView.tsx index 940ed6386..8c8b74560 100644 --- a/src/client/views/nodes/formattedText/DashFieldView.tsx +++ b/src/client/views/nodes/formattedText/DashFieldView.tsx @@ -8,7 +8,6 @@ import { SchemaHeaderField } from '../../../../fields/SchemaHeaderField'; import { ComputedField } from '../../../../fields/ScriptField'; import { Cast, StrCast } from '../../../../fields/Types'; import { DocServer } from '../../../DocServer'; -import { CollectionViewType } from '../../collections/CollectionView'; import './DashFieldView.scss'; import { FormattedTextBox } from './FormattedTextBox'; import React = require('react'); @@ -16,6 +15,7 @@ import { emptyFunction, returnFalse, setupMoveUpEvents } from '../../../../Utils import { AntimodeMenu, AntimodeMenuProps } from '../../AntimodeMenu'; import { Tooltip } from '@material-ui/core'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { CollectionViewType } from '../../../documents/DocumentTypes'; export class DashFieldView { dom: HTMLDivElement; // container for label and value diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx index 61940ac09..cfaa428f9 100644 --- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx +++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx @@ -25,7 +25,6 @@ import { GoogleApiClientUtils, Pulls, Pushes } from '../../../apis/google_docs/G import { DocServer } from '../../../DocServer'; import { Docs, DocUtils } from '../../../documents/Documents'; import { DocumentType } from '../../../documents/DocumentTypes'; -import { CurrentUserUtils } from '../../../util/CurrentUserUtils'; import { DictationManager } from '../../../util/DictationManager'; import { DocumentManager } from '../../../util/DocumentManager'; import { DragManager } from '../../../util/DragManager'; @@ -259,7 +258,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps e.preventDefault(); e.stopPropagation(); const targetCreator = (annotationOn?: Doc) => { - const target = CurrentUserUtils.GetNewTextDoc('Note linked to ' + this.rootDoc.title, 0, 0, 100, 100, undefined, annotationOn); + const target = DocUtils.GetNewTextDoc('Note linked to ' + this.rootDoc.title, 0, 0, 100, 100, undefined, annotationOn); FormattedTextBox.SelectOnLoad = target[Id]; return target; }; @@ -386,7 +385,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps var tr = this._editorView.state.tr as any; const autoAnch = this._editorView.state.schema.marks.autoLinkAnchor; tr = tr.removeMark(0, tr.doc.content.size, autoAnch); - DocListCast(CurrentUserUtils.MyPublishedDocs.data).forEach(term => (tr = this.hyperlinkTerm(tr, term, newAutoLinks))); + DocListCast(Doc.MyPublishedDocs.data).forEach(term => (tr = this.hyperlinkTerm(tr, term, newAutoLinks))); tr = tr.setSelection(new TextSelection(tr.doc.resolve(f), tr.doc.resolve(t))); this._editorView?.dispatch(tr); oldAutoLinks.filter(oldLink => !newAutoLinks.has(oldLink) && oldLink.anchor2 !== this.rootDoc).forEach(LinkManager.Instance.deleteLink); @@ -411,7 +410,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps if (!(cfield instanceof ComputedField)) { this.dataDoc.title = prefix + str.substring(0, Math.min(40, str.length)) + (str.length > 40 ? '...' : ''); if (str.startsWith('@') && str.length > 1) { - Doc.AddDocToList(CurrentUserUtils.MyPublishedDocs, undefined, this.rootDoc); + Doc.AddDocToList(Doc.MyPublishedDocs, undefined, this.rootDoc); } } } @@ -1774,7 +1773,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps ); }; return ( - <div className={'formattedTextBox-sidebar' + (CurrentUserUtils.ActiveTool !== InkTool.None ? '-inking' : '')} style={{ width: `${this.sidebarWidthPercent}`, backgroundColor: `${this.sidebarColor}` }}> + <div className={'formattedTextBox-sidebar' + (Doc.ActiveTool !== InkTool.None ? '-inking' : '')} style={{ width: `${this.sidebarWidthPercent}`, backgroundColor: `${this.sidebarColor}` }}> {renderComponent(StrCast(this.layoutDoc.sidebarViewType))} </div> ); @@ -1785,7 +1784,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps const selected = active; const scale = (this.props.scaling?.() || 1) * NumCast(this.layoutDoc._viewScale, 1); const rounded = StrCast(this.layoutDoc.borderRounding) === '100%' ? '-rounded' : ''; - const interactive = (CurrentUserUtils.ActiveTool === InkTool.None || SnappingManager.GetIsDragging()) && (this.layoutDoc.z || !this.layoutDoc._lockedPosition); + const interactive = (Doc.ActiveTool === InkTool.None || SnappingManager.GetIsDragging()) && (this.layoutDoc.z || !this.layoutDoc._lockedPosition); if (!selected && FormattedTextBoxComment.textBox === this) setTimeout(FormattedTextBoxComment.Hide); const minimal = this.props.ignoreAutoHeight; const paddingX = NumCast(this.layoutDoc._xMargin, this.props.xPadding || 0); diff --git a/src/client/views/nodes/trails/PresBox.tsx b/src/client/views/nodes/trails/PresBox.tsx index 91f8d1efc..292c187e4 100644 --- a/src/client/views/nodes/trails/PresBox.tsx +++ b/src/client/views/nodes/trails/PresBox.tsx @@ -8,19 +8,19 @@ import { Bounce, Fade, Flip, LightSpeed, Roll, Rotate, Zoom } from 'react-reveal import { Doc, DocListCast, DocListCastAsync, FieldResult } from '../../../../fields/Doc'; import { InkTool } from '../../../../fields/InkField'; import { List } from '../../../../fields/List'; -import { PrefetchProxy } from '../../../../fields/Proxy'; import { listSpec } from '../../../../fields/Schema'; import { BoolCast, Cast, DocCast, NumCast, StrCast } from '../../../../fields/Types'; import { emptyFunction, returnFalse, returnOne, returnTrue, setupMoveUpEvents } from '../../../../Utils'; import { Docs } from '../../../documents/Documents'; -import { DocumentType } from '../../../documents/DocumentTypes'; -import { CurrentUserUtils } from '../../../util/CurrentUserUtils'; +import { CollectionViewType, DocumentType } from '../../../documents/DocumentTypes'; import { DocumentManager } from '../../../util/DocumentManager'; +import { ScriptingGlobals } from '../../../util/ScriptingGlobals'; import { SelectionManager } from '../../../util/SelectionManager'; +import { SettingsManager } from '../../../util/SettingsManager'; import { undoBatch, UndoManager } from '../../../util/UndoManager'; import { CollectionDockingView } from '../../collections/CollectionDockingView'; import { MarqueeViewBounds } from '../../collections/collectionFreeForm'; -import { CollectionView, CollectionViewType } from '../../collections/CollectionView'; +import { CollectionView } from '../../collections/CollectionView'; import { TabDocView } from '../../collections/TabDocView'; import { ViewBoxBaseComponent } from '../../DocComponent'; import { Colors } from '../../global/globalEnums'; @@ -29,8 +29,6 @@ import { CollectionFreeFormDocumentView } from '../CollectionFreeFormDocumentVie import { FieldView, FieldViewProps } from '../FieldView'; import './PresBox.scss'; import { PresEffect, PresMovement, PresStatus } from './PresEnums'; -import { ScriptingGlobals } from '../../../util/ScriptingGlobals'; -import { PresElementBox } from '.'; export interface PinProps { audioRange?: boolean; @@ -155,7 +153,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { } constructor(props: any) { super(props); - if ((CurrentUserUtils.ActivePresentation = this.rootDoc)) runInAction(() => (PresBox.Instance = this)); + if ((Doc.ActivePresentation = this.rootDoc)) runInAction(() => (PresBox.Instance = this)); this.props.Document.presentationFieldKey = this.fieldKey; // provide info to the presElement script so that it can look up rendering information about the presBox } @computed get selectedDocumentView() { @@ -195,7 +193,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { this.layoutDoc._gridGap = 0; this.layoutDoc._yMargin = 0; this.turnOffEdit(true); - DocListCastAsync(CurrentUserUtils.MyTrails.data).then(pres => !pres?.includes(this.rootDoc) && Doc.AddDocToList(CurrentUserUtils.MyTrails, 'data', this.rootDoc)); + DocListCastAsync(Doc.MyTrails.data).then(pres => !pres?.includes(this.rootDoc) && Doc.AddDocToList(Doc.MyTrails, 'data', this.rootDoc)); this._disposers.selection = reaction( () => SelectionManager.Views(), views => views.some(view => view.props.Document === this.rootDoc) && this.updateCurrentPresentation() @@ -204,8 +202,8 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { @action updateCurrentPresentation = (pres?: Doc) => { - if (pres) CurrentUserUtils.ActivePresentation = pres; - else CurrentUserUtils.ActivePresentation = this.rootDoc; + if (pres) Doc.ActivePresentation = pres; + else Doc.ActivePresentation = this.rootDoc; document.removeEventListener('keydown', PresBox.keyEventsWrapper, true); document.addEventListener('keydown', PresBox.keyEventsWrapper, true); this._presKeyEventsActive = true; @@ -648,9 +646,9 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { */ @action updateMinimize = async () => { - if (DocListCast(CurrentUserUtils.MyOverlayDocs?.data).includes(this.layoutDoc)) { + if (DocListCast(Doc.MyOverlayDocs?.data).includes(this.layoutDoc)) { this.layoutDoc.presStatus = PresStatus.Edit; - Doc.RemoveDocFromList(CurrentUserUtils.MyOverlayDocs, undefined, this.rootDoc); + Doc.RemoveDocFromList(Doc.MyOverlayDocs, undefined, this.rootDoc); CollectionDockingView.AddSplit(this.rootDoc, 'right'); } else { this.layoutDoc.presStatus = PresStatus.Edit; @@ -660,7 +658,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { this.rootDoc.y = pt[1] + 10; this.rootDoc._height = 30; this.rootDoc._width = 248; - Doc.AddDocToList(CurrentUserUtils.MyOverlayDocs, undefined, this.rootDoc); + Doc.AddDocToList(Doc.MyOverlayDocs, undefined, this.rootDoc); this.props.removeDocument?.(this.layoutDoc); } }; @@ -768,7 +766,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { getTransform = () => this.props.ScreenToLocalTransform().translate(-5, -65); // listBox padding-left and pres-box-cont minHeight panelHeight = () => this.props.PanelHeight() - 40; isContentActive = (outsideReaction?: boolean) => - CurrentUserUtils.ActiveTool === InkTool.None && !this.layoutDoc._lockedPosition && (this.layoutDoc.forceActive || this.props.isSelected(outsideReaction) || this._isChildActive || this.props.renderDepth === 0) ? true : false; + Doc.ActiveTool === InkTool.None && !this.layoutDoc._lockedPosition && (this.layoutDoc.forceActive || this.props.isSelected(outsideReaction) || this._isChildActive || this.props.renderDepth === 0) ? true : false; /** * For sorting the array so that the order is maintained when it is dropped. @@ -918,7 +916,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { } break; case 'Escape': - if (DocListCast(CurrentUserUtils.MyOverlayDocs?.data).includes(this.layoutDoc)) { + if (DocListCast(Doc.MyOverlayDocs?.data).includes(this.layoutDoc)) { this.updateMinimize(); } else if (this.layoutDoc.presStatus === 'edit') { this._selectedArray.clear(); @@ -2714,19 +2712,19 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { @action toggleProperties = () => { - if (CurrentUserUtils.propertiesWidth > 0) { - CurrentUserUtils.propertiesWidth = 0; + if (SettingsManager.propertiesWidth > 0) { + SettingsManager.propertiesWidth = 0; } else { - CurrentUserUtils.propertiesWidth = 250; + SettingsManager.propertiesWidth = 250; } }; @computed get toolbar() { - const propIcon = CurrentUserUtils.propertiesWidth > 0 ? 'angle-double-right' : 'angle-double-left'; - const propTitle = CurrentUserUtils.propertiesWidth > 0 ? 'Close Presentation Panel' : 'Open Presentation Panel'; + const propIcon = SettingsManager.propertiesWidth > 0 ? 'angle-double-right' : 'angle-double-left'; + const propTitle = SettingsManager.propertiesWidth > 0 ? 'Close Presentation Panel' : 'Open Presentation Panel'; const mode = StrCast(this.rootDoc._viewType) as CollectionViewType; const isMini: boolean = this.toolbarWidth <= 100; - const presKeyEvents: boolean = this.isPres && this._presKeyEventsActive && this.rootDoc === CurrentUserUtils.ActivePresentation; + const presKeyEvents: boolean = this.isPres && this._presKeyEventsActive && this.rootDoc === Doc.ActivePresentation; const activeColor = Colors.LIGHT_BLUE; const inactiveColor = Colors.WHITE; return mode === CollectionViewType.Carousel3D ? null : ( @@ -2776,7 +2774,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { </> }> <div className="toolbar-button" style={{ position: 'absolute', right: 4, fontSize: 16 }} onClick={this.toggleProperties}> - <FontAwesomeIcon className={'toolbar-thumbtack'} icon={propIcon} style={{ color: CurrentUserUtils.propertiesWidth > 0 ? activeColor : inactiveColor }} /> + <FontAwesomeIcon className={'toolbar-thumbtack'} icon={propIcon} style={{ color: SettingsManager.propertiesWidth > 0 ? activeColor : inactiveColor }} /> </div> </Tooltip> </> @@ -3093,10 +3091,10 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { // needed to ensure that the childDocs are loaded for looking up fields this.childDocs.slice(); const mode = StrCast(this.rootDoc._viewType) as CollectionViewType; - const presKeyEvents: boolean = this.isPres && this._presKeyEventsActive && this.rootDoc === CurrentUserUtils.ActivePresentation; + const presKeyEvents: boolean = this.isPres && this._presKeyEventsActive && this.rootDoc === Doc.ActivePresentation; const presEnd: boolean = !this.layoutDoc.presLoop && this.itemIndex === this.childDocs.length - 1; const presStart: boolean = !this.layoutDoc.presLoop && this.itemIndex === 0; - return DocListCast(CurrentUserUtils.MyOverlayDocs?.data).includes(this.rootDoc) ? ( + return DocListCast(Doc.MyOverlayDocs?.data).includes(this.rootDoc) ? ( <div className="miniPres" onClick={e => e.stopPropagation()}> <div className="presPanelOverlay" style={{ display: 'inline-flex', height: 30, background: '#323232', top: 0, zIndex: 3000000, boxShadow: presKeyEvents ? '0 0 0px 3px ' + Colors.MEDIUM_BLUE : undefined }}> <Tooltip @@ -3151,7 +3149,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { </div> </div> ) : ( - <div className="presBox-cont" style={{ minWidth: DocListCast(CurrentUserUtils.MyOverlayDocs?.data).includes(this.layoutDoc) ? 240 : undefined }}> + <div className="presBox-cont" style={{ minWidth: DocListCast(Doc.MyOverlayDocs?.data).includes(this.layoutDoc) ? 240 : undefined }}> {this.topPanel} {this.toolbar} {this.newDocumentToolbarDropdown} diff --git a/src/client/views/nodes/trails/PresElementBox.tsx b/src/client/views/nodes/trails/PresElementBox.tsx index cc7bd5189..1a2054676 100644 --- a/src/client/views/nodes/trails/PresElementBox.tsx +++ b/src/client/views/nodes/trails/PresElementBox.tsx @@ -1,52 +1,66 @@ -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { Tooltip } from "@material-ui/core"; -import { action, computed, IReactionDisposer, observable, reaction } from "mobx"; -import { observer } from "mobx-react"; -import { Doc, DocListCast, HeightSym, Opt, WidthSym } from "../../../../fields/Doc"; -import { Id } from "../../../../fields/FieldSymbols"; -import { BoolCast, Cast, DocCast, NumCast, StrCast } from "../../../../fields/Types"; -import { emptyFunction, returnEmptyDoclist, returnFalse, returnTrue, setupMoveUpEvents } from "../../../../Utils"; -import { Docs, DocUtils } from "../../../documents/Documents"; -import { DocumentType } from "../../../documents/DocumentTypes"; -import { CurrentUserUtils } from "../../../util/CurrentUserUtils"; -import { DocumentManager } from "../../../util/DocumentManager"; -import { DragManager } from "../../../util/DragManager"; -import { Transform } from "../../../util/Transform"; -import { undoBatch } from "../../../util/UndoManager"; -import { CollectionViewType } from "../../collections/CollectionView"; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { Tooltip } from '@material-ui/core'; +import { action, computed, IReactionDisposer, observable, reaction } from 'mobx'; +import { observer } from 'mobx-react'; +import { Doc, DocListCast, HeightSym, Opt, WidthSym } from '../../../../fields/Doc'; +import { Id } from '../../../../fields/FieldSymbols'; +import { List } from '../../../../fields/List'; +import { Cast, DocCast, NumCast, StrCast } from '../../../../fields/Types'; +import { emptyFunction, returnEmptyDoclist, returnFalse, returnTrue, setupMoveUpEvents } from '../../../../Utils'; +import { Docs, DocUtils } from '../../../documents/Documents'; +import { CollectionViewType, DocumentType } from '../../../documents/DocumentTypes'; +import { DocumentManager } from '../../../util/DocumentManager'; +import { DragManager } from '../../../util/DragManager'; +import { SettingsManager } from '../../../util/SettingsManager'; +import { Transform } from '../../../util/Transform'; +import { undoBatch } from '../../../util/UndoManager'; import { ViewBoxBaseComponent } from '../../DocComponent'; -import { EditableView } from "../../EditableView"; -import { Colors } from "../../global/globalEnums"; -import { DocumentView, DocumentViewProps } from "../../nodes/DocumentView"; +import { EditableView } from '../../EditableView'; +import { Colors } from '../../global/globalEnums'; +import { DocumentView, DocumentViewProps } from '../../nodes/DocumentView'; import { FieldView, FieldViewProps } from '../../nodes/FieldView'; -import { StyleProp } from "../../StyleProvider"; -import { PresBox } from "./PresBox"; -import "./PresElementBox.scss"; -import { PresMovement } from "./PresEnums"; -import React = require("react"); -import { List } from "../../../../fields/List"; +import { StyleProp } from '../../StyleProvider'; +import { PresBox } from './PresBox'; +import './PresElementBox.scss'; +import { PresMovement } from './PresEnums'; +import React = require('react'); /** * This class models the view a document added to presentation will have in the presentation. * It involves some functionality for its buttons and options. */ @observer export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() { - public static LayoutString(fieldKey: string) { return FieldView.LayoutString(PresElementBox, fieldKey); } + public static LayoutString(fieldKey: string) { + return FieldView.LayoutString(PresElementBox, fieldKey); + } _heightDisposer: IReactionDisposer | undefined; @observable _dragging = false; // Idea: this boolean will determine whether to automatically show the video when this preselement is selected. // @observable static showVideo: boolean = false; - @computed get indexInPres() { return DocListCast(this.presBox[StrCast(this.presBox.presFieldKey, "data")]).indexOf(this.rootDoc); } // the index field is where this document is in the presBox display list (since this value is different for each presentation element, the value can't be stored on the layout template which is used by all display elements) - @computed get collapsedHeight() { return [CollectionViewType.Tree, CollectionViewType.Stacking].includes(this.presBox._viewType as any) ? 35 : 31; } // the collapsed height changes depending on the state of the presBox. We could store this on the presentation element template if it's used by only one presentation - but if it's shared by multiple, then this value must be looked up - @computed get presStatus() { return this.presBox.presStatus; } - @computed get presBox() { return (this.props.DocumentView?.().props.treeViewDoc ?? this.props.ContainingCollectionDoc)!; } - @computed get targetDoc() { return Cast(this.rootDoc.presentationTargetDoc, Doc, null) || this.rootDoc; } + @computed get indexInPres() { + return DocListCast(this.presBox[StrCast(this.presBox.presFieldKey, 'data')]).indexOf(this.rootDoc); + } // the index field is where this document is in the presBox display list (since this value is different for each presentation element, the value can't be stored on the layout template which is used by all display elements) + @computed get collapsedHeight() { + return [CollectionViewType.Tree, CollectionViewType.Stacking].includes(this.presBox._viewType as any) ? 35 : 31; + } // the collapsed height changes depending on the state of the presBox. We could store this on the presentation element template if it's used by only one presentation - but if it's shared by multiple, then this value must be looked up + @computed get presStatus() { + return this.presBox.presStatus; + } + @computed get presBox() { + return (this.props.DocumentView?.().props.treeViewDoc ?? this.props.ContainingCollectionDoc)!; + } + @computed get targetDoc() { + return Cast(this.rootDoc.presentationTargetDoc, Doc, null) || this.rootDoc; + } componentDidMount() { this.layoutDoc.hideLinkButton = true; - this._heightDisposer = reaction(() => [this.rootDoc.presExpandInlineButton, this.collapsedHeight], - params => this.layoutDoc._height = NumCast(params[1]) + (Number(params[0]) ? 100 : 0), { fireImmediately: true }); + this._heightDisposer = reaction( + () => [this.rootDoc.presExpandInlineButton, this.collapsedHeight], + params => (this.layoutDoc._height = NumCast(params[1]) + (Number(params[0]) ? 100 : 0)), + { fireImmediately: true } + ); } componentWillUnmount() { this._heightDisposer?.(); @@ -60,26 +74,26 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() { @action presExpandDocumentClick = () => { this.rootDoc.presExpandInlineButton = !this.rootDoc.presExpandInlineButton; - } + }; embedHeight = (): number => 97; // embedWidth = () => this.props.PanelWidth(); // embedHeight = () => Math.min(this.props.PanelWidth() - 20, this.props.PanelHeight() - this.collapsedHeight); embedWidth = (): number => this.props.PanelWidth() - 35; - styleProvider = (doc: (Doc | undefined), props: Opt<DocumentViewProps>, property: string): any => { + styleProvider = (doc: Doc | undefined, props: Opt<DocumentViewProps>, property: string): any => { if (property === StyleProp.Opacity) return 1; return this.props.styleProvider?.(doc, props, property); - } + }; /** * The function that is responsible for rendering a preview or not for this * presentation element. */ @computed get renderEmbeddedInline() { - return !this.rootDoc.presExpandInlineButton || !this.targetDoc ? (null) : + return !this.rootDoc.presExpandInlineButton || !this.targetDoc ? null : ( <div className="presItem-embedded" style={{ height: this.embedHeight(), width: this.embedWidth() }}> <DocumentView Document={this.rootDoc} - DataDoc={undefined}//this.targetDoc[DataSym] !== this.targetDoc && this.targetDoc[DataSym]} + DataDoc={undefined} //this.targetDoc[DataSym] !== this.targetDoc && this.targetDoc[DataSym]} styleProvider={this.styleProvider} docViewPath={returnEmptyDoclist} rootSelected={returnTrue} @@ -105,22 +119,23 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() { hideLinkButton={true} /> <div className="presItem-embeddedMask" /> - </div>; + </div> + ); } @computed get renderGroupSlides() { const childDocs = DocListCast(this.targetDoc.data); - const groupSlides = childDocs.map((doc: Doc, ind: number) => - <div className="presItem-groupSlide" onClick={(e) => { - console.log("Clicked on slide with index: ", ind); - e.stopPropagation(); - e.preventDefault(); - PresBox.Instance.modifierSelect(doc, this._itemRef.current!, this._dragRef.current!, !e.shiftKey && !e.ctrlKey && !e.metaKey, e.ctrlKey || e.metaKey, e.shiftKey); - this.presExpandDocumentClick(); - }}> - <div className="presItem-groupNum"> - {`${ind + 1}.`} - </div> + const groupSlides = childDocs.map((doc: Doc, ind: number) => ( + <div + className="presItem-groupSlide" + onClick={e => { + console.log('Clicked on slide with index: ', ind); + e.stopPropagation(); + e.preventDefault(); + PresBox.Instance.modifierSelect(doc, this._itemRef.current!, this._dragRef.current!, !e.shiftKey && !e.ctrlKey && !e.metaKey, e.ctrlKey || e.metaKey, e.shiftKey); + this.presExpandDocumentClick(); + }}> + <div className="presItem-groupNum">{`${ind + 1}.`}</div> {/* style={{ maxWidth: showMore ? (toolbarWidth - 195) : toolbarWidth - 105, cursor: isSelected ? 'text' : 'grab' }} */} <div className="presItem-name"> <EditableView @@ -130,37 +145,36 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() { overflow={'ellipsis'} GetValue={() => StrCast(doc.title)} SetValue={(value: string) => { - doc.title = !value.trim().length ? "-untitled-" : value; + doc.title = !value.trim().length ? '-untitled-' : value; return true; }} /> </div> </div> - ); - return ( - groupSlides - ); + )); + return groupSlides; } @computed get duration() { let durationInS: number; - if (this.rootDoc.type === DocumentType.AUDIO || this.rootDoc.type === DocumentType.VID) { durationInS = NumCast(this.rootDoc.presEndTime) - NumCast(this.rootDoc.presStartTime); durationInS = Math.round(durationInS * 10) / 10; } - else if (this.rootDoc.presDuration) durationInS = NumCast(this.rootDoc.presDuration) / 1000; + if (this.rootDoc.type === DocumentType.AUDIO || this.rootDoc.type === DocumentType.VID) { + durationInS = NumCast(this.rootDoc.presEndTime) - NumCast(this.rootDoc.presStartTime); + durationInS = Math.round(durationInS * 10) / 10; + } else if (this.rootDoc.presDuration) durationInS = NumCast(this.rootDoc.presDuration) / 1000; else durationInS = 2; - return "D: " + durationInS + "s"; + return 'D: ' + durationInS + 's'; } @computed get transition() { let transitionInS: number; if (this.rootDoc.presTransition) transitionInS = NumCast(this.rootDoc.presTransition) / 1000; else transitionInS = 0.5; - return this.rootDoc.presMovement === PresMovement.Jump || this.rootDoc.presMovement === PresMovement.None ? (null) : "M: " + transitionInS + "s"; + return this.rootDoc.presMovement === PresMovement.Jump || this.rootDoc.presMovement === PresMovement.None ? null : 'M: ' + transitionInS + 's'; } private _itemRef: React.RefObject<HTMLDivElement> = React.createRef(); private _dragRef: React.RefObject<HTMLDivElement> = React.createRef(); private _titleRef: React.RefObject<EditableView> = React.createRef(); - @action headerDown = (e: React.PointerEvent<HTMLDivElement>) => { const element = e.target as any; @@ -171,18 +185,24 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() { PresBox.Instance._selectedArray.size === 1 && PresBox.Instance.regularSelect(this.rootDoc, this._itemRef.current!, this._dragRef.current!, false, false); setupMoveUpEvents(this, e, this.startDrag, emptyFunction, emptyFunction); } else { - setupMoveUpEvents(this, e, ((e: PointerEvent) => { - PresBox.Instance.regularSelect(this.rootDoc, this._itemRef.current!, this._dragRef.current!, false, false); - return this.startDrag(e); - }), emptyFunction, emptyFunction); + setupMoveUpEvents( + this, + e, + (e: PointerEvent) => { + PresBox.Instance.regularSelect(this.rootDoc, this._itemRef.current!, this._dragRef.current!, false, false); + return this.startDrag(e); + }, + emptyFunction, + emptyFunction + ); } } - } + }; headerUp = (e: React.PointerEvent<HTMLDivElement>) => { e.stopPropagation(); e.preventDefault(); - } + }; /** * Function to drag and drop the pres element to a diferent location @@ -193,37 +213,43 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() { const dragArray = PresBox.Instance._dragArray; const dragData = new DragManager.DocumentDragData(PresBox.Instance.sortArray()); if (!dragData.draggedDocuments.length) dragData.draggedDocuments.push(this.rootDoc); - dragData.dropAction = "move"; + dragData.dropAction = 'move'; dragData.treeViewDoc = this.props.docViewPath().lastElement()?.props.treeViewDoc; dragData.moveDocument = this.props.docViewPath().lastElement()?.props.moveDocument; const dragItem: HTMLElement[] = []; if (dragArray.length === 1) { const doc = this._itemRef.current || dragArray[0]; - doc.className = miniView ? "presItem-miniSlide" : "presItem-slide"; + doc.className = miniView ? 'presItem-miniSlide' : 'presItem-slide'; dragItem.push(doc); } else if (dragArray.length >= 1) { const doc = document.createElement('div'); - doc.className = "presItem-multiDrag"; - doc.innerText = "Move " + PresBox.Instance._selectedArray.size + " slides"; + doc.className = 'presItem-multiDrag'; + doc.innerText = 'Move ' + PresBox.Instance._selectedArray.size + ' slides'; doc.style.position = 'absolute'; - doc.style.top = (e.clientY) + 'px'; - doc.style.left = (e.clientX - 50) + 'px'; + doc.style.top = e.clientY + 'px'; + doc.style.left = e.clientX - 50 + 'px'; dragItem.push(doc); } // const dropEvent = () => runInAction(() => this._dragging = false); if (activeItem) { - DragManager.StartDocumentDrag(dragItem.map(ele => ele), dragData, e.clientX, e.clientY, undefined); + DragManager.StartDocumentDrag( + dragItem.map(ele => ele), + dragData, + e.clientX, + e.clientY, + undefined + ); // runInAction(() => this._dragging = true); return true; } return false; - } + }; onPointerOver = (e: any) => { - document.removeEventListener("pointermove", this.onPointerMove); - document.addEventListener("pointermove", this.onPointerMove); - } + document.removeEventListener('pointermove', this.onPointerMove); + document.addEventListener('pointermove', this.onPointerMove); + }; onPointerMove = (e: PointerEvent) => { const slide = this._itemRef.current!; @@ -233,32 +259,32 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() { } if (slide && dragIsPresItem) { const rect = slide.getBoundingClientRect(); - const y = e.clientY - rect.top; //y position within the element. + const y = e.clientY - rect.top; //y position within the element. const height = slide.clientHeight; const halfLine = height / 2; if (y <= halfLine) { slide.style.borderTop = `solid 2px ${Colors.MEDIUM_BLUE}`; - slide.style.borderBottom = "0px"; + slide.style.borderBottom = '0px'; } else if (y > halfLine) { - slide.style.borderTop = "0px"; + slide.style.borderTop = '0px'; slide.style.borderBottom = `solid 2px ${Colors.MEDIUM_BLUE}`; } } - document.removeEventListener("pointermove", this.onPointerMove); - } + document.removeEventListener('pointermove', this.onPointerMove); + }; onPointerLeave = (e: any) => { - this._itemRef.current!.style.borderTop = "0px"; - this._itemRef.current!.style.borderBottom = "0px"; - document.removeEventListener("pointermove", this.onPointerMove); - } + this._itemRef.current!.style.borderTop = '0px'; + this._itemRef.current!.style.borderBottom = '0px'; + document.removeEventListener('pointermove', this.onPointerMove); + }; @action toggleProperties = () => { - if (CurrentUserUtils.propertiesWidth < 5) { - action(() => (CurrentUserUtils.propertiesWidth = 250)); + if (SettingsManager.propertiesWidth < 5) { + action(() => (SettingsManager.propertiesWidth = 250)); } - } + }; @undoBatch removeItem = action((e: React.MouseEvent) => { @@ -267,32 +293,35 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() { if (PresBox.Instance._selectedArray.has(this.rootDoc)) { PresBox.Instance._selectedArray.delete(this.rootDoc); } - this.removeAllRecordingInOverlay() + this.removeAllRecordingInOverlay(); }); // set the value/title of the individual pres element @undoBatch @action onSetValue = (value: string) => { - this.rootDoc.title = !value.trim().length ? "-untitled-" : value; + this.rootDoc.title = !value.trim().length ? '-untitled-' : value; return true; - } + }; /** * Method called for updating the view of the currently selected document - * - * @param targetDoc - * @param activeItem + * + * @param targetDoc + * @param activeItem */ @undoBatch @action updateView = (targetDoc: Doc, activeItem: Doc) => { switch (targetDoc.type) { - case DocumentType.PDF: case DocumentType.WEB: case DocumentType.RTF: + case DocumentType.PDF: + case DocumentType.WEB: + case DocumentType.RTF: const scroll = targetDoc._scrollTop; activeItem.presPinViewScroll = scroll; break; - case DocumentType.VID: case DocumentType.AUDIO: + case DocumentType.VID: + case DocumentType.AUDIO: activeItem.presStartTime = targetDoc._currentTimecode; break; case DocumentType.COMPARISON: @@ -307,53 +336,53 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() { activeItem.presPinViewY = y; activeItem.presPinViewScale = scale; } - } + }; @computed get recordingIsInOverlay() { - let isInOverlay = false - DocListCast(CurrentUserUtils.MyOverlayDocs.data).forEach((doc) => { + let isInOverlay = false; + DocListCast(Doc.MyOverlayDocs.data).forEach(doc => { if (doc.slides === this.rootDoc) { - isInOverlay = true + isInOverlay = true; // return } - }) - return isInOverlay + }); + return isInOverlay; } // a previously recorded video will have timecode defined static videoIsRecorded = (activeItem: Doc) => { const casted = Cast(activeItem.recording, Doc, null); return casted && 'currentTimecode' in casted; - } + }; removeAllRecordingInOverlay = () => { - DocListCast(CurrentUserUtils.MyOverlayDocs.data).forEach((doc) => { + DocListCast(Doc.MyOverlayDocs.data).forEach(doc => { if (doc.slides === this.rootDoc) { - Doc.RemoveDocFromList(CurrentUserUtils.MyOverlayDocs, undefined, doc); + Doc.RemoveDocFromList(Doc.MyOverlayDocs, undefined, doc); } - }) - } + }); + }; static removeEveryExistingRecordingInOverlay = () => { // Remove every recording that already exists in overlay view - DocListCast(CurrentUserUtils.MyOverlayDocs.data).forEach((doc) => { + DocListCast(Doc.MyOverlayDocs.data).forEach(doc => { // if it's a recording video, don't remove from overlay (user can lose data) - if (!PresElementBox.videoIsRecorded(DocCast(doc.slides))) return + if (!PresElementBox.videoIsRecorded(DocCast(doc.slides))) return; if (doc.slides !== null) { - Doc.RemoveDocFromList(CurrentUserUtils.MyOverlayDocs, undefined, doc); + Doc.RemoveDocFromList(Doc.MyOverlayDocs, undefined, doc); } - }) - } + }); + }; @undoBatch @action hideRecording = (e: React.MouseEvent, iconClick: boolean = false) => { - e.stopPropagation() - this.removeAllRecordingInOverlay() + e.stopPropagation(); + this.removeAllRecordingInOverlay(); // if (iconClick) PresElementBox.showVideo = false; - } + }; @undoBatch @action @@ -364,47 +393,46 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() { // remove the overlays on switch *IF* not opened from the specific icon if (!iconClick) PresElementBox.removeEveryExistingRecordingInOverlay(); - if (activeItem.recording) { - Doc.AddDocToList(CurrentUserUtils.MyOverlayDocs, undefined, Cast(activeItem.recording, Doc, null)); + Doc.AddDocToList(Doc.MyOverlayDocs, undefined, Cast(activeItem.recording, Doc, null)); } - } + }; @undoBatch @action startRecording = (activeItem: Doc) => { - console.log('start recording', 'activeItem', activeItem) + console.log('start recording', 'activeItem', activeItem); if (PresElementBox.videoIsRecorded(activeItem)) { // if we already have an existing recording this.showRecording(activeItem, true); // // if we already have an existing recording - // Doc.AddDocToList(CurrentUserUtils.MyOverlayDocs, undefined, Cast(activeItem.recording, Doc, null)); - + // Doc.AddDocToList(Doc.MyOverlayDocs, undefined, Cast(activeItem.recording, Doc, null)); } else { // Remove every recording that already exists in overlay view // this is a design decision to clear to focus in on the recoding mode PresElementBox.removeEveryExistingRecordingInOverlay(); // if we dont have any recording - const recording = Docs.Create.WebCamDocument("", { - _width: 384, _height: 216, + const recording = Docs.Create.WebCamDocument('', { + _width: 384, + _height: 216, hideDocumentButtonBar: true, hideDecorationTitle: true, hideOpenButton: true, // hideDeleteButton: true, - cloneFieldFilter: new List<string>(["system"]) + cloneFieldFilter: new List<string>(['system']), }); // attach the recording to the slide, and attach the slide to the recording - recording.slides = activeItem - activeItem.recording = recording + recording.slides = activeItem; + activeItem.recording = recording; // make recording box appear in the bottom right corner of the screen recording.x = window.innerWidth - recording[WidthSym]() - 20; recording.y = window.innerHeight - recording[HeightSym]() - 20; - Doc.AddDocToList(CurrentUserUtils.MyOverlayDocs, undefined, recording); + Doc.AddDocToList(Doc.MyOverlayDocs, undefined, recording); } - } + }; @computed get toolbarWidth(): number { @@ -422,17 +450,18 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() { const miniView: boolean = this.toolbarWidth <= 110; const presBox: Doc = this.presBox; //presBox const presBoxColor: string = StrCast(presBox._backgroundColor); - const presColorBool: boolean = presBoxColor ? (presBoxColor !== Colors.WHITE && presBoxColor !== "transparent") : false; + const presColorBool: boolean = presBoxColor ? presBoxColor !== Colors.WHITE && presBoxColor !== 'transparent' : false; const targetDoc: Doc = this.targetDoc; const activeItem: Doc = this.rootDoc; return ( - <div className={`presItem-container`} + <div + className={`presItem-container`} key={this.props.Document[Id] + this.indexInPres} ref={this._itemRef} style={{ - backgroundColor: presColorBool ? isSelected ? "rgba(250,250,250,0.3)" : "transparent" : isSelected ? Colors.LIGHT_BLUE : "transparent", - opacity: this._dragging ? 0.3 : 1 + backgroundColor: presColorBool ? (isSelected ? 'rgba(250,250,250,0.3)' : 'transparent') : isSelected ? Colors.LIGHT_BLUE : 'transparent', + opacity: this._dragging ? 0.3 : 1, }} onClick={e => { e.stopPropagation(); @@ -447,8 +476,7 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() { onPointerOver={this.onPointerOver} onPointerLeave={this.onPointerLeave} onPointerDown={this.headerDown} - onPointerUp={this.headerUp} - > + onPointerUp={this.headerUp}> {/* {miniView ? // when width is LESS than 110 px <div className={`presItem-miniSlide ${isSelected ? "active" : ""}`} ref={miniView ? this._dragRef : null}> @@ -462,50 +490,63 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() { {/* <div className="presItem-number"> {`${this.indexInPres + 1}.`} </div> */} - {miniView ? (null) : <div ref={miniView ? null : this._dragRef} className={`presItem-slide ${isSelected ? "active" : ""}`} - style={{ - backgroundColor: this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.BackgroundColor), - boxShadow: presBoxColor && presBoxColor !== "white" && presBoxColor !== "transparent" ? isSelected ? "0 0 0px 1.5px" + presBoxColor : undefined : undefined - }}> - <div className="presItem-name" style={{ maxWidth: showMore ? (toolbarWidth - 195) : toolbarWidth - 105, cursor: isSelected ? 'text' : 'grab' }}> - <div>{`${this.indexInPres + 1}. `}</div> - <EditableView - ref={this._titleRef} - editing={!isSelected ? false : undefined} - contents={activeItem.title} - overflow={'ellipsis'} - GetValue={() => StrCast(activeItem.title)} - SetValue={this.onSetValue} - /> - </div> - {/* <Tooltip title={<><div className="dash-tooltip">{"Movement speed"}</div></>}><div className="presItem-time" style={{ display: showMore ? "block" : "none" }}>{this.transition}</div></Tooltip> */} - {/* <Tooltip title={<><div className="dash-tooltip">{"Duration"}</div></>}><div className="presItem-time" style={{ display: showMore ? "block" : "none" }}>{this.duration}</div></Tooltip> */} - <div className={"presItem-slideButtons"}> - <Tooltip title={<><div className="dash-tooltip">{"Update view"}</div></>}> - <div className="slideButton" - onClick={() => this.updateView(targetDoc, activeItem)} - style={{ fontWeight: 700, display: activeItem.presPinView ? "flex" : "none" }}>V</div> - </Tooltip> - - {this.recordingIsInOverlay ? - <Tooltip title={<><div className="dash-tooltip">{"Hide Recording"}</div></>}> - <div className="slideButton" - onClick={(e) => this.hideRecording(e, true)} - style={{ fontWeight: 700 }}> - <FontAwesomeIcon icon={"video-slash"} onPointerDown={e => e.stopPropagation()} /> - </div> - </Tooltip> : - <Tooltip title={<><div className="dash-tooltip">{`${PresElementBox.videoIsRecorded(activeItem) ? "Show" : "Start"} recording`}</div></>}> - <div className="slideButton" - onClick={(e) => { e.stopPropagation(); this.startRecording(activeItem); }} - style={{ fontWeight: 700 }}> - <FontAwesomeIcon icon={"video"} onPointerDown={e => e.stopPropagation()} /> + {miniView ? null : ( + <div + ref={miniView ? null : this._dragRef} + className={`presItem-slide ${isSelected ? 'active' : ''}`} + style={{ + backgroundColor: this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.BackgroundColor), + boxShadow: presBoxColor && presBoxColor !== 'white' && presBoxColor !== 'transparent' ? (isSelected ? '0 0 0px 1.5px' + presBoxColor : undefined) : undefined, + }}> + <div className="presItem-name" style={{ maxWidth: showMore ? toolbarWidth - 195 : toolbarWidth - 105, cursor: isSelected ? 'text' : 'grab' }}> + <div>{`${this.indexInPres + 1}. `}</div> + <EditableView ref={this._titleRef} editing={!isSelected ? false : undefined} contents={activeItem.title} overflow={'ellipsis'} GetValue={() => StrCast(activeItem.title)} SetValue={this.onSetValue} /> + </div> + {/* <Tooltip title={<><div className="dash-tooltip">{"Movement speed"}</div></>}><div className="presItem-time" style={{ display: showMore ? "block" : "none" }}>{this.transition}</div></Tooltip> */} + {/* <Tooltip title={<><div className="dash-tooltip">{"Duration"}</div></>}><div className="presItem-time" style={{ display: showMore ? "block" : "none" }}>{this.duration}</div></Tooltip> */} + <div className={'presItem-slideButtons'}> + <Tooltip + title={ + <> + <div className="dash-tooltip">{'Update view'}</div> + </> + }> + <div className="slideButton" onClick={() => this.updateView(targetDoc, activeItem)} style={{ fontWeight: 700, display: activeItem.presPinView ? 'flex' : 'none' }}> + V </div> </Tooltip> - } - - {/* {this.indexInPres === 0 ? (null) : <Tooltip title={<><div className="dash-tooltip">{activeItem.groupWithUp ? "Ungroup" : "Group with up"}</div></>}> + {this.recordingIsInOverlay ? ( + <Tooltip + title={ + <> + <div className="dash-tooltip">{'Hide Recording'}</div> + </> + }> + <div className="slideButton" onClick={e => this.hideRecording(e, true)} style={{ fontWeight: 700 }}> + <FontAwesomeIcon icon={'video-slash'} onPointerDown={e => e.stopPropagation()} /> + </div> + </Tooltip> + ) : ( + <Tooltip + title={ + <> + <div className="dash-tooltip">{`${PresElementBox.videoIsRecorded(activeItem) ? 'Show' : 'Start'} recording`}</div> + </> + }> + <div + className="slideButton" + onClick={e => { + e.stopPropagation(); + this.startRecording(activeItem); + }} + style={{ fontWeight: 700 }}> + <FontAwesomeIcon icon={'video'} onPointerDown={e => e.stopPropagation()} /> + </div> + </Tooltip> + )} + + {/* {this.indexInPres === 0 ? (null) : <Tooltip title={<><div className="dash-tooltip">{activeItem.groupWithUp ? "Ungroup" : "Group with up"}</div></>}> <div className="slideButton" onClick={() => activeItem.groupWithUp = !activeItem.groupWithUp} style={{ @@ -520,22 +561,41 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() { </div> </div> </Tooltip>} */} - <Tooltip title={<><div className="dash-tooltip">{this.rootDoc.presExpandInlineButton ? "Minimize" : "Expand"}</div></>}><div className={"slideButton"} onClick={e => { e.stopPropagation(); this.presExpandDocumentClick(); }}> - <FontAwesomeIcon icon={this.rootDoc.presExpandInlineButton ? "eye-slash" : "eye"} onPointerDown={e => e.stopPropagation()} /> - </div></Tooltip> - <Tooltip title={<><div className="dash-tooltip">{"Remove from presentation"}</div></>}><div - className={"slideButton"} - onClick={this.removeItem}> - <FontAwesomeIcon icon={"trash"} onPointerDown={e => e.stopPropagation()} /> - </div></Tooltip> + <Tooltip + title={ + <> + <div className="dash-tooltip">{this.rootDoc.presExpandInlineButton ? 'Minimize' : 'Expand'}</div> + </> + }> + <div + className={'slideButton'} + onClick={e => { + e.stopPropagation(); + this.presExpandDocumentClick(); + }}> + <FontAwesomeIcon icon={this.rootDoc.presExpandInlineButton ? 'eye-slash' : 'eye'} onPointerDown={e => e.stopPropagation()} /> + </div> + </Tooltip> + <Tooltip + title={ + <> + <div className="dash-tooltip">{'Remove from presentation'}</div> + </> + }> + <div className={'slideButton'} onClick={this.removeItem}> + <FontAwesomeIcon icon={'trash'} onPointerDown={e => e.stopPropagation()} /> + </div> + </Tooltip> + </div> + {/* <div className="presItem-docName" style={{ maxWidth: showMore ? (toolbarWidth - 195) : toolbarWidth - 105 }}>{activeItem.presPinView ? (<><i>View of </i> {targetDoc.title}</>) : targetDoc.title}</div> */} + {this.renderEmbeddedInline} </div> - {/* <div className="presItem-docName" style={{ maxWidth: showMore ? (toolbarWidth - 195) : toolbarWidth - 105 }}>{activeItem.presPinView ? (<><i>View of </i> {targetDoc.title}</>) : targetDoc.title}</div> */} - {this.renderEmbeddedInline} - </div>} - </div >); + )} + </div> + ); } render() { - return !(this.rootDoc instanceof Doc) || this.targetDoc instanceof Promise ? (null) : this.mainItem; + return !(this.rootDoc instanceof Doc) || this.targetDoc instanceof Promise ? null : this.mainItem; } -}
\ No newline at end of file +} |
