diff options
Diffstat (limited to 'src/client/views/nodes')
23 files changed, 719 insertions, 912 deletions
diff --git a/src/client/views/nodes/AudioBox.scss b/src/client/views/nodes/AudioBox.scss index fc881ca25..3fcb024df 100644 --- a/src/client/views/nodes/AudioBox.scss +++ b/src/client/views/nodes/AudioBox.scss @@ -144,24 +144,6 @@ border-radius: 3px; z-index: 1000; overflow: hidden; - - .waveform { - position: relative; - width: 100%; - height: 100%; - overflow: hidden; - z-index: -1000; - bottom: 0; - pointer-events: none; - div { - height: 100% !important; - width: 100% !important; - } - canvas { - height: 100% !important; - width: 100% !important; - } - } } .audioBox-total-time, diff --git a/src/client/views/nodes/AudioBox.tsx b/src/client/views/nodes/AudioBox.tsx index ecc93ce67..06a27c22a 100644 --- a/src/client/views/nodes/AudioBox.tsx +++ b/src/client/views/nodes/AudioBox.tsx @@ -1,18 +1,15 @@ import React = require("react"); import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import axios from "axios"; import { action, computed, IReactionDisposer, observable, reaction, runInAction } from "mobx"; import { observer } from "mobx-react"; -import Waveform from "react-audio-waveform"; import { DateField } from "../../../fields/DateField"; import { Doc, DocListCast, Opt } from "../../../fields/Doc"; import { documentSchema } from "../../../fields/documentSchemas"; -import { List } from "../../../fields/List"; -import { createSchema, listSpec, makeInterface } from "../../../fields/Schema"; +import { makeInterface } from "../../../fields/Schema"; import { ComputedField } from "../../../fields/ScriptField"; import { Cast, NumCast } from "../../../fields/Types"; import { AudioField, nullAudio } from "../../../fields/URLField"; -import { emptyFunction, formatTime, numberRange, Utils } from "../../../Utils"; +import { emptyFunction, formatTime, Utils } from "../../../Utils"; import { DocUtils } from "../../documents/Documents"; import { Networking } from "../../Network"; import { CurrentUserUtils } from "../../util/CurrentUserUtils"; @@ -23,7 +20,6 @@ import { ContextMenuProps } from "../ContextMenuItem"; import { ViewBoxAnnotatableComponent } from "../DocComponent"; import "./AudioBox.scss"; import { FieldView, FieldViewProps } from './FieldView'; -import { FormattedTextBoxComment } from "./formattedText/FormattedTextBoxComment"; import { LinkDocPreview } from "./LinkDocPreview"; declare class MediaRecorder { constructor(e: any); // whatever MediaRecorder has @@ -36,7 +32,6 @@ const AudioDocument = makeInterface(documentSchema); export class AudioBox extends ViewBoxAnnotatableComponent<FieldViewProps, AudioDocument>(AudioDocument) { public static LayoutString(fieldKey: string) { return FieldView.LayoutString(AudioBox, fieldKey); } public static Enabled = false; - public static NUMBER_OF_BUCKETS = 100; static playheadWidth = 30; // width of playhead static heightPercent = 80; // height of timeline in percent of height of audioBox. static Instance: AudioBox; @@ -58,8 +53,8 @@ export class AudioBox extends ViewBoxAnnotatableComponent<FieldViewProps, AudioD @observable _position: number = 0; @observable _waveHeight: Opt<number> = this.layoutDoc._height; @observable _paused: boolean = false; - @computed get audioState(): undefined | "recording" | "paused" | "playing" { return this.dataDoc.audioState as (undefined | "recording" | "paused" | "playing"); } - set audioState(value) { this.dataDoc.audioState = value; } + @computed get mediaState(): undefined | "recording" | "paused" | "playing" { return this.dataDoc.mediaState as (undefined | "recording" | "paused" | "playing"); } + set mediaState(value) { this.dataDoc.mediaState = value; } public static SetScrubTime = action((timeInMillisFrom1970: number) => { AudioBox._scrubTime = 0; AudioBox._scrubTime = timeInMillisFrom1970; }); @computed get recordingStart() { return Cast(this.dataDoc[this.props.fieldKey + "-recordingStart"], DateField)?.date.getTime(); } @computed get duration() { return NumCast(this.dataDoc[`${this.fieldKey}-duration`]); } @@ -89,7 +84,10 @@ export class AudioBox extends ViewBoxAnnotatableComponent<FieldViewProps, AudioD } 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.audioState === "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 === "recording" ? (Date.now() - (this.recordingStart || 0)) / 1000 : undefined)) + || this.rootDoc; } componentWillUnmount() { @@ -102,9 +100,7 @@ export class AudioBox extends ViewBoxAnnotatableComponent<FieldViewProps, AudioD componentDidMount() { this.props.setContentView?.(this); // this tells the DocumentView that this AudioBox is the "content" of the document. this allows the DocumentView to indirectly call getAnchor() on the AudioBox when making a link. - this.audioState = this.path ? "paused" : undefined; - - //this._disposers.scrubbing = reaction(() => AudioBox._scrubTime, (time) => this.layoutDoc.playOnSelect && this.playFromTime(AudioBox._scrubTime)); + this.mediaState = this.path ? "paused" : undefined; this._disposers.triggerAudio = reaction( () => !LinkDocPreview.LinkInfo && this.props.renderDepth !== -1 ? NumCast(this.Document._triggerAudio, null) : undefined, @@ -131,7 +127,7 @@ export class AudioBox extends ViewBoxAnnotatableComponent<FieldViewProps, AudioD // for updating the timecode timecodeChanged = () => { const htmlEle = this._ele; - if (this.audioState !== "recording" && htmlEle) { + if (this.mediaState !== "recording" && htmlEle) { htmlEle.duration && htmlEle.duration !== Infinity && runInAction(() => this.dataDoc[this.fieldKey + "-duration"] = htmlEle.duration); this.links.map(l => this.getLinkData(l)).forEach(({ la1, la2, linkTime }) => { if (linkTime > NumCast(this.layoutDoc._currentTimecode) && linkTime < htmlEle.currentTime) { @@ -145,7 +141,7 @@ export class AudioBox extends ViewBoxAnnotatableComponent<FieldViewProps, AudioD // pause play back Pause = action(() => { this._ele!.pause(); - this.audioState = "paused"; + this.mediaState = "paused"; }); // play audio for documents created during recording @@ -169,7 +165,7 @@ export class AudioBox extends ViewBoxAnnotatableComponent<FieldViewProps, AudioD } else if (seekTimeInSeconds <= this._ele.duration) { this._ele.currentTime = seekTimeInSeconds; this._ele.play(); - runInAction(() => this.audioState = "playing"); + runInAction(() => this.mediaState = "playing"); if (endTime !== this.duration) { this._play = setTimeout(() => this.Pause(), (endTime - seekTimeInSeconds) * 1000); // use setTimeout to play a specific duration } @@ -181,7 +177,7 @@ export class AudioBox extends ViewBoxAnnotatableComponent<FieldViewProps, AudioD // update the recording time updateRecordTime = () => { - if (this.audioState === "recording") { + if (this.mediaState === "recording") { setTimeout(this.updateRecordTime, 30); if (this._paused) { this._pausedTime += (new Date().getTime() - this._recordStart) / 1000; @@ -204,7 +200,7 @@ export class AudioBox extends ViewBoxAnnotatableComponent<FieldViewProps, AudioD } }; this._recordStart = new Date().getTime(); - runInAction(() => this.audioState = "recording"); + runInAction(() => this.mediaState = "recording"); setTimeout(this.updateRecordTime, 0); this._recorder.start(); setTimeout(() => this._recorder && this.stopRecording(), 60 * 60 * 1000); // stop after an hour @@ -214,8 +210,8 @@ export class AudioBox extends ViewBoxAnnotatableComponent<FieldViewProps, AudioD specificContextMenu = (e: React.MouseEvent): void => { const funcs: ContextMenuProps[] = []; funcs.push({ description: (this.layoutDoc.hideAnchors ? "Don't hide" : "Hide") + " anchors", event: () => this.layoutDoc.hideAnchors = !this.layoutDoc.hideAnchors, icon: "expand-arrows-alt" }); - funcs.push({ description: (this.layoutDoc.playOnSelect ? "Don't play" : "Play") + " when link is selected", event: () => this.layoutDoc.playOnSelect = !this.layoutDoc.playOnSelect, icon: "expand-arrows-alt" }); - funcs.push({ description: (this.layoutDoc.autoPlay ? "Don't auto play" : "Auto play") + " anchors onClick", event: () => this.layoutDoc.autoPlay = !this.layoutDoc.autoPlay, icon: "expand-arrows-alt" }); + funcs.push({ description: (this.layoutDoc.dontAutoPlayFollowedLinks ? "" : "Don't") + " play when link is selected", event: () => this.layoutDoc.dontAutoPlayFollowedLinks = !this.layoutDoc.dontAutoPlayFollowedLinks, icon: "expand-arrows-alt" }); + funcs.push({ description: (this.layoutDoc.autoPlayAnchors ? "Don't auto play" : "Auto play") + " anchors onClick", event: () => this.layoutDoc.autoPlayAnchors = !this.layoutDoc.autoPlayAnchors, icon: "expand-arrows-alt" }); ContextMenu.Instance?.addItem({ description: "Options...", subitems: funcs, icon: "asterisk" }); } @@ -224,7 +220,7 @@ export class AudioBox extends ViewBoxAnnotatableComponent<FieldViewProps, AudioD this._recorder.stop(); this._recorder = undefined; this.dataDoc[this.fieldKey + "-duration"] = (new Date().getTime() - this._recordStart - this.pauseTime) / 1000; - this.audioState = "paused"; + this.mediaState = "paused"; this._stream?.getAudioTracks()[0].stop(); const ind = DocUtils.ActiveRecordings.indexOf(this); ind !== -1 && (DocUtils.ActiveRecordings.splice(ind, 1)); @@ -250,7 +246,7 @@ export class AudioBox extends ViewBoxAnnotatableComponent<FieldViewProps, AudioD NumCast(this.props.Document._width), 2 * NumCast(this.props.Document._height)); Doc.GetProto(newDoc).recordingSource = this.dataDoc; Doc.GetProto(newDoc).recordingStart = ComputedField.MakeFunction(`self.recordingSource["${this.props.fieldKey}-recordingStart"]`); - Doc.GetProto(newDoc).audioState = ComputedField.MakeFunction("self.recordingSource.audioState"); + Doc.GetProto(newDoc).mediaState = ComputedField.MakeFunction("self.recordingSource.mediaState"); this.props.addDocument?.(newDoc); e.stopPropagation(); } @@ -295,40 +291,11 @@ export class AudioBox extends ViewBoxAnnotatableComponent<FieldViewProps, AudioD e.stopPropagation(); } - // returns the audio waveform - @computed get waveform() { - const audioBuckets = Cast(this.dataDoc.audioBuckets, listSpec("number"), []); - !audioBuckets.length && setTimeout(() => this.createWaveformBuckets()); - return <Waveform - color={"darkblue"} - height={this._waveHeight} - barWidth={0.1} - pos={this.duration} - duration={this.duration} - peaks={audioBuckets.length === AudioBox.NUMBER_OF_BUCKETS ? audioBuckets : undefined} - progressColor={"blue"} />; - } - - // decodes the audio file into peaks for generating the waveform - createWaveformBuckets = async () => { - axios({ url: this.path, responseType: "arraybuffer" }) - .then(response => (new (window.AudioContext)()).decodeAudioData(response.data, - action(buffer => { - const decodedAudioData = buffer.getChannelData(0); - const bucketDataSize = Math.floor(decodedAudioData.length / AudioBox.NUMBER_OF_BUCKETS); - const brange = Array.from(Array(bucketDataSize)); - this.dataDoc.audioBuckets = new List<number>( - numberRange(AudioBox.NUMBER_OF_BUCKETS).map(i => - brange.reduce((p, x, j) => Math.abs(Math.max(p, decodedAudioData[i * bucketDataSize + j])), 0) / 2)); - })) - ); - } - - playing = () => this.audioState === "playing"; + playing = () => this.mediaState === "playing"; playLink = (link: Doc) => { const stack = this._stackedTimeline.current; if (link.annotationOn === this.rootDoc) { - if (this.layoutDoc.playOnSelect) this.playFrom(stack?.anchorStart(link) || 0, stack?.anchorEnd(link)); + if (!this.layoutDoc.dontAutoPlayFollowedLinks) this.playFrom(stack?.anchorStart(link) || 0, stack?.anchorEnd(link)); else this._ele!.currentTime = this.layoutDoc._currentTimecode = (stack?.anchorStart(link) || 0); } else { @@ -337,7 +304,7 @@ export class AudioBox extends ViewBoxAnnotatableComponent<FieldViewProps, AudioD const startTime = stack?.anchorStart(la1) || stack?.anchorStart(la2); const endTime = stack?.anchorEnd(la1) || stack?.anchorEnd(la2); if (startTime !== undefined) { - if (this.layoutDoc.playOnSelect) endTime ? this.playFrom(startTime, endTime) : this.playFrom(startTime); + if (!this.layoutDoc.dontAutoPlayFollowedLinks) endTime ? this.playFrom(startTime, endTime) : this.playFrom(startTime); else this._ele!.currentTime = this.layoutDoc._currentTimecode = startTime; } }); @@ -353,6 +320,7 @@ export class AudioBox extends ViewBoxAnnotatableComponent<FieldViewProps, AudioD @computed get renderTimeline() { return <CollectionStackedTimeline ref={this._stackedTimeline} {...this.props} fieldKey={this.annotationKey} + mediaPath={this.path} renderDepth={this.props.renderDepth + 1} startTag={"_timecodeToShow" /* audioStart */} endTag={"_timecodeToHide" /* audioEnd */} @@ -385,9 +353,9 @@ export class AudioBox extends ViewBoxAnnotatableComponent<FieldViewProps, AudioD {!this.path ? <div className="audiobox-buttons"> <div className="audiobox-dictation" onClick={this.onFile}> - <FontAwesomeIcon style={{ width: "30px", background: this.layoutDoc.playOnSelect ? "yellow" : "rgba(0,0,0,0)" }} icon="file-alt" size={this.props.PanelHeight() < 36 ? "1x" : "2x"} /> + <FontAwesomeIcon style={{ width: "30px", background: !this.layoutDoc.dontAutoPlayFollowedLinks ? "yellow" : "rgba(0,0,0,0)" }} icon="file-alt" size={this.props.PanelHeight() < 36 ? "1x" : "2x"} /> </div> - {this.audioState === "recording" || this.audioState === "paused" ? + {this.mediaState === "recording" || this.mediaState === "paused" ? <div className="recording" onClick={e => e.stopPropagation()}> <div className="buttons" onClick={this.recordClick}> <FontAwesomeIcon icon={"stop"} size={this.props.PanelHeight() < 36 ? "1x" : "2x"} /> @@ -405,11 +373,8 @@ export class AudioBox extends ViewBoxAnnotatableComponent<FieldViewProps, AudioD <div className="audiobox-controls" style={{ pointerEvents: this._isChildActive || this.active() ? "all" : "none" }} > <div className="audiobox-dictation" /> <div className="audiobox-player" style={{ height: `${AudioBox.heightPercent}%` }} > - <div className="audiobox-playhead" style={{ width: AudioBox.playheadWidth }} title={this.audioState === "paused" ? "play" : "pause"} onClick={this.Play}> <FontAwesomeIcon style={{ width: "100%", position: "absolute", left: "0px", top: "5px", borderWidth: "thin", borderColor: "white" }} icon={this.audioState === "paused" ? "play" : "pause"} size={"1x"} /></div> + <div className="audiobox-playhead" style={{ width: AudioBox.playheadWidth }} title={this.mediaState === "paused" ? "play" : "pause"} onClick={this.Play}> <FontAwesomeIcon style={{ width: "100%", position: "absolute", left: "0px", top: "5px", borderWidth: "thin", borderColor: "white" }} icon={this.mediaState === "paused" ? "play" : "pause"} size={"1x"} /></div> <div className="audiobox-timeline" style={{ top: 0, height: `100%`, left: AudioBox.playheadWidth, width: `calc(100% - ${AudioBox.playheadWidth}px)`, background: "white" }}> - <div className="waveform"> - {this.waveform} - </div> {this.renderTimeline} </div> {this.audio} diff --git a/src/client/views/nodes/CollectionFreeFormDocumentView.scss b/src/client/views/nodes/CollectionFreeFormDocumentView.scss index da287649e..724394025 100644 --- a/src/client/views/nodes/CollectionFreeFormDocumentView.scss +++ b/src/client/views/nodes/CollectionFreeFormDocumentView.scss @@ -5,4 +5,5 @@ touch-action: manipulation; top: 0; left: 0; + pointer-events: none; }
\ No newline at end of file diff --git a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx index 58fb005b5..e96fc088e 100644 --- a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx +++ b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx @@ -176,7 +176,6 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF zIndex: this.ZInd, mixBlendMode: StrCast(this.layoutDoc.mixBlendMode) as any, display: this.ZInd === -99 ? "none" : undefined, - pointerEvents: "none" }} > {Doc.UserDoc().renderStyle !== "comic" ? (null) : diff --git a/src/client/views/nodes/ColorBox.scss b/src/client/views/nodes/ColorBox.scss index da3266dc1..d5f2a7ec7 100644 --- a/src/client/views/nodes/ColorBox.scss +++ b/src/client/views/nodes/ColorBox.scss @@ -6,7 +6,6 @@ transform-origin: top left; .sketch-picker { - margin:auto; div { cursor: crosshair; } diff --git a/src/client/views/nodes/ColorBox.tsx b/src/client/views/nodes/ColorBox.tsx index 61cc8b3d1..59a26f323 100644 --- a/src/client/views/nodes/ColorBox.tsx +++ b/src/client/views/nodes/ColorBox.tsx @@ -2,7 +2,7 @@ import React = require("react"); import { action } from "mobx"; import { observer } from "mobx-react"; import { ColorState, SketchPicker } from 'react-color'; -import { Doc } from "../../../fields/Doc"; +import { Doc, WidthSym, HeightSym } from '../../../fields/Doc'; import { Utils } from "../../../Utils"; import { documentSchema } from "../../../fields/documentSchemas"; import { InkTool } from "../../../fields/InkField"; @@ -28,54 +28,45 @@ export class ColorBox extends ViewBoxBaseComponent<FieldViewProps, ColorDocument @undoBatch @action static switchColor(color: ColorState) { - Doc.UserDoc().backgroundColor = Utils.colorString(color); + // Doc.UserDoc().backgroundColor = Utils.colorString(color); // bcz: this can't go here ... needs a proper home in the settings panel SetActiveInkColor(color.hex); - if (CurrentUserUtils.SelectedTool === InkTool.None) { - const selected = SelectionManager.Views(); - selected.map(view => { - const targetDoc = view.props.Document.dragFactory instanceof Doc ? view.props.Document.dragFactory : - view.props.Document.layout instanceof Doc ? view.props.Document.layout : - view.props.Document.isTemplateForField ? view.props.Document : Doc.GetProto(view.props.Document); - if (targetDoc) { - if (view.props.LayoutTemplate?.() || view.props.LayoutTemplateString) { // this situation typically occurs when you have a link dot - targetDoc.backgroundColor = Doc.UserDoc().backgroundColor; // 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"] = Doc.UserDoc().backgroundColor; - } else { - Doc.Layout(view.props.Document)._backgroundColor = Doc.UserDoc().backgroundColor; // '_backgroundColor' is template specific. 'backgroundColor' would apply to all templates, but has no UI at the moment - } + SelectionManager.Views().map(view => { + const targetDoc = view.props.Document.dragFactory instanceof Doc ? view.props.Document.dragFactory : + view.props.Document.layout instanceof Doc ? view.props.Document.layout : + view.props.Document.isTemplateForField ? view.props.Document : Doc.GetProto(view.props.Document); + if (targetDoc) { + if (view.props.LayoutTemplate?.() || view.props.LayoutTemplateString) { // this situation typically occurs when you have a link dot + targetDoc.backgroundColor = color.hex; // bcz: don't know how to change the color of an inline template... } - }); - } + else if (RichTextMenu.Instance?.TextViewFieldKey && window.getSelection()?.toString() !== "") { + Doc.Layout(view.props.Document)[RichTextMenu.Instance.TextViewFieldKey + "-color"] = color.hex; + } else { + Doc.Layout(view.props.Document)._backgroundColor = color.hex; // '_backgroundColor' is template specific. 'backgroundColor' would apply to all templates, but has no UI at the moment + } + } + }); } - constructor(props: any) { - super(props); - } render() { - const selDoc = SelectionManager.Views()?.[0]?.rootDoc; + 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.active() ? "-interactive" : ""}`} - onPointerDown={e => e.button === 0 && !e.ctrlKey && e.stopPropagation()} onClick={e => { (e.nativeEvent as any).stuff = true; e.stopPropagation(); }} - style={{ width: `${100}%`, height: `${100}%` }} > + 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={ColorBox.switchColor} presetColors={['#D0021B', '#F5A623', '#F8E71C', '#8B572A', '#7ED321', '#417505', '#9013FE', '#4A90E2', '#50E3C2', '#B8E986', '#000000', '#4A4A4A', '#9B9B9B', '#FFFFFF', '#f1efeb', 'transparent']} - color={StrCast(selDoc?._backgroundColor, ActiveInkColor())} /> + <SketchPicker + onChange={c => CurrentUserUtils.SelectedTool === 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={{ display: "grid", gridTemplateColumns: "20% 80%", paddingTop: "10px" }}> - <div> {ActiveInkWidth() ?? 2}</div> - <input type="range" defaultValue={ActiveInkWidth() ?? 2} min={1} max={100} onChange={(e: React.ChangeEvent<HTMLInputElement>) => { + <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> {ActiveInkBezierApprox() ?? 2}</div> - <input type="range" defaultValue={ActiveInkBezierApprox() ?? 2} min={0} max={300} onChange={(e: React.ChangeEvent<HTMLInputElement>) => { - SetActiveBezierApprox(e.target.value); - SelectionManager.SelectedDocuments().filter(i => StrCast(i.rootDoc.type) === DocumentType.INK).map(i => i.rootDoc.strokeBezier = e.target.value); - }} /> */} - <br /> - <br /> </div> </div>; } diff --git a/src/client/views/nodes/DocumentLinksButton.tsx b/src/client/views/nodes/DocumentLinksButton.tsx index 57d1a41b6..a6d07374a 100644 --- a/src/client/views/nodes/DocumentLinksButton.tsx +++ b/src/client/views/nodes/DocumentLinksButton.tsx @@ -31,7 +31,6 @@ interface DocumentLinksButtonProps { AlwaysOn?: boolean; InMenu?: boolean; StartLink?: boolean; - links: Doc[]; } @observer export class DocumentLinksButton extends React.Component<DocumentLinksButtonProps, {}> { @@ -225,7 +224,6 @@ export class DocumentLinksButton extends React.Component<DocumentLinksButtonProp } })); - @action clearLinks() { DocumentLinksButton.StartLink = undefined; DocumentLinksButton.StartLinkView = undefined; @@ -233,15 +231,13 @@ export class DocumentLinksButton extends React.Component<DocumentLinksButtonProp @computed get filteredLinks() { const results = [] as Doc[]; - Array.from(new Set<Doc>(this.props.links)).forEach(link => { - if (!DocUtils.FilterDocs([link], this.props.View.props.docFilters(), []).length) { - if (DocUtils.FilterDocs([link.anchor2 as Doc], this.props.View.props.docFilters(), []).length) { - results.push(link); - } - if (DocUtils.FilterDocs([link.anchor1 as Doc], this.props.View.props.docFilters(), []).length) { - results.push(link); - } - } else results.push(link); + const filters = this.props.View.props.docFilters(); + Array.from(new Set<Doc>(this.props.View.allLinks)).forEach(link => { + if (DocUtils.FilterDocs([link], filters, []).length || + DocUtils.FilterDocs([link.anchor2 as Doc], filters, []).length || + DocUtils.FilterDocs([link.anchor1 as Doc], filters, []).length) { + results.push(link); + } }); return results; } @@ -296,12 +292,12 @@ export class DocumentLinksButton extends React.Component<DocumentLinksButtonProp return !Array.from(this.filteredLinks).length && !this.props.AlwaysOn ? (null) : this.props.InMenu && (DocumentLinksButton.StartLink || this.props.StartLink) ? - <Tooltip title={<><div className="dash-tooltip">{title}</div></>}> + <Tooltip title={<div className="dash-tooltip">{title}</div>}> {this.linkButtonInner} </Tooltip> : !DocumentLinksButton.LinkEditorDocView && !this.props.InMenu ? - <Tooltip title={<><div className="dash-tooltip">{title}</div></>}> + <Tooltip title={<div className="dash-tooltip">{title}</div>}> {this.linkButtonInner} </Tooltip> : this.linkButtonInner; diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx index bbbc6572f..231c9ff38 100644 --- a/src/client/views/nodes/DocumentView.tsx +++ b/src/client/views/nodes/DocumentView.tsx @@ -85,6 +85,7 @@ export interface DocComponentView { menuControls?: () => JSX.Element; // controls to display in the top menu bar when the document is selected. getKeyFrameEditing?: () => boolean; // whether the document is in keyframe editing mode (if it is, then all hidden documents that are not active at the keyframe time will still be shown) setKeyFrameEditing?: (set: boolean) => void; // whether the document is in keyframe editing mode (if it is, then all hidden documents that are not active at the keyframe time will still be shown) + playFrom?: (time: number, endTime?: number) => void; } export interface DocumentViewSharedProps { renderDepth: number; @@ -101,6 +102,7 @@ export interface DocumentViewSharedProps { layerProvider: undefined | ((doc: Doc, assign?: boolean) => boolean); styleProvider: Opt<StyleProviderFunc>; focus: DocFocusFunc; + fitWidth?: () => boolean; docFilters: () => string[]; docRangeFilters: () => string[]; searchFilterDocs: () => Doc[]; @@ -158,7 +160,8 @@ export interface DocumentViewInternalProps extends DocumentViewProps { @observer export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps, Document>(Document) { @observable _animateScalingTo = 0; - @observable _audioState = 0; + @observable _mediaState = 0; + @observable _pendingDoubleClick = false; private _downX: number = 0; private _downY: number = 0; private _firstX: number = -1; @@ -171,7 +174,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps private _dropDisposer?: DragManager.DragDropDisposer; private _holdDisposer?: InteractionUtils.MultiTouchEventDisposer; protected _multiTouchDisposer?: InteractionUtils.MultiTouchEventDisposer; - _componentView: Opt<DocComponentView>; + _componentView: Opt<DocComponentView>; // needs to be accessed from DocumentView wrapper class private get topMost() { return this.props.renderDepth === 0; } private get active() { return this.props.isSelected(true) || this.props.parentActive(true); } @@ -292,7 +295,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps if (e.cancelBubble && this.active) { this.removeMoveListeners(); } - else if (!e.cancelBubble && (this.props.isSelected(true) || this.props.parentActive(true) || this.layoutDoc.onDragStart || this.onClickHandler) && !this.layoutDoc._lockedPosition && !CurrentUserUtils.OverlayDocs.includes(this.layoutDoc)) { + else if (!e.cancelBubble && (this.active || this.layoutDoc.onDragStart || this.onClickHandler) && !this.layoutDoc._lockedPosition && !CurrentUserUtils.OverlayDocs.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)) { @@ -342,7 +345,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps Doc.SetNativeWidth(layoutDoc, actualdW / (layoutDoc._width || 1) * Doc.NativeWidth(layoutDoc)); } layoutDoc._width = actualdW; - if (fixedAspect && !layoutDoc._fitWidth) layoutDoc._height = nheight / nwidth * layoutDoc._width; + if (fixedAspect && !this.props.DocumentView().fitWidth) layoutDoc._height = nheight / nwidth * layoutDoc._width; else layoutDoc._height = actualdH; } else { @@ -350,7 +353,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps Doc.SetNativeHeight(layoutDoc, actualdH / (layoutDoc._height || 1) * Doc.NativeHeight(doc)); } layoutDoc._height = actualdH; - if (fixedAspect && !layoutDoc._fitWidth) layoutDoc._width = nwidth / nheight * layoutDoc._height; + if (fixedAspect && !this.props.DocumentView().fitWidth) layoutDoc._width = nwidth / nheight * layoutDoc._height; else layoutDoc._width = actualdW; } } else { @@ -483,10 +486,9 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps 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 { - const ctrlPressed = e.ctrlKey || e.shiftKey; - if (this.props.Document.type === DocumentType.WEB) { - this._timeout = setTimeout(() => { this._timeout = undefined; this.props.select(ctrlPressed); }, 350); - } else this.props.select(ctrlPressed); + runInAction(() => this._pendingDoubleClick = true); + this._timeout = setTimeout(action(() => { this._pendingDoubleClick = false; this._timeout = undefined; }), 350); + this.props.select(e.ctrlKey || e.shiftKey); } preventDefault = false; } @@ -530,7 +532,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps if (e.cancelBubble && this.active) { document.removeEventListener("pointermove", this.onPointerMove); // stop listening to pointerMove if something else has stopPropagated it (e.g., the MarqueeView) } - else if (!e.cancelBubble && (this.props.isSelected(true) || this.props.parentActive(true) || this.layoutDoc.onDragStart) && !this.layoutDoc._lockedPosition && !CurrentUserUtils.OverlayDocs.includes(this.layoutDoc)) { + else if (!e.cancelBubble && (this.active || this.layoutDoc.onDragStart) && !this.layoutDoc._lockedPosition && !CurrentUserUtils.OverlayDocs.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); @@ -560,9 +562,9 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps @undoBatch @action toggleFollowLink = (location: Opt<string>, zoom: boolean, setPushpin: boolean): void => { this.Document.ignoreClick = false; - this.Document.isLinkButton = !this.Document.isLinkButton; - setPushpin && (this.Document.isPushpin = this.Document.isLinkButton); - if (this.Document.isLinkButton && !this.onClickHandler) { + this.Document._isLinkButton = !this.Document._isLinkButton; + setPushpin && (this.Document.isPushpin = this.Document._isLinkButton); + if (this.Document._isLinkButton && !this.onClickHandler) { this.Document.followLinkZoom = zoom; this.Document.followLinkLocation = location; } else { @@ -572,13 +574,13 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps @undoBatch @action toggleTargetOnClick = (): void => { this.Document.ignoreClick = false; - this.Document.isLinkButton = true; + this.Document._isLinkButton = true; this.Document.isPushpin = true; } @undoBatch @action followLinkOnClick = (location: Opt<string>, zoom: boolean,): void => { this.Document.ignoreClick = false; - this.Document.isLinkButton = true; + this.Document._isLinkButton = true; this.Document.isPushpin = false; this.Document.followLinkZoom = zoom; this.Document.followLinkLocation = location; @@ -586,14 +588,14 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps @undoBatch @action selectOnClick = (): void => { this.Document.ignoreClick = false; - this.Document.isLinkButton = 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; + this.Document._isLinkButton = false; } @undoBatch deleteClicked = () => this.props.removeDocument?.(this.props.Document); @@ -632,7 +634,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps } this.Document.followLinkLocation = "inPlace"; this.Document.followLinkZoom = true; - this.Document.isLinkButton = true; + this.Document._isLinkButton = true; } @action @@ -723,7 +725,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps (this.rootDoc._viewType !== CollectionViewType.Docking || !Doc.UserDoc().noviceMode) && moreItems.push({ description: "Share", event: () => SharingManager.Instance.open(this.props.DocumentView()), icon: "users" }); if (!Doc.UserDoc().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._chromeStatus ? "Hide" : "Show"} Chrome`, event: () => this.Document._chromeStatus = (this.Document._chromeStatus ? undefined : "enabled"), icon: "project-diagram" }); + 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" }); @@ -772,7 +774,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps style={{ height: 25, position: "absolute", top: 10, left: 10 }} > <FontAwesomeIcon className="documentView-audioFont" - style={{ color: [DocListCast(this.dataDoc[this.LayoutFieldKey + "-audioAnnotations"]).length ? "blue" : "gray", "green", "red"][this._audioState] }} + 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" @@ -793,9 +795,9 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps onClick={this.onClickFunc} focus={this.focus} layoutKey={this.finalLayoutKey} /> - {this.layoutDoc.hideAllLinks ? (null) : this.allAnchors} + {this.layoutDoc.hideAllLinks ? (null) : this.allLinkEndpoints} {this.hideLinkButton ? (null) : - <DocumentLinksButton View={this.props.DocumentView()} links={this.allLinks} Offset={[this.topMost ? 0 : -15, undefined, undefined, this.topMost ? 10 : -20]} />} + <DocumentLinksButton View={this.props.DocumentView()} Offset={[this.topMost ? 0 : -15, undefined, undefined, this.topMost ? 10 : -20]} />} {audioView} </div>; @@ -816,11 +818,10 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps } @computed get directLinks() { TraceMobx(); return LinkManager.Instance.getAllDirectLinks(this.rootDoc); } @computed get allLinks() { TraceMobx(); return LinkManager.Instance.getAllRelatedLinks(this.rootDoc); } - @computed get allAnchors() { + @computed get allLinkEndpoints() { // the small blue dots that mark the endpoints of links TraceMobx(); if (this.props.LayoutTemplateString?.includes(LinkAnchorBox.name)) return null; if (this.layoutDoc.presBox || this.rootDoc.type === DocumentType.LINK || this.props.dontRegisterView) return (null); - // need to use allLinks for RTF since embedded linked text anchors are not rendered with DocumentViews. All other documents render their anchors with nested DocumentViews so we just need to render the directLinks here const filtered = DocUtils.FilterDocs(this.rootDoc.type === DocumentType.RTF ? this.allLinks : this.directLinks, this.props.docFilters(), []).filter(d => !d.hidden); return filtered.map((link, i) => @@ -841,7 +842,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps onPointerEnter = () => { const self = this; const audioAnnos = DocListCast(this.dataDoc[this.LayoutFieldKey + "-audioAnnotations"]); - if (audioAnnos && audioAnnos.length && this._audioState === 0) { + 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], @@ -850,10 +851,10 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps loop: false, volume: 0.5, onend: function () { - runInAction(() => self._audioState = 0); + runInAction(() => self._mediaState = 0); } }); - this._audioState = 1; + this._mediaState = 1; } } recordAudioAnnotation = () => { @@ -878,11 +879,11 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps } } }; - runInAction(() => self._audioState = 2); + runInAction(() => self._mediaState = 2); recorder.start(); setTimeout(() => { recorder.stop(); - runInAction(() => self._audioState = 0); + runInAction(() => self._mediaState = 0); gumStream.getAudioTracks()[0].stop(); }, 5000); }); @@ -1002,6 +1003,7 @@ export class DocumentView extends React.Component<DocumentViewProps> { 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.layoutDoc.fitWidth; } @computed get docViewPath() { return this.props.docViewPath ? [...this.props.docViewPath(), this] : [this]; } @computed get layoutDoc() { return Doc.Layout(this.Document, this.props.LayoutTemplate?.()); } @@ -1013,13 +1015,13 @@ export class DocumentView extends React.Component<DocumentViewProps> { return this.docView?._componentView?.reverseNativeScaling?.() ? 0 : returnVal(this.props.NativeHeight?.(), Doc.NativeHeight(this.layoutDoc, this.props.DataDoc, this.props.freezeDimensions)); } - @computed get shouldNotScale() { return (this.layoutDoc._fitWidth && !this.nativeWidth) || [CollectionViewType.Docking, CollectionViewType.Tree].includes(this.Document._viewType as any); } + @computed get shouldNotScale() { return (this.fitWidth && !this.nativeWidth) || [CollectionViewType.Docking, CollectionViewType.Tree].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 nativeScaling() { if (this.shouldNotScale) return 1; const minTextScale = this.Document.type === DocumentType.RTF ? 0.1 : 0; - if (this.layoutDoc._fitWidth || this.props.PanelHeight() / this.effectiveNativeHeight > this.props.PanelWidth() / this.effectiveNativeWidth) { + if (this.fitWidth || this.props.PanelHeight() / this.effectiveNativeHeight > this.props.PanelWidth() / this.effectiveNativeWidth) { return Math.max(minTextScale, this.props.PanelWidth() / this.effectiveNativeWidth); // width-limited or fitWidth } return Math.max(minTextScale, this.props.PanelHeight() / this.effectiveNativeHeight); // height-limited or unscaled @@ -1035,7 +1037,7 @@ export class DocumentView extends React.Component<DocumentViewProps> { @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.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.Document._fitWidth || this.props.dontCenter?.includes("y") ? 0 : this.Yshift; } + @computed get centeringY() { return this.fitWidth || this.props.dontCenter?.includes("y") ? 0 : this.Yshift; } toggleNativeDimensions = () => this.docView && Doc.toggleNativeDimensions(this.layoutDoc, this.docView.ContentScale, this.props.PanelWidth(), this.props.PanelHeight()); contentsActive = () => this.docView?.contentsActive(); @@ -1108,16 +1110,16 @@ export class DocumentView extends React.Component<DocumentViewProps> { render() { TraceMobx(); - const xshift = this.props.Document.isInkMask ? InkingStroke.MaskDim : Math.abs(this.Xshift) <= 0.001 ? this.props.PanelWidth() : undefined; - const yshift = this.props.Document.isInkMask ? InkingStroke.MaskDim : Math.abs(this.Yshift) <= 0.001 ? this.props.PanelHeight() : undefined; + const xshift = () => (this.props.Document.isInkMask ? InkingStroke.MaskDim : Math.abs(this.Xshift) <= 0.001 ? this.props.PanelWidth() : undefined); + const yshift = () => (this.props.Document.isInkMask ? InkingStroke.MaskDim : Math.abs(this.Yshift) <= 0.001 ? this.props.PanelHeight() : undefined); return (<div className="contentFittingDocumentView"> {!this.props.Document || !this.props.PanelWidth() ? (null) : ( <div className="contentFittingDocumentView-previewDoc" ref={this.ContentRef} style={{ position: this.props.Document.isInkMask ? "absolute" : undefined, transform: `translate(${this.centeringX}px, ${this.centeringY}px)`, - width: xshift ?? `${100 * (this.props.PanelWidth() - this.Xshift * 2) / this.props.PanelWidth()}%`, - height: yshift ?? (this.props.Document._fitWidth ? `${this.panelHeight}px` : + width: xshift() ?? `${100 * (this.props.PanelWidth() - this.Xshift * 2) / this.props.PanelWidth()}%`, + height: yshift() ?? (this.fitWidth ? `${this.panelHeight}px` : `${100 * this.effectiveNativeHeight / this.effectiveNativeWidth * this.props.PanelWidth() / this.props.PanelHeight()}%`), }}> <DocumentViewInternal {...this.props} diff --git a/src/client/views/nodes/FieldView.tsx b/src/client/views/nodes/FieldView.tsx index 465c18309..97f53aac0 100644 --- a/src/client/views/nodes/FieldView.tsx +++ b/src/client/views/nodes/FieldView.tsx @@ -73,28 +73,7 @@ export class FieldView extends React.Component<FieldViewProps> { return <p>{field.date.toLocaleString()}</p>; } else if (field instanceof Doc) { - return <p><b>{field.title && field.title.toString()}</b></p>; - //return <p><b>{field.title + " : id= " + field[Id]}</b></p>; - // let returnHundred = () => 100; - // return ( - // <DocumentContentsView Document={field} - // addDocument={undefined} - // addDocTab={this.props.addDocTab} - // removeDocument={undefined} - // ScreenToLocalTransform={Transform.Identity} - // ContentScaling={returnOne} - // PanelWidth={returnHundred} - // PanelHeight={returnHundred} - // renderDepth={0} //TODO Why is renderDepth reset? - // focus={emptyFunction} - // isSelected={this.props.isSelected} - // select={returnFalse} - // layoutKey={"layout"} - // ContainingCollectionView={this.props.ContainingCollectionView} - // parentActive={this.props.active} - // whenActiveChanged={this.props.whenActiveChanged} - // bringToFront={emptyFunction} /> - // ); + return <p><b>{field.title?.toString()}</b></p>; } else if (field instanceof List) { return <div> {field.length ? field.map(f => Field.toString(f)).join(", ") : ""} </div>; diff --git a/src/client/views/nodes/FunctionPlotBox.tsx b/src/client/views/nodes/FunctionPlotBox.tsx index e8bec9676..4be7d1c37 100644 --- a/src/client/views/nodes/FunctionPlotBox.tsx +++ b/src/client/views/nodes/FunctionPlotBox.tsx @@ -37,11 +37,7 @@ export class FunctionPlotBox extends ViewBoxBaseComponent<FieldViewProps, Equati () => this.createGraph()); } getAnchor = () => { - const anchor = Docs.Create.TextanchorDocument({ - useLinkSmallAnchor: true, - hideLinkButton: true, - annotationOn: this.rootDoc - }); + const anchor = Docs.Create.TextanchorDocument({ annotationOn: this.rootDoc }); anchor.xRange = new List<number>(Array.from(this._plot.options.xAxis.domain)); anchor.yRange = new List<number>(Array.from(this._plot.options.yAxis.domain)); return anchor; diff --git a/src/client/views/nodes/KeyValueBox.tsx b/src/client/views/nodes/KeyValueBox.tsx index 383650e89..111509fdb 100644 --- a/src/client/views/nodes/KeyValueBox.tsx +++ b/src/client/views/nodes/KeyValueBox.tsx @@ -163,7 +163,7 @@ export class KeyValueBox extends React.Component<FieldViewProps> { } getTemplate = async () => { - const parent = Docs.Create.StackingDocument([], { _width: 800, _height: 800, title: "Template" }); + const parent = Docs.Create.StackingDocument([], { _width: 800, _height: 800, title: "Template", _chromeHidden: true }); parent._columnWidth = 100; for (const row of this.rows.filter(row => row.isChecked)) { await this.createTemplateField(parent, row); diff --git a/src/client/views/nodes/LinkAnchorBox.tsx b/src/client/views/nodes/LinkAnchorBox.tsx index 392565402..3d72d047e 100644 --- a/src/client/views/nodes/LinkAnchorBox.tsx +++ b/src/client/views/nodes/LinkAnchorBox.tsx @@ -93,7 +93,7 @@ export class LinkAnchorBox extends ViewBoxBaseComponent<FieldViewProps, LinkAnch } openLinkTargetOnRight = (e: React.MouseEvent) => { const alias = Doc.MakeAlias(Cast(this.layoutDoc[this.fieldKey], Doc, null)); - alias.isLinkButton = undefined; + alias._isLinkButton = undefined; alias._layerTags = undefined; alias.layoutKey = "layout"; this.props.addDocTab(alias, "add:right"); diff --git a/src/client/views/nodes/PDFBox.scss b/src/client/views/nodes/PDFBox.scss index 564873cf5..0f46da294 100644 --- a/src/client/views/nodes/PDFBox.scss +++ b/src/client/views/nodes/PDFBox.scss @@ -18,11 +18,12 @@ top: 0; left: 0; - .pdfBox-overlayButton-sidebar { + .pdfBox-sidebarBtn { background: #121721; height: 25px; width: 25px; right: 0; + color: white; display: flex; position: absolute; align-items: center; @@ -39,8 +40,7 @@ left: 5px; top: 5px; - .pdfBox-overlayButton-fwd, - .pdfBox-overlayButton-back { + .pdfBox-fwdBtn, .pdfBox-backBtn { background: #121721; height: 25px; width: 25px; @@ -63,6 +63,9 @@ padding: 0; position: absolute; pointer-events: all; + color: white; + bottom: 0; + right: 0; .pdfBox-overlayButton-arrow { width: 0; @@ -89,6 +92,7 @@ .pdfBox-nextIcon, .pdfBox-prevIcon { background: #121721; + color: white; height: 20px; width: 25px; display: flex; @@ -189,26 +193,7 @@ } } - .pdfBox-tagList { - display: flex; - flex-direction: row; - overflow: auto; - flex-flow: row; - flex-wrap: wrap; - .pdfBox-filterTag, .pdfBox-filterTag-active { - font-weight: bold; - padding-left: 6; - padding-right: 6; - box-shadow: black 1px 1px 4px; - border-radius: 5; - margin: 2; - height: 20; - background-color: lightgrey; - } - .pdfBox-filterTag-active { - background-color: white; - } - } + .pdfBox-title-outer { width: 100%; height: 100%; @@ -330,6 +315,7 @@ font-size: 30px; width: 50px; height: 50px; + color: white; } .pdfBox .pdfBox-ui .pdfBox-nextIcon, diff --git a/src/client/views/nodes/PDFBox.tsx b/src/client/views/nodes/PDFBox.tsx index 0dbe0c917..f27a34e36 100644 --- a/src/client/views/nodes/PDFBox.tsx +++ b/src/client/views/nodes/PDFBox.tsx @@ -3,33 +3,26 @@ import { action, computed, IReactionDisposer, observable, reaction, runInAction import { observer } from "mobx-react"; import * as Pdfjs from "pdfjs-dist"; import "pdfjs-dist/web/pdf_viewer.css"; -import { Doc, DocListCast, Opt, StrListCast, WidthSym } from "../../../fields/Doc"; +import { Doc, Opt, WidthSym } from "../../../fields/Doc"; import { documentSchema } from '../../../fields/documentSchemas'; -import { Id } from '../../../fields/FieldSymbols'; import { makeInterface } from "../../../fields/Schema"; import { Cast, NumCast, StrCast } from '../../../fields/Types'; import { PdfField } from "../../../fields/URLField"; import { TraceMobx } from '../../../fields/util'; -import { emptyFunction, OmitKeys, returnOne, returnTrue, returnZero, Utils } from '../../../Utils'; -import { Docs, DocUtils } from '../../documents/Documents'; +import { Utils } from '../../../Utils'; +import { Docs } from '../../documents/Documents'; import { KeyCodes } from '../../util/KeyCodes'; import { undoBatch } from '../../util/UndoManager'; import { panZoomSchema } from '../collections/collectionFreeForm/CollectionFreeFormView'; -import { CollectionStackingView } from '../collections/CollectionStackingView'; -import { CollectionViewType } from '../collections/CollectionView'; import { ContextMenu } from '../ContextMenu'; import { ContextMenuProps } from '../ContextMenuItem'; import { ViewBoxAnnotatableComponent } from "../DocComponent"; import { PDFViewer } from "../pdf/PDFViewer"; -import { SearchBox } from '../search/SearchBox'; -import { StyleProp } from '../StyleProvider'; +import { SidebarAnnos } from '../SidebarAnnos'; import { FieldView, FieldViewProps } from './FieldView'; -import { FormattedTextBox } from './formattedText/FormattedTextBox'; import { pageSchema } from "./ImageBox"; import "./PDFBox.scss"; import React = require("react"); -import { DocFocusOptions } from './DocumentView'; -import { List } from '../../../fields/List'; type PdfDocument = makeInterface<[typeof documentSchema, typeof panZoomSchema, typeof pageSchema]>; const PdfDocument = makeInterface(documentSchema, panZoomSchema, pageSchema); @@ -38,32 +31,30 @@ const PdfDocument = makeInterface(documentSchema, panZoomSchema, pageSchema); export class PDFBox extends ViewBoxAnnotatableComponent<FieldViewProps, PdfDocument>(PdfDocument) { public static LayoutString(fieldKey: string) { return FieldView.LayoutString(PDFBox, fieldKey); } private _searchString: string = ""; - private _initialScale: number = 0; // the initial scale of the PDF when first rendered which determines whether the document will be live on startup or not. Getting bigger after startup won't make it automatically be live. - private _displayPdfLive = false; // has this box ever had its contents activated -- if so, stop drawing the overlay title + private _initialScrollTarget: Opt<Doc>; private _pdfViewer: PDFViewer | undefined; private _searchRef = React.createRef<HTMLInputElement>(); private _selectReactionDisposer: IReactionDisposer | undefined; + private _sidebarRef = React.createRef<SidebarAnnos>(); @observable private _searching: boolean = false; @observable private _pdf: Opt<Pdfjs.PDFDocumentProxy>; @observable private _pageControls = false; + @computed get pdfUrl() { return Cast(this.dataDoc[this.props.fieldKey], PdfField); } + constructor(props: any) { super(props); - this._initialScale = this.props.ScreenToLocalTransform().Scale; const nw = Doc.NativeWidth(this.Document, this.dataDoc) || 927; const nh = Doc.NativeHeight(this.Document, this.dataDoc) || 1200; !this.Document._fitWidth && (this.Document._height = this.Document[WidthSym]() * (nh / nw)); - const pdfUrl = Cast(this.dataDoc[this.props.fieldKey], PdfField); - if (pdfUrl) { - if (PDFBox.pdfcache.get(pdfUrl.url.href)) runInAction(() => this._pdf = PDFBox.pdfcache.get(pdfUrl.url.href)); - else if (PDFBox.pdfpromise.get(pdfUrl.url.href)) PDFBox.pdfpromise.get(pdfUrl.url.href)?.then(action(pdf => this._pdf = pdf)); + if (this.pdfUrl) { + if (PDFBox.pdfcache.get(this.pdfUrl.url.href)) runInAction(() => this._pdf = PDFBox.pdfcache.get(this.pdfUrl!.url.href)); + else if (PDFBox.pdfpromise.get(this.pdfUrl.url.href)) PDFBox.pdfpromise.get(this.pdfUrl.url.href)?.then(action(pdf => this._pdf = pdf)); } const backup = "oldPath"; - const { Document } = this.props; - const pdf = Cast(this.dataDoc[this.props.fieldKey], PdfField); - const href = pdf?.url?.href; + const href = this.pdfUrl?.url.href; if (href) { const pathCorrectionTest = /upload\_[a-z0-9]{32}.(.*)/g; const matches = pathCorrectionTest.exec(href); @@ -75,7 +66,7 @@ export class PDFBox extends ViewBoxAnnotatableComponent<FieldViewProps, PdfDocum //console.log(properPath); if (!properPath.includes(href)) { console.log(`The two (url and proper path) were not equal`); - const proto = Doc.GetProto(Document); + const proto = Doc.GetProto(this.props.Document); proto[this.props.fieldKey] = new PdfField(properPath); proto[backup] = href; } else { @@ -87,37 +78,30 @@ export class PDFBox extends ViewBoxAnnotatableComponent<FieldViewProps, PdfDocum } } - initialScrollTarget: Opt<Doc>; + componentWillUnmount() { this._selectReactionDisposer?.(); } + componentDidMount() { + this.props.setContentView?.(this); + this._selectReactionDisposer = reaction(() => this.props.isSelected(), + () => { + document.removeEventListener("keydown", this.onKeyDown); + this.props.isSelected(true) && document.addEventListener("keydown", this.onKeyDown); + }, { fireImmediately: true }); + } + scrollFocus = (doc: Doc, smooth: boolean) => { - if (DocListCast(this.rootDoc[this.sidebarKey()]).includes(doc)) { - if (this.layoutDoc["_" + this.sidebarKey() + "-docFilters"]) { - this.layoutDoc["_" + this.sidebarKey() + "-docFilters"] = new List<string>(); - return 1; - } - } - this.initialScrollTarget = doc; + if (this._sidebarRef?.current?.makeDocUnfiltered(doc)) return 1; + this._initialScrollTarget = doc; return this._pdfViewer?.scrollFocus(doc, smooth); } getAnchor = () => { const anchor = Docs.Create.TextanchorDocument({ title: StrCast(this.rootDoc.title + " " + this.layoutDoc._scrollTop), - useLinkSmallAnchor: true, - hideLinkButton: true, annotationOn: this.rootDoc, y: NumCast(this.layoutDoc._scrollTop), }); this.addDocument(anchor); return anchor; } - componentWillUnmount() { this._selectReactionDisposer?.(); } - componentDidMount() { - this.props.setContentView?.(this); - this._selectReactionDisposer = reaction(() => this.props.isSelected(), - () => { - document.removeEventListener("keydown", this.onKeyDown); - this.props.isSelected(true) && document.addEventListener("keydown", this.onKeyDown); - }, { fireImmediately: true }); - } @action loaded = (nw: number, nh: number, np: number) => { @@ -127,89 +111,25 @@ export class PDFBox extends ViewBoxAnnotatableComponent<FieldViewProps, PdfDocum this.layoutDoc._height = this.layoutDoc[WidthSym]() / (Doc.NativeAspect(this.dataDoc) || 1); !this.Document._fitWidth && (this.Document._height = this.Document[WidthSym]() * (nh / nw)); } - sidebarKey = () => this.fieldKey + "-sidebar"; - sidebarFiltersHeight = () => 50; - sidebarTransform = () => this.props.ScreenToLocalTransform().translate(Doc.NativeWidth(this.dataDoc), 0).scale(this.props.scaling?.() || 1); - sidebarWidth = () => !this.layoutDoc._showSidebar ? 0 : (NumCast(this.layoutDoc.nativeWidth) - Doc.NativeWidth(this.dataDoc)) * this.props.PanelWidth() / NumCast(this.layoutDoc.nativeWidth); - sidebarHeight = () => this.props.PanelHeight() - this.sidebarFiltersHeight() - 20; - sidebarAddDocument = (doc: Doc | Doc[]) => this.addDocument(doc, this.sidebarKey()); - sidebarMoveDocument = (doc: Doc | Doc[], targetCollection: Doc | undefined, addDocument: (doc: Doc | Doc[]) => boolean) => this.moveDocument(doc, targetCollection, addDocument, this.sidebarKey()); - sidebarRemDocument = (doc: Doc | Doc[]) => this.removeDocument(doc, this.sidebarKey()); - sidebarDocFilters = () => [...StrListCast(this.layoutDoc._docFilters), ...StrListCast(this.layoutDoc[this.sidebarKey() + "-docFilters"])]; - @computed get allTags() { - const keys = new Set<string>(); - DocListCast(this.rootDoc[this.sidebarKey()]).forEach(doc => SearchBox.documentKeys(doc).forEach(key => keys.add(key))); - return Array.from(keys.keys()).filter(key => key[0]).filter(key => !key.startsWith("_") && (key[0] === "#" || key[0] === key[0].toUpperCase())).sort(); - } - renderTag = (tag: string) => { - const active = StrListCast(this.rootDoc[this.sidebarKey() + "-docFilters"]).includes(`${tag}:${tag}:check`); - return <div key={tag} className={`pdfbox-filterTag${active ? "-active" : ""}`} - onClick={e => Doc.setDocFilter(this.rootDoc, tag, tag, "check", true, this.sidebarKey())}> - {tag} - </div>; - } - @computed get sidebarOverlay() { - return !this.layoutDoc._showSidebar ? (null) : - <div style={{ - position: "absolute", pointerEvents: this.active() ? "all" : undefined, top: 0, right: 0, - background: this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.WidgetColor), - width: `${this.sidebarWidth()}px`, - height: "100%" - }}> - <div style={{ width: "100%", height: this.sidebarHeight(), position: "relative" }}> - <CollectionStackingView {...OmitKeys(this.props, ["NativeWidth", "NativeHeight", "setContentView"]).omit} - NativeWidth={returnZero} - NativeHeight={returnZero} - PanelHeight={this.sidebarHeight} - PanelWidth={this.sidebarWidth} - xMargin={0} - yMargin={0} - docFilters={this.sidebarDocFilters} - chromeStatus={"enabled"} - scaleField={this.sidebarKey() + "-scale"} - isAnnotationOverlay={false} - select={emptyFunction} - active={this.annotationsActive} - scaling={returnOne} - whenActiveChanged={this.whenActiveChanged} - childHideDecorationTitle={returnTrue} - removeDocument={this.sidebarRemDocument} - moveDocument={this.sidebarMoveDocument} - addDocument={this.sidebarAddDocument} - CollectionView={undefined} - ScreenToLocalTransform={this.sidebarTransform} - renderDepth={this.props.renderDepth + 1} - viewType={CollectionViewType.Stacking} - fieldKey={this.sidebarKey()} - pointerEvents={"all"} - /> - </div> - <div className="pdfBox-tagList" style={{ height: this.sidebarFiltersHeight(), width: this.sidebarWidth() }}> - {this.allTags.map(tag => this.renderTag(tag))} - </div> - </div>; - } - public search = (string: string, fwd: boolean) => { this._pdfViewer?.search(string, fwd); }; - public prevAnnotation = () => { this._pdfViewer?.prevAnnotation(); }; - public nextAnnotation = () => { this._pdfViewer?.nextAnnotation(); }; + public search = (string: string, fwd: boolean) => this._pdfViewer?.search(string, fwd); + public prevAnnotation = () => this._pdfViewer?.prevAnnotation(); + public nextAnnotation = () => this._pdfViewer?.nextAnnotation(); public backPage = () => { this.Document._curPage = (this.Document._curPage || 1) - 1; return true; }; public forwardPage = () => { this.Document._curPage = (this.Document._curPage || 1) + 1; return true; }; - public gotoPage = (p: number) => { this.Document._curPage = p; }; + public gotoPage = (p: number) => this.Document._curPage = p; @undoBatch onKeyDown = action((e: KeyboardEvent) => { let processed = false; - if (e.key === "f" && e.ctrlKey) { - this._searching = true; - setTimeout(() => this._searchRef.current && this._searchRef.current.focus(), 100); - processed = true; - } - if (e.key === "PageDown") processed = this.forwardPage(); - if (e.key === "PageUp") processed = this.backPage(); - if (e.target instanceof HTMLInputElement || this.props.ContainingCollectionDoc?._viewType !== CollectionViewType.Freeform) { - if (e.key === "ArrowDown" || e.key === "ArrowRight") processed = this.forwardPage(); - if (e.key === "ArrowUp" || e.key === "ArrowLeft") processed = this.backPage(); + switch (e.key) { + case "f": if (e.ctrlKey) { + setTimeout(() => this._searchRef.current?.focus(), 100); + this._searching = processed = true; + } + break; + case "PageDown": processed = this.forwardPage(); break; + case "PageUp": processed = this.backPage(); break; } if (processed) { e.stopImmediatePropagation(); @@ -219,97 +139,87 @@ export class PDFBox extends ViewBoxAnnotatableComponent<FieldViewProps, PdfDocum setPdfViewer = (pdfViewer: PDFViewer) => { this._pdfViewer = pdfViewer; - if (this.initialScrollTarget) { - this.scrollFocus(this.initialScrollTarget, false); - this.initialScrollTarget = undefined; + if (this._initialScrollTarget) { + this.scrollFocus(this._initialScrollTarget, false); + this._initialScrollTarget = undefined; } } searchStringChanged = (e: React.ChangeEvent<HTMLInputElement>) => this._searchString = e.currentTarget.value; - toggleSidebar = () => { - if (this.layoutDoc.nativeWidth === this.layoutDoc[this.fieldKey + "-nativeWidth"]) { - this.layoutDoc.nativeWidth = 250 + NumCast(this.layoutDoc[this.fieldKey + "-nativeWidth"]); - } else { - this.layoutDoc.nativeWidth = NumCast(this.layoutDoc[this.fieldKey + "-nativeWidth"]); - } - this.layoutDoc._width = NumCast(this.layoutDoc._nativeWidth) * (NumCast(this.layoutDoc[this.fieldKey + "-nativeWidth"]) / NumCast(this.layoutDoc[this.fieldKey + "-nativeHeight"])); + + sidebarAddDocument = (doc: Doc | Doc[], sidebarKey?: string) => { + if (!this.layoutDoc._showSidebar) this.toggleSidebar(); + return this.addDocument(doc, sidebarKey); } + toggleSidebar = action(() => { + const nativeWidth = NumCast(this.layoutDoc[this.fieldKey + "-nativeWidth"]); + const ratio = ((!this.layoutDoc.nativeWidth || this.layoutDoc.nativeWidth === nativeWidth ? 250 : 0) + nativeWidth) / nativeWidth; + const curNativeWidth = NumCast(this.layoutDoc.nativeWidth, nativeWidth); + this.layoutDoc.nativeWidth = nativeWidth * ratio; + this.layoutDoc._width = this.layoutDoc[WidthSym]() * nativeWidth * ratio / curNativeWidth; + this.layoutDoc._showSidebar = nativeWidth !== this.layoutDoc._nativeWidth; + }); settingsPanel() { const pageBtns = <> - <button className="pdfBox-overlayButton-back" key="back" title="Page Back" - onPointerDown={e => e.stopPropagation()} onClick={e => this.backPage()} > + <button className="pdfBox-backBtn" key="back" title="Page Back" + onPointerDown={e => e.stopPropagation()} onClick={this.backPage} > <FontAwesomeIcon style={{ color: "white" }} icon={"arrow-left"} size="sm" /> </button> - <button className="pdfBox-overlayButton-fwd" key="fwd" title="Page Forward" - onPointerDown={e => e.stopPropagation()} onClick={e => this.forwardPage()} > + <button className="pdfBox-fwdBtn" key="fwd" title="Page Forward" + onPointerDown={e => e.stopPropagation()} onClick={this.forwardPage} > <FontAwesomeIcon style={{ color: "white" }} icon={"arrow-right"} size="sm" /> </button> </>; const searchTitle = `${!this._searching ? "Open" : "Close"} Search Bar`; const curPage = this.Document._curPage || 1; return !this.active() ? (null) : - (<div className="pdfBox-ui" onKeyDown={e => e.keyCode === KeyCodes.BACKSPACE || e.keyCode === KeyCodes.DELETE ? e.stopPropagation() : true} + <div className="pdfBox-ui" onKeyDown={e => [KeyCodes.BACKSPACE, KeyCodes.DELETE].includes(e.keyCode) ? e.stopPropagation() : true} onPointerDown={e => e.stopPropagation()} style={{ display: this.active() ? "flex" : "none" }}> - <div className="pdfBox-overlayCont" key="cont" onPointerDown={(e) => e.stopPropagation()} style={{ left: `${this._searching ? 0 : 100}%` }}> + <div className="pdfBox-overlayCont" onPointerDown={(e) => e.stopPropagation()} style={{ left: `${this._searching ? 0 : 100}%` }}> <button className="pdfBox-overlayButton" title={searchTitle} /> - <input className="pdfBox-searchBar" placeholder="Search" ref={this._searchRef} onChange={this.searchStringChanged} onKeyDown={e => e.keyCode === KeyCodes.ENTER && this.search(this._searchString, !e.shiftKey)} /> + <input className="pdfBox-searchBar" placeholder="Search" ref={this._searchRef} onChange={this.searchStringChanged} + onKeyDown={e => e.keyCode === KeyCodes.ENTER && this.search(this._searchString, !e.shiftKey)} /> <button className="pdfBox-search" title="Search" onClick={e => this.search(this._searchString, !e.shiftKey)}> - <FontAwesomeIcon icon="search" size="sm" color="white" /></button> - <button className="pdfBox-prevIcon " title="Previous Annotation" onClick={this.prevAnnotation} > - <FontAwesomeIcon style={{ color: "white" }} icon={"arrow-up"} size="lg" /> + <FontAwesomeIcon icon="search" size="sm" /> + </button> + <button className="pdfBox-prevIcon" title="Previous Annotation" onClick={this.prevAnnotation} > + <FontAwesomeIcon icon={"arrow-up"} size="lg" /> </button> <button className="pdfBox-nextIcon" title="Next Annotation" onClick={this.nextAnnotation} > - <FontAwesomeIcon style={{ color: "white" }} icon={"arrow-down"} size="lg" /> + <FontAwesomeIcon icon={"arrow-down"} size="lg" /> </button> </div> - <button className="pdfBox-overlayButton" key="search" onClick={action(() => { - this._searching = !this._searching; - this.search("mxytzlaf", true); - })} title={searchTitle} style={{ bottom: 0, right: 0 }}> - <div className="pdfBox-overlayButton-arrow" onPointerDown={(e) => e.stopPropagation()}></div> + <button className="pdfBox-overlayButton" title={searchTitle} + onClick={action(() => { this._searching = !this._searching; this.search("mxytzlaf", true); })} > + <div className="pdfBox-overlayButton-arrow" onPointerDown={(e) => e.stopPropagation()} /> <div className="pdfBox-overlayButton-iconCont" onPointerDown={(e) => e.stopPropagation()}> - <FontAwesomeIcon style={{ color: "white" }} icon={this._searching ? "times" : "search"} size="lg" /></div> + <FontAwesomeIcon icon={this._searching ? "times" : "search"} size="lg" /> + </div> </button> <div className="pdfBox-pageNums"> - <input value={curPage} + <input value={curPage} style={{ width: `${curPage > 99 ? 4 : 3}ch`, pointerEvents: "all" }} onChange={e => this.Document._curPage = Number(e.currentTarget.value)} - style={{ width: `${curPage > 99 ? 4 : 3}ch`, pointerEvents: "all" }} onClick={action(() => this._pageControls = !this._pageControls)} /> {this._pageControls ? pageBtns : (null)} </div> - <button className="pdfBox-overlayButton-sidebar" key="sidebar" title="Toggle Sidebar" style={{ right: this.sidebarWidth() + 7 }} + <button className="pdfBox-sidebarBtn" title="Toggle Sidebar" + style={{ right: this.sidebarWidth() + 7, display: !this.active() ? "none" : undefined }} onPointerDown={e => e.stopPropagation()} onClick={e => this.toggleSidebar()} > - <FontAwesomeIcon style={{ color: "white" }} icon={"chevron-left"} size="sm" /> + <FontAwesomeIcon icon={"chevron-left"} size="sm" /> </button> - </div>); + </div>; } + sidebarWidth = () => !this.layoutDoc._showSidebar ? 0 : (NumCast(this.layoutDoc.nativeWidth) - Doc.NativeWidth(this.dataDoc)) * this.props.PanelWidth() / NumCast(this.layoutDoc.nativeWidth); specificContextMenu = (e: React.MouseEvent): void => { - const pdfUrl = Cast(this.dataDoc[this.props.fieldKey], PdfField); const funcs: ContextMenuProps[] = []; - pdfUrl && funcs.push({ description: "Copy path", event: () => Utils.CopyText(pdfUrl.url.pathname), icon: "expand-arrows-alt" }); + funcs.push({ description: "Copy path", event: () => this.pdfUrl && Utils.CopyText(this.pdfUrl.url.pathname), icon: "expand-arrows-alt" }); funcs.push({ description: "Toggle Fit Width " + (this.Document._fitWidth ? "Off" : "On"), event: () => this.Document._fitWidth = !this.Document._fitWidth, icon: "expand-arrows-alt" }); funcs.push({ description: "Toggle Annotation View ", event: () => this.Document._showSidebar = !this.Document._showSidebar, icon: "expand-arrows-alt" }); funcs.push({ description: "Toggle Sidebar ", event: () => this.toggleSidebar(), icon: "expand-arrows-alt" }); - ContextMenu.Instance.addItem({ description: "Options...", subitems: funcs, icon: "asterisk" }); } - anchorMenuClick = (anchor: Doc) => { - this.Document._showSidebar = true; - const startup = StrListCast(this.rootDoc.docFilters).map(filter => filter.split(":")[0]).join(" "); - const target = Docs.Create.TextDocument(startup, { - title: "anno", - annotationOn: this.rootDoc, _width: 200, _height: 50, _fitWidth: true, _autoHeight: true, _fontSize: StrCast(Doc.UserDoc().fontSize), - _fontFamily: StrCast(Doc.UserDoc().fontFamily) - }); - FormattedTextBox.SelectOnLoad = target[Id]; - FormattedTextBox.DontSelectInitialText = true; - this.allTags.map(tag => target[tag] = tag); - DocUtils.MakeLink({ doc: anchor }, { doc: target }, "inline markup", "annotation"); - this.sidebarAddDocument(target); - } - @computed get renderTitleBox() { const classname = "pdfBox" + (this.active() ? "-interactive" : ""); return <div className={classname} > @@ -318,17 +228,20 @@ export class PDFBox extends ViewBoxAnnotatableComponent<FieldViewProps, PdfDocum </div> </div>; } + @computed get renderPdfView() { TraceMobx(); - const pdfUrl = Cast(this.dataDoc[this.props.fieldKey], PdfField); return <div className={"pdfBox"} onContextMenu={this.specificContextMenu} - style={{ height: this.props.Document._scrollTop && !this.Document._fitWidth && (window.screen.width > 600) ? NumCast(this.Document._height) * this.props.PanelWidth() / NumCast(this.Document._width) : undefined }}> + style={{ + height: this.props.Document._scrollTop && !this.Document._fitWidth && (window.screen.width > 600) ? + NumCast(this.Document._height) * this.props.PanelWidth() / NumCast(this.Document._width) : undefined + }}> <div className="pdfBox-background" /> <PDFViewer {...this.props} pdf={this._pdf!} - url={pdfUrl!.url.pathname} + url={this.pdfUrl!.url.pathname} active={this.active} - anchorMenuClick={this.anchorMenuClick} + anchorMenuClick={this._sidebarRef.current?.anchorMenuClick} loaded={!Doc.NativeAspect(this.dataDoc) ? this.loaded : undefined} setPdfViewer={this.setPdfViewer} addDocument={this.addDocument} @@ -337,7 +250,17 @@ export class PDFBox extends ViewBoxAnnotatableComponent<FieldViewProps, PdfDocum ContentScaling={this.props.scaling} sidebarWidth={this.sidebarWidth} /> - {this.sidebarOverlay} + <SidebarAnnos ref={this._sidebarRef} + {...this.props} + annotationsActive={this.annotationsActive} + rootDoc={this.rootDoc} + layoutDoc={this.layoutDoc} + dataDoc={this.dataDoc} + sidebarAddDocument={this.sidebarAddDocument} + moveDocument={this.moveDocument} + removeDocument={this.removeDocument} + active={this.active} + /> {this.settingsPanel()} </div>; } @@ -346,19 +269,14 @@ export class PDFBox extends ViewBoxAnnotatableComponent<FieldViewProps, PdfDocum static pdfpromise = new Map<string, Pdfjs.PDFPromise<Pdfjs.PDFDocumentProxy>>(); render() { TraceMobx(); - if (true) {//this.props.isSelected() || (this.props.active() && this.props.renderDepth === 0)) { - this._displayPdfLive = true; - } - if (this._displayPdfLive) { - if (this._pdf) return this.renderPdfView; + if (this._pdf) return this.renderPdfView; - const href = Cast(this.dataDoc[this.props.fieldKey], PdfField, null)?.url.href; - if (href) { - if (PDFBox.pdfcache.get(href)) setTimeout(action(() => this._pdf = PDFBox.pdfcache.get(href))); - else { - if (!PDFBox.pdfpromise.get(href)) PDFBox.pdfpromise.set(href, Pdfjs.getDocument(href).promise); - PDFBox.pdfpromise.get(href)?.then(action(pdf => PDFBox.pdfcache.set(href, this._pdf = pdf))); - } + const href = this.pdfUrl?.url.href; + if (href) { + if (PDFBox.pdfcache.get(href)) setTimeout(action(() => this._pdf = PDFBox.pdfcache.get(href))); + else { + if (!PDFBox.pdfpromise.get(href)) PDFBox.pdfpromise.set(href, Pdfjs.getDocument(href).promise); + PDFBox.pdfpromise.get(href)?.then(action(pdf => PDFBox.pdfcache.set(href, this._pdf = pdf))); } } return this.renderTitleBox; diff --git a/src/client/views/nodes/PresBox.tsx b/src/client/views/nodes/PresBox.tsx index 3c1fda465..2aba461e0 100644 --- a/src/client/views/nodes/PresBox.tsx +++ b/src/client/views/nodes/PresBox.tsx @@ -13,7 +13,7 @@ import { PrefetchProxy } from "../../../fields/Proxy"; import { listSpec, makeInterface } from "../../../fields/Schema"; import { ScriptField } from "../../../fields/ScriptField"; import { BoolCast, Cast, NumCast, StrCast } from "../../../fields/Types"; -import { returnFalse, returnOne } from "../../../Utils"; +import { returnFalse, returnOne, returnTrue, emptyFunction } from '../../../Utils'; import { Docs } from "../../documents/Documents"; import { DocumentType } from "../../documents/DocumentTypes"; import { CurrentUserUtils } from "../../util/CurrentUserUtils"; @@ -195,7 +195,6 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema> componentDidMount() { this.rootDoc.presBox = this.rootDoc; this.rootDoc._forceRenderEngine = "timeline"; - this.rootDoc._replacedChrome = "replaced"; this.layoutDoc.presStatus = PresStatus.Edit; this.layoutDoc._gridGap = 0; this.layoutDoc._yMargin = 0; @@ -231,17 +230,20 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema> // 'Play on next' for audio or video therefore first navigate to the audio/video before it should be played startTempMedia = (targetDoc: Doc, activeItem: Doc) => { const duration: number = NumCast(activeItem.presEndTime) - NumCast(activeItem.presStartTime); - if (targetDoc.type === DocumentType.AUDIO) { - if (this._mediaTimer && this._mediaTimer[1] === targetDoc) clearTimeout(this._mediaTimer[0]); - targetDoc._triggerAudio = NumCast(activeItem.presStartTime); - this._mediaTimer = [setTimeout(() => targetDoc._audioStop = true, duration * 1000), targetDoc]; - } else if (targetDoc.type === DocumentType.VID) { - if (this._mediaTimer && this._mediaTimer[1] === targetDoc) clearTimeout(this._mediaTimer[0]); - targetDoc._triggerVideoStop = true; - setTimeout(() => targetDoc._currentTimecode = NumCast(activeItem.presStartTime), 10); - setTimeout(() => targetDoc._triggerVideo = true, 20); - this._mediaTimer = [setTimeout(() => targetDoc._triggerVideoStop = true, (duration * 1000) + 20), targetDoc]; + if ([DocumentType.VID, DocumentType.AUDIO].includes(targetDoc.type as any)) { + const targMedia = DocumentManager.Instance.getDocumentView(targetDoc); + targMedia?.ComponentView?.playFrom?.(NumCast(activeItem.presStartTime), NumCast(activeItem.presStartTime) + duration); } + // if (targetDoc.type === DocumentType.AUDIO) { + // if (this._mediaTimer && this._mediaTimer[1] === targetDoc) clearTimeout(this._mediaTimer[0]); + // targetDoc._triggerAudio = NumCast(activeItem.presStartTime); + // this._mediaTimer = [setTimeout(() => targetDoc._audioStop = true, duration * 1000), targetDoc]; + // } else if (targetDoc.type === DocumentType.VID) { + // targetDoc._triggerVideoStop = true; + // setTimeout(() => targetDoc._currentTimecode = NumCast(activeItem.presStartTime), 10); + // setTimeout(() => targetDoc._triggerVideo = true, 20); + // this._mediaTimer = [setTimeout(() => targetDoc._triggerVideoStop = true, (duration * 1000) + 20), targetDoc]; + // } } stopTempMedia = (targetDoc: Doc) => { @@ -424,7 +426,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema> // this still needs some fixing setTimeout(resetSelection, 500); if (doc !== targetDoc) { - setTimeout(() => finished?.(), 100); /// give it some time to create the targetDoc if we're opening up its context + setTimeout(finished ?? emptyFunction, 100); /// give it some time to create the targetDoc if we're opening up its context } else { finished?.(); } @@ -2262,7 +2264,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema> const mode = StrCast(this.rootDoc._viewType) as CollectionViewType; const isMini: boolean = this.toolbarWidth <= 100; return ( - <div className="presBox-buttons" style={{ display: !this.rootDoc._chromeStatus ? "none" : undefined }}> + <div className="presBox-buttons" style={{ display: !this.rootDoc._chromeHidden ? "none" : undefined }}> {isMini ? (null) : <select className="presBox-viewPicker" style={{ display: this.layoutDoc.presStatus === "edit" ? "block" : "none" }} onPointerDown={e => e.stopPropagation()} @@ -2449,6 +2451,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema> PanelHeight={this.panelHeight} childIgnoreNativeSize={true} moveDocument={returnFalse} + childFitWidth={returnTrue} childOpacity={returnOne} childLayoutTemplate={this.childLayoutTemplate} filterAddDocument={this.addDocumentFilter} diff --git a/src/client/views/nodes/ScreenshotBox.scss b/src/client/views/nodes/ScreenshotBox.scss index 141960f60..ab54cf526 100644 --- a/src/client/views/nodes/ScreenshotBox.scss +++ b/src/client/views/nodes/ScreenshotBox.scss @@ -26,7 +26,7 @@ position: absolute; right: 25; top: 0; - width:50; + width:25; height: 25; .screenshotBox-snapshot{ color : white; diff --git a/src/client/views/nodes/ScreenshotBox.tsx b/src/client/views/nodes/ScreenshotBox.tsx index a14d8ccae..c00c79eb9 100644 --- a/src/client/views/nodes/ScreenshotBox.tsx +++ b/src/client/views/nodes/ScreenshotBox.tsx @@ -1,40 +1,59 @@ import React = require("react"); import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { action, computed, IReactionDisposer, observable, runInAction } from "mobx"; +import { action, computed, observable } from "mobx"; import { observer } from "mobx-react"; -import * as rp from 'request-promise'; +import { DateField } from "../../../fields/DateField"; import { Doc, WidthSym } from "../../../fields/Doc"; import { documentSchema } from "../../../fields/documentSchemas"; +import { Id } from "../../../fields/FieldSymbols"; import { InkTool } from "../../../fields/InkField"; -import { listSpec, makeInterface } from "../../../fields/Schema"; +import { makeInterface } from "../../../fields/Schema"; +import { ComputedField } from "../../../fields/ScriptField"; import { Cast, NumCast } from "../../../fields/Types"; -import { VideoField } from "../../../fields/URLField"; -import { emptyFunction, returnFalse, returnOne, returnZero, Utils, OmitKeys } from "../../../Utils"; -import { Docs } from "../../documents/Documents"; +import { AudioField, VideoField } from "../../../fields/URLField"; +import { emptyFunction, OmitKeys, returnFalse, returnOne, Utils } from "../../../Utils"; +import { DocUtils } from "../../documents/Documents"; +import { DocumentType } from "../../documents/DocumentTypes"; +import { Networking } from "../../Network"; +import { CurrentUserUtils } from "../../util/CurrentUserUtils"; import { CollectionFreeFormView } from "../collections/collectionFreeForm/CollectionFreeFormView"; +import { CollectionStackedTimeline } from "../collections/CollectionStackedTimeline"; import { ContextMenu } from "../ContextMenu"; -import { ContextMenuProps } from "../ContextMenuItem"; -import { ViewBoxBaseComponent } from "../DocComponent"; +import { ViewBoxAnnotatableComponent } from "../DocComponent"; import { FieldView, FieldViewProps } from './FieldView'; import "./ScreenshotBox.scss"; -import { CurrentUserUtils } from "../../util/CurrentUserUtils"; -const path = require('path'); +import { VideoBox } from "./VideoBox"; +import { TraceMobx } from "../../../fields/util"; +import { FormattedTextBox } from "./formattedText/FormattedTextBox"; +declare class MediaRecorder { + constructor(e: any, options?: any); // whatever MediaRecorder has +} type ScreenshotDocument = makeInterface<[typeof documentSchema]>; const ScreenshotDocument = makeInterface(documentSchema); @observer -export class ScreenshotBox extends ViewBoxBaseComponent<FieldViewProps, ScreenshotDocument>(ScreenshotDocument) { - private _reactionDisposer?: IReactionDisposer; - private _videoRef: HTMLVideoElement | null = null; +export class ScreenshotBox extends ViewBoxAnnotatableComponent<FieldViewProps, ScreenshotDocument>(ScreenshotDocument) { public static LayoutString(fieldKey: string) { return FieldView.LayoutString(ScreenshotBox, fieldKey); } + private _videoRef = React.createRef<HTMLVideoElement>(); + private _audioRec: any; + private _videoRec: any; + @observable _screenCapture = false; + @computed get recordingStart() { return Cast(this.dataDoc[this.props.fieldKey + "-recordingStart"], DateField)?.date.getTime(); } - public get player(): HTMLVideoElement | null { - return this._videoRef; + constructor(props: any) { + super(props); + 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.player!.videoWidth / this.player!.videoHeight; + const aspect = this._videoRef.current!.videoWidth / this._videoRef.current!.videoHeight; const nativeWidth = Doc.NativeWidth(this.layoutDoc); const nativeHeight = Doc.NativeHeight(this.layoutDoc); if (!nativeWidth || !nativeHeight) { @@ -44,95 +63,24 @@ export class ScreenshotBox extends ViewBoxBaseComponent<FieldViewProps, Screensh } } - @action public Snapshot() { - const width = NumCast(this.layoutDoc._width); - const height = NumCast(this.layoutDoc._height); - const canvas = document.createElement('canvas'); - canvas.width = 640; - canvas.height = 640 / (Doc.NativeAspect(this.layoutDoc) || 1); - const ctx = canvas.getContext('2d');//draw image to canvas. scale to target dimensions - if (ctx) { - ctx.rect(0, 0, canvas.width, canvas.height); - ctx.fillStyle = "blue"; - ctx.fill(); - this._videoRef && ctx.drawImage(this._videoRef, 0, 0, canvas.width, canvas.height); - } - - if (this._videoRef) { - //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 filename = path.basename(encodeURIComponent("screenshot" + Utils.GenerateGuid().replace(/\..*$/, "").replace(" ", "_"))); - ScreenshotBox.convertDataUri(dataUrl, filename).then(returnedFilename => { - setTimeout(() => { - if (returnedFilename) { - const imageSummary = Docs.Create.ImageDocument(Utils.prepend(returnedFilename), { - x: NumCast(this.layoutDoc.x) + width, y: NumCast(this.layoutDoc.y), - _width: 150, _height: height / width * 150, title: "--screenshot--" - }); - if (!this.props.addDocument || this.props.addDocument === returnFalse) { - const spt = this.props.ScreenToLocalTransform().inverse().transformPoint(0, 0); - imageSummary.x = spt[0]; - imageSummary.y = spt[1]; - Cast(Cast(Doc.UserDoc().myOverlayDocs, Doc, null)?.data, listSpec(Doc), []).push(imageSummary); - } else { - this.props.addDocument?.(imageSummary); - } - } - }, 500); - }); - } - } - 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. } - componentWillUnmount() { - this._reactionDisposer && this._reactionDisposer(); - } - - @action - setVideoRef = (vref: HTMLVideoElement | null) => { - this._videoRef = vref; + const ind = DocUtils.ActiveRecordings.indexOf(this); + ind !== -1 && (DocUtils.ActiveRecordings.splice(ind, 1)); } - public static async convertDataUri(imageUri: string, returnedFilename: string) { - try { - const posting = Utils.prepend("/uploadURI"); - const returnedUri = await rp.post(posting, { - body: { - uri: imageUri, - name: returnedFilename - }, - json: true, - }); - return returnedUri; - - } catch (e) { - console.log("ScreenShotBox:" + e); - } - } - @observable _screenCapture = false; specificContextMenu = (e: React.MouseEvent): void => { - const field = Cast(this.dataDoc[this.fieldKey], VideoField); - if (field) { - const url = field.url.href; - const subitems: ContextMenuProps[] = []; - subitems.push({ description: "Take Snapshot", event: () => this.Snapshot(), icon: "expand-arrows-alt" }); - 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" - }); - ContextMenu.Instance.addItem({ description: "Options...", subitems: subitems, icon: "video" }); - } + const subitems = [{ description: "Screen Capture", event: this.toggleRecording, icon: "expand-arrows-alt" as any }]; + ContextMenu.Instance.addItem({ description: "Options...", subitems, icon: "video" }); } @computed get content() { const interactive = CurrentUserUtils.SelectedTool !== InkTool.None || !this.props.isSelected() ? "" : "-interactive"; - const style = "videoBox-content" + interactive; - return <video className={`${style}`} key="video" autoPlay={this._screenCapture} ref={this.setVideoRef} + return <video className={"videoBox-content" + interactive} key="video" ref={this._videoRef} + autoPlay={this._screenCapture} style={{ width: this._screenCapture ? "100%" : undefined, height: this._screenCapture ? "100%" : undefined }} onCanPlay={this.videoLoad} controls={true} @@ -144,53 +92,114 @@ export class ScreenshotBox extends ViewBoxBaseComponent<FieldViewProps, Screensh toggleRecording = action(async () => { this._screenCapture = !this._screenCapture; - this._videoRef!.srcObject = !this._screenCapture ? undefined : await (navigator.mediaDevices as any).getDisplayMedia({ video: true }); + 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(Utils.prepend(result.accessPaths.agnostic.client)); + } + }; + this._videoRef.current!.srcObject = await (navigator.mediaDevices as any).getDisplayMedia({ video: true }); + this._videoRec = new MediaRecorder(this._videoRef.current!.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) => { + 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(Utils.prepend(result.accessPaths.agnostic.client)); + } else alert("video conversion failed"); + }; + this._audioRec.start(); + this._videoRec.start(); + this.dataDoc.mediaState = "recording"; + DocUtils.ActiveRecordings.push(this); + } else { + this._audioRec.stop(); + this._videoRec.stop(); + this.dataDoc.mediaState = "paused"; + const ind = DocUtils.ActiveRecordings.indexOf(this); + ind !== -1 && (DocUtils.ActiveRecordings.splice(ind, 1)); + } }); - private get uIButtons() { - return (<div className="screenshotBox-uiButtons"> - <div className="screenshotBox-recorder" key="snap" onPointerDown={this.toggleRecording} > - <FontAwesomeIcon icon="file" size="lg" /> - </div>, - <div className="screenshotBox-snapshot" key="snap" onPointerDown={this.onSnapshot} > - <FontAwesomeIcon icon="camera" size="lg" /> - </div> - </div>); - } - - onSnapshot = (e: React.PointerEvent) => { - this.Snapshot(); - e.stopPropagation(); - e.preventDefault(); + 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.content]; + videoPanelHeight = () => NumCast(this.dataDoc[this.fieldKey + "-nativeHeight"], 1) / NumCast(this.dataDoc[this.fieldKey + "-nativeWidth"], 1) * this.props.PanelWidth(); + formattedPanelHeight = () => Math.max(0, this.props.PanelHeight() - this.videoPanelHeight()); render() { - return (<div className="videoBox" onContextMenu={this.specificContextMenu} - style={{ width: `${100}%`, height: `${100}%` }} > + TraceMobx(); + return <div className="videoBox" onContextMenu={this.specificContextMenu} style={{ width: "100%", height: "100%" }} > <div className="videoBox-viewer" > - <CollectionFreeFormView {...OmitKeys(this.props, ["NativeWidth", "NativeHeight"]).omit} - PanelHeight={this.props.PanelHeight} - PanelWidth={this.props.PanelWidth} - focus={this.props.focus} - isSelected={this.props.isSelected} - isAnnotationOverlay={true} - select={emptyFunction} - active={returnFalse} - scaling={returnOne} - whenActiveChanged={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 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} + active={returnFalse} + scaling={returnOne} + whenActiveChanged={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() }}> + <FormattedTextBox {...OmitKeys(this.props, ["NativeWidth", "NativeHeight"]).omit} + Document={this.dataDoc[this.fieldKey + "-dictation"]} + fieldKey={"text"} + PanelHeight={this.formattedPanelHeight} + PanelWidth={this.props.PanelWidth} + focus={this.props.focus} + isSelected={this.props.isSelected} + isAnnotationOverlay={true} + select={emptyFunction} + active={returnFalse} + scaling={returnOne} + xMargin={25} + yMargin={10} + whenActiveChanged={emptyFunction} + removeDocument={returnFalse} + moveDocument={returnFalse} + addDocument={returnFalse} + CollectionView={undefined} + ScreenToLocalTransform={this.props.ScreenToLocalTransform} + renderDepth={this.props.renderDepth + 1} + ContainingCollectionDoc={this.props.ContainingCollectionDoc}> + </FormattedTextBox></div> </div> - {this.props.isSelected() ? this.uIButtons : (null)} - </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 diff --git a/src/client/views/nodes/VideoBox.tsx b/src/client/views/nodes/VideoBox.tsx index 575fbcf2e..efcddd7b3 100644 --- a/src/client/views/nodes/VideoBox.tsx +++ b/src/client/views/nodes/VideoBox.tsx @@ -1,18 +1,18 @@ import React = require("react"); import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { action, computed, IReactionDisposer, observable, reaction, runInAction, untracked, ObservableMap } from "mobx"; +import { action, computed, IReactionDisposer, observable, ObservableMap, reaction, runInAction, untracked } from "mobx"; import { observer } from "mobx-react"; import * as rp from 'request-promise'; -import { Dictionary } from "typescript-collections"; -import { Doc, DocListCast, StrListCast } from "../../../fields/Doc"; +import { Doc, DocListCast } from "../../../fields/Doc"; import { documentSchema } from "../../../fields/documentSchemas"; import { InkTool } from "../../../fields/InkField"; import { makeInterface } from "../../../fields/Schema"; import { Cast, NumCast, StrCast } from "../../../fields/Types"; -import { VideoField } from "../../../fields/URLField"; +import { AudioField, nullAudio, VideoField } from "../../../fields/URLField"; import { emptyFunction, formatTime, OmitKeys, returnOne, setupMoveUpEvents, Utils } from "../../../Utils"; import { Docs, DocUtils } from "../../documents/Documents"; import { Networking } from "../../Network"; +import { CurrentUserUtils } from "../../util/CurrentUserUtils"; import { SelectionManager } from "../../util/SelectionManager"; import { SnappingManager } from "../../util/SnappingManager"; import { CollectionFreeFormView } from "../collections/collectionFreeForm/CollectionFreeFormView"; @@ -24,10 +24,8 @@ import { DocumentDecorations } from "../DocumentDecorations"; import { MarqueeAnnotator } from "../MarqueeAnnotator"; import { StyleProp } from "../StyleProvider"; import { FieldView, FieldViewProps } from './FieldView'; -import { FormattedTextBoxComment } from "./formattedText/FormattedTextBoxComment"; import { LinkDocPreview } from "./LinkDocPreview"; import "./VideoBox.scss"; -import { CurrentUserUtils } from "../../util/CurrentUserUtils"; const path = require('path'); type VideoDocument = makeInterface<[typeof documentSchema]>; @@ -72,8 +70,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps, VideoD getAnchor = () => { const timecode = Cast(this.layoutDoc._currentTimecode, "number", null); - const anchor = CollectionStackedTimeline.createAnchor(this.rootDoc, this.dataDoc, this.annotationKey, "_timecodeToShow"/* videoStart */, "_timecodeToHide" /* videoEnd */, timecode ? timecode : undefined) || this.rootDoc; - return anchor; + return CollectionStackedTimeline.createAnchor(this.rootDoc, this.dataDoc, this.annotationKey, "_timecodeToShow"/* videoStart */, "_timecodeToHide" /* videoEnd */, timecode ? timecode : undefined) || this.rootDoc; } choosePath(url: string) { @@ -85,13 +82,17 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps, VideoD Doc.SetNativeWidth(this.dataDoc, this.player!.videoWidth); Doc.SetNativeHeight(this.dataDoc, this.player!.videoHeight); this.layoutDoc._height = (this.layoutDoc._width || 0) / aspect; - this.dataDoc[this.fieldKey + "-duration"] = this.player!.duration; + if (Number.isFinite(this.player!.duration)) { + this.dataDoc[this.fieldKey + "-duration"] = this.player!.duration; + } } @action public Play = (update: boolean = true) => { this._playing = true; try { + this._audioPlayer && this.player && (this._audioPlayer.currentTime = this.player?.currentTime); update && this.player?.play(); + update && this._audioPlayer?.play(); update && this._youtubePlayer?.playVideo(); this._youtubePlayer && !this._playTimer && (this._playTimer = setInterval(this.updateTimecode, 5)); } catch (e) { @@ -107,12 +108,14 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps, VideoD console.log("Video Seek Exception:", e); } this.player && (this.player.currentTime = time); + this._audioPlayer && (this._audioPlayer.currentTime = time); } @action public Pause = (update: boolean = true) => { this._playing = false; try { update && this.player?.pause(); + update && this._audioPlayer?.pause(); update && this._youtubePlayer?.pauseVideo(); this._youtubePlayer && this._playTimer && clearInterval(this._playTimer); this._youtubePlayer?.seekTo(this._youtubePlayer?.getCurrentTime(), true); @@ -152,8 +155,8 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps, VideoD const b = Docs.Create.LabelDocument({ x: (this.layoutDoc.x || 0) + width, y: (this.layoutDoc.y || 1), _width: 150, _height: 50, title: (this.layoutDoc._currentTimecode || 0).toString(), + _isLinkButton: true }); - b.isLinkButton = true; this.props.addDocument?.(b); DocUtils.MakeLink({ doc: b }, { doc: this.rootDoc }, "video snapshot"); Networking.PostToServer("/youtubeScreenshot", { @@ -179,11 +182,11 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps, VideoD private createRealSummaryLink = (relative: string) => { const url = this.choosePath(Utils.prepend(relative)); - const width = this.layoutDoc._width || 0; + const width = this.layoutDoc._width || 1; const height = this.layoutDoc._height || 0; const imageSummary = Docs.Create.ImageDocument(url, { _nativeWidth: Doc.NativeWidth(this.layoutDoc), _nativeHeight: Doc.NativeHeight(this.layoutDoc), - x: (this.layoutDoc.x || 0) + width, y: (this.layoutDoc.y || 0), isLinkButton: true, + x: (this.layoutDoc.x || 0) + width, y: (this.layoutDoc.y || 0), _isLinkButton: true, _width: 150, _height: height / width * 150, title: "--snapshot" + (this.layoutDoc._currentTimecode || 0) + " image-" }); Doc.SetNativeWidth(Doc.GetProto(imageSummary), Doc.NativeWidth(this.layoutDoc)); @@ -286,12 +289,21 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps, VideoD this._videoRef!.srcObject = !this._screenCapture ? undefined : await (navigator.mediaDevices as any).getDisplayMedia({ video: true }); }), icon: "expand-arrows-alt" }); - subitems.push({ description: (this.layoutDoc.playOnSelect ? "Don't play" : "Play") + " when link is selected", event: () => this.layoutDoc.playOnSelect = !this.layoutDoc.playOnSelect, icon: "expand-arrows-alt" }); - subitems.push({ description: (this.layoutDoc.autoPlay ? "Don't auto play" : "Auto play") + " anchors onClick", event: () => this.layoutDoc.autoPlay = !this.layoutDoc.autoPlay, 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" }); ContextMenu.Instance.addItem({ description: "Options...", subitems: subitems, icon: "video" }); } } + // 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 ?? ""; + } + // ref for updating time + _audioPlayer: HTMLAudioElement | null = null; + setAudioRef = (e: HTMLAudioElement | null) => this._audioPlayer = e; @computed get content() { const field = Cast(this.dataDoc[this.fieldKey], VideoField); const interactive = CurrentUserUtils.SelectedTool !== InkTool.None || !this.props.isSelected() ? "" : "-interactive"; @@ -308,8 +320,13 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps, VideoD onPause={() => this.Pause()} onClick={e => e.preventDefault()}> <source src={field.url.href} type="video/mp4" /> - Not supported. + Not supported. </video> + {!this.audiopath || this.audiopath === field.url.href ? (null) : + <audio ref={this.setAudioRef} className={`audiobox-control${this.active() ? "-interactive" : ""}`}> + <source src={this.audiopath} type="audio/mpeg" /> + Not supported. + </audio>} </div> </div>; } @@ -461,7 +478,9 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps, VideoD } } else if (seekTimeInSeconds <= this.player.duration) { this.player.currentTime = seekTimeInSeconds; + this._audioPlayer && (this._audioPlayer.currentTime = seekTimeInSeconds); this.player.play(); + this._audioPlayer?.play(); runInAction(() => this._playing = true); if (endTime !== this.duration) { this._playRegionTimer = setTimeout(() => this.Pause(), (this._playRegionDuration) * 1000); // use setTimeout to play a specific duration @@ -473,10 +492,10 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps, VideoD } playLink = (doc: Doc) => { - const startTime = this._stackedTimeline.current?.anchorStart(doc) || 0; + const startTime = Math.max(0, (this._stackedTimeline.current?.anchorStart(doc) || 0) - .25); const endTime = this._stackedTimeline.current?.anchorEnd(doc); if (startTime !== undefined) { - if (this.layoutDoc.playOnSelect) endTime ? this.playFrom(startTime, endTime) : this.playFrom(startTime); + if (!this.layoutDoc.dontAutoPlayFollowedLinks) endTime ? this.playFrom(startTime, endTime) : this.playFrom(startTime); else this.Seek(startTime); } } @@ -491,6 +510,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps, VideoD return <div className="videoBox-stackPanel" style={{ transition: this.transition, height: `${100 - this.heightPercent}%` }}> <CollectionStackedTimeline ref={this._stackedTimeline} {...this.props} fieldKey={this.annotationKey} + mediaPath={this.audiopath} renderDepth={this.props.renderDepth + 1} startTag={"_timecodeToShow" /* videoStart */} endTag={"_timecodeToHide" /* videoEnd */} @@ -529,7 +549,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps, VideoD contentFunc = () => [this.youtubeVideoId ? this.youtubeContent : this.content]; scaling = () => this.props.scaling?.() || 1; panelWidth = () => this.props.PanelWidth() * this.heightPercent / 100; - panelHeight = () => this.layoutDoc._fitWidth ? this.panelWidth() / Doc.NativeAspect(this.rootDoc) : this.props.PanelHeight() * 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); @@ -544,7 +564,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps, VideoD style={{ pointerEvents: this.props.layerProvider?.(this.layoutDoc) === false ? "none" : undefined, borderRadius - }} > + }} 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: `${(100 - this.heightPercent) / 2}%` }}> <CollectionFreeFormView {...OmitKeys(this.props, ["NativeWidth", "NativeHeight", "setContentView"]).omit} diff --git a/src/client/views/nodes/WebBox.scss b/src/client/views/nodes/WebBox.scss index cdff4aa74..ca82c049c 100644 --- a/src/client/views/nodes/WebBox.scss +++ b/src/client/views/nodes/WebBox.scss @@ -18,26 +18,6 @@ border-radius: 3px; pointer-events: all; } - .webBox-tagList { - display: flex; - flex-direction: row; - overflow: auto; - flex-flow: row; - flex-wrap: wrap; - .webBox-filterTag, .webBox-filterTag-active { - font-weight: bold; - padding-left: 6; - padding-right: 6; - box-shadow: black 1px 1px 4px; - border-radius: 5; - margin: 2; - height: 20; - background-color: lightgrey; - } - .webBox-filterTag-active { - background-color: white; - } - } .pdfViewerDash-dragAnnotationBox { position: absolute; diff --git a/src/client/views/nodes/WebBox.tsx b/src/client/views/nodes/WebBox.tsx index ed412ad99..3337865a5 100644 --- a/src/client/views/nodes/WebBox.tsx +++ b/src/client/views/nodes/WebBox.tsx @@ -8,19 +8,17 @@ import { Id } from "../../../fields/FieldSymbols"; import { HtmlField } from "../../../fields/HtmlField"; import { InkTool } from "../../../fields/InkField"; import { List } from "../../../fields/List"; -import { listSpec, makeInterface } from "../../../fields/Schema"; +import { makeInterface, listSpec } from "../../../fields/Schema"; import { Cast, NumCast, StrCast } from "../../../fields/Types"; import { WebField } from "../../../fields/URLField"; import { TraceMobx } from "../../../fields/util"; -import { emptyFunction, getWordAtPoint, OmitKeys, returnOne, returnTrue, returnZero, smoothScroll, Utils } from "../../../Utils"; -import { Docs, DocUtils } from "../../documents/Documents"; +import { emptyFunction, getWordAtPoint, OmitKeys, returnOne, smoothScroll, Utils } from "../../../Utils"; +import { Docs } from "../../documents/Documents"; import { DocumentType } from '../../documents/DocumentTypes'; import { CurrentUserUtils } from "../../util/CurrentUserUtils"; import { SnappingManager } from "../../util/SnappingManager"; import { undoBatch } from "../../util/UndoManager"; import { CollectionFreeFormView } from "../collections/collectionFreeForm/CollectionFreeFormView"; -import { CollectionStackingView } from "../collections/CollectionStackingView"; -import { CollectionViewType } from "../collections/CollectionView"; import { ContextMenu } from "../ContextMenu"; import { ContextMenuProps } from "../ContextMenuItem"; import { ViewBoxAnnotatableComponent } from "../DocComponent"; @@ -29,13 +27,12 @@ import { LightboxView } from "../LightboxView"; import { MarqueeAnnotator } from "../MarqueeAnnotator"; import { AnchorMenu } from "../pdf/AnchorMenu"; import { Annotation } from "../pdf/Annotation"; -import { SearchBox } from "../search/SearchBox"; -import { StyleProp } from "../StyleProvider"; +import { SidebarAnnos } from "../SidebarAnnos"; import { FieldView, FieldViewProps } from './FieldView'; -import { FormattedTextBox } from "./formattedText/FormattedTextBox"; import { LinkDocPreview } from "./LinkDocPreview"; import "./WebBox.scss"; import React = require("react"); +const _global = (window /* browser */ || global /* node */) as any; const htmlToText = require("html-to-text"); type WebDocument = makeInterface<[typeof documentSchema]>; @@ -44,34 +41,90 @@ const WebDocument = makeInterface(documentSchema); @observer export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps, WebDocument>(WebDocument) { public static LayoutString(fieldKey: string) { return FieldView.LayoutString(WebBox, fieldKey); } + private _setPreviewCursor: undefined | ((x: number, y: number, drag: boolean) => void); private _mainCont: React.RefObject<HTMLDivElement> = React.createRef(); private _outerRef: React.RefObject<HTMLDivElement> = React.createRef(); private _disposers: { [name: string]: IReactionDisposer } = {}; private _annotationLayer: React.RefObject<HTMLDivElement> = React.createRef(); private _keyInput = React.createRef<HTMLInputElement>(); - @observable _scrollTimer: any; - @observable private _overlayAnnoInfo: Opt<Doc>; private _initialScroll: Opt<number>; - private _setPreviewCursor: undefined | ((x: number, y: number, drag: boolean) => void); + private _sidebarRef = React.createRef<SidebarAnnos>(); + @observable private _scrollTimer: any; + @observable private _overlayAnnoInfo: Opt<Doc>; @observable private _marqueeing: number[] | undefined; @observable private _url: string = "hello"; @observable private _isAnnotating = false; + @observable private _iframeClick: HTMLIFrameElement | undefined = undefined; @observable private _iframe: HTMLIFrameElement | null = null; @observable private _savedAnnotations = new ObservableMap<number, HTMLDivElement[]>(); @observable private _scrollHeight = 1500; @computed get scrollHeight() { return 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; } constructor(props: any) { super(props); - if (this.dataDoc[this.fieldKey] instanceof WebField) { + if (this.webField) { Doc.SetNativeWidth(this.dataDoc, Doc.NativeWidth(this.dataDoc) || 850); Doc.SetNativeHeight(this.dataDoc, Doc.NativeHeight(this.dataDoc) || this.Document[HeightSym]() / this.Document[WidthSym]() * 850); } if (this.layoutDoc[this.fieldKey + "-contentWidth"] === undefined) { this.layoutDoc[this.fieldKey + "-contentWidth"] = Doc.NativeWidth(this.layoutDoc); } - this._annotationKey = "annotations-" + this.urlHash(this._url); + } + + async componentDidMount() { + this.props.setContentView?.(this); // this tells the DocumentView that this AudioBox is the "content" of the document. this allows the DocumentView to indirectly call getAnchor() on the AudioBox when making a link. + + runInAction(() => { + this._url = this.webField?.toString() || ""; + this._annotationKey = "annotations-" + this.urlHash(this._url); + }); + + this._disposers.selection = reaction(() => this.props.isSelected(), + selected => !selected && setTimeout(() => { + Array.from(this._savedAnnotations.values()).forEach(v => v.forEach(a => a.remove())); + this._savedAnnotations.clear(); + })); + + if (this.webField?.href.indexOf("youtube") !== -1) { + const youtubeaspect = 400 / 315; + const nativeWidth = Doc.NativeWidth(this.layoutDoc); + const nativeHeight = Doc.NativeHeight(this.layoutDoc); + if (this.webField) { + if (!nativeWidth || !nativeHeight || Math.abs(nativeWidth / nativeHeight - youtubeaspect) > 0.05) { + if (!nativeWidth) Doc.SetNativeWidth(this.layoutDoc, 600); + Doc.SetNativeHeight(this.layoutDoc, (nativeWidth || 600) / youtubeaspect); + this.layoutDoc._height = this.layoutDoc[WidthSym]() / youtubeaspect; + } + } // else it's an HTMLfield + } else if (this.webField && !this.dataDoc.text) { + const result = await WebRequest.get(Utils.CorsProxy(this.webField.href)); + if (result) { + this.dataDoc.text = htmlToText.fromString(result.content); + } + } + + var quickScroll = true; + this._disposers.scrollReaction = reaction(() => NumCast(this.layoutDoc._scrollTop), + (scrollTop) => { + if (quickScroll) this._initialScroll = scrollTop; + else { + const viewTrans = StrCast(this.Document._viewTransition); + const durationMiliStr = viewTrans.match(/([0-9]*)ms/); + const durationSecStr = viewTrans.match(/([0-9.]*)s/); + const duration = durationMiliStr ? Number(durationMiliStr[1]) : durationSecStr ? Number(durationSecStr[1]) * 1000 : 0; + this.goTo(scrollTop, duration); + } + }, + { fireImmediately: true } + ); + quickScroll = false; + } + componentWillUnmount() { + Object.values(this._disposers).forEach(disposer => disposer?.()); + this._iframe?.removeEventListener('wheel', this.iframeWheel, true); } @action @@ -95,11 +148,35 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps, WebDocum //this._selectionText = selRange.cloneContents().textContent || ""; // clear selection - if (sel.empty) { // Chrome - sel.empty(); - } else if (sel.removeAllRanges) { // Firefox - sel.removeAllRanges(); + if (sel.empty) sel.empty();// Chrome + else if (sel.removeAllRanges) sel.removeAllRanges(); // Firefox + } + + 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 (this._sidebarRef?.current?.makeDocUnfiltered(doc)) return 1; + if (doc !== this.rootDoc && this._outerRef.current) { + const scrollTo = doc.type === DocumentType.TEXTANCHOR ? NumCast(doc.y) : Utils.scrollIntoView(NumCast(doc.y), doc[HeightSym](), NumCast(this.layoutDoc._scrollTop), this.props.PanelHeight() / (this.props.scaling?.() || 1)); + if (scrollTo !== undefined) { + const focusSpeed = smooth ? 500 : 0; + this._initialScroll !== undefined && (this._initialScroll = scrollTo); + this.goTo(scrollTo, focusSpeed); + return focusSpeed; + } } + this._initialScroll = NumCast(doc.y); + return 0; + } + + getAnchor = () => { + const anchor = Docs.Create.TextanchorDocument({ + title: StrCast(this.rootDoc.title + " " + this.layoutDoc._scrollTop), + annotationOn: this.rootDoc, + y: NumCast(this.layoutDoc._scrollTop), + }); + this.addDocument(anchor); + return anchor; } @action @@ -127,6 +204,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps, WebDocum this._iframe?.contentDocument?.addEventListener("pointerup", this.iframeUp); setTimeout(action(() => this._marqueeing = undefined), 100); // bcz: hack .. anchor menu is setup within MarqueeAnnotator so we need to at least create the marqueeAnnotator even though we aren't using it. } else { + this._iframeClick = this._iframe ?? undefined; this._isAnnotating = true; this.props.select(false); e.stopPropagation(); @@ -134,6 +212,12 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps, WebDocum } } + isFirefox = () => { + return "InstallTrigger" in window; // navigator.userAgent.indexOf("Chrome") !== -1; + } + iframeClick = () => this._iframeClick; + iframeScaling = () => 1 / this.props.ScreenToLocalTransform().Scale; + @action iframeLoaded = (e: any) => { const iframe = this._iframe; @@ -151,8 +235,8 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps, WebDocum for (let ele = e.target; ele; ele = ele.parentElement) { href = (typeof (ele.href) === "string" ? ele.href : ele.href?.baseVal) || ele.parentElement?.href || href; } - if (href) { - this.submitURL(href.replace(Utils.prepend(""), Cast(this.dataDoc[this.fieldKey], WebField, null)?.url.origin)); + if (href && this.webField?.origin) { + this.submitURL(href.replace(Utils.prepend(""), this.webField?.origin)); if (this._outerRef.current) { this._outerRef.current.scrollTop = NumCast(this.layoutDoc._scrollTop); this._outerRef.current.scrollLeft = 0; @@ -165,6 +249,13 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps, WebDocum } @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 + } + } + + @action setDashScrollTop = (scrollTop: number, timeout: number = 250) => { const iframeHeight = Math.max(1000, this._scrollHeight - this.panelHeight()); timeout = scrollTop > iframeHeight ? 0 : timeout; @@ -177,92 +268,6 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps, WebDocum } }), timeout); } - @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 - } - } - onWheel = (e: any) => { - e.stopPropagation(); - e.preventDefault(); - } - onScroll = (e: any) => this.setDashScrollTop(this._outerRef.current?.scrollTop || 0); - scrollFocus = (doc: Doc, smooth: boolean) => { - if (doc !== this.rootDoc && this._outerRef.current) { - const scrollTo = doc.type === DocumentType.TEXTANCHOR ? NumCast(doc.y) : Utils.scrollIntoView(NumCast(doc.y), doc[HeightSym](), NumCast(this.layoutDoc._scrollTop), this.props.PanelHeight() / (this.props.scaling?.() || 1)); - if (scrollTo !== undefined) { - const focusSpeed = smooth ? 500 : 0; - this._initialScroll !== undefined && (this._initialScroll = scrollTo); - this.goTo(scrollTo, focusSpeed); - return focusSpeed; - } - } - this._initialScroll = NumCast(doc.y); - return 0; - } - - getAnchor = () => { - const anchor = Docs.Create.TextanchorDocument({ - title: StrCast(this.rootDoc.title + " " + this.layoutDoc._scrollTop), - useLinkSmallAnchor: true, - hideLinkButton: true, - annotationOn: this.rootDoc, - y: NumCast(this.layoutDoc._scrollTop), - }); - this.addDocument(anchor); - return anchor; - } - - async componentDidMount() { - this.props.setContentView?.(this); // this tells the DocumentView that this AudioBox is the "content" of the document. this allows the DocumentView to indirectly call getAnchor() on the AudioBox when making a link. - - const urlField = Cast(this.dataDoc[this.props.fieldKey], WebField); - runInAction(() => this._url = urlField?.url.toString() || ""); - - this._disposers.selection = reaction(() => this.props.isSelected(), - selected => !selected && setTimeout(() => { - Array.from(this._savedAnnotations.values()).forEach(v => v.forEach(a => a.remove())); - this._savedAnnotations.clear(); - })); - - const field = Cast(this.rootDoc[this.props.fieldKey], WebField); - if (field?.url.href.indexOf("youtube") !== -1) { - const youtubeaspect = 400 / 315; - const nativeWidth = Doc.NativeWidth(this.layoutDoc); - const nativeHeight = Doc.NativeHeight(this.layoutDoc); - if (field) { - if (!nativeWidth || !nativeHeight || Math.abs(nativeWidth / nativeHeight - youtubeaspect) > 0.05) { - if (!nativeWidth) Doc.SetNativeWidth(this.layoutDoc, 600); - Doc.SetNativeHeight(this.layoutDoc, (nativeWidth || 600) / youtubeaspect); - this.layoutDoc._height = this.layoutDoc[WidthSym]() / youtubeaspect; - } - } // else it's an HTMLfield - } else if (field?.url && !this.dataDoc.text) { - const result = await WebRequest.get(Utils.CorsProxy(field.url.href)); - if (result) { - this.dataDoc.text = htmlToText.fromString(result.content); - } - } - - var quickScroll = true; - this._disposers.scrollReaction = reaction(() => NumCast(this.layoutDoc._scrollTop), - (scrollTop) => { - if (quickScroll) { - this._initialScroll = scrollTop; - } - else { - const viewTrans = StrCast(this.Document._viewTransition); - const durationMiliStr = viewTrans.match(/([0-9]*)ms/); - const durationSecStr = viewTrans.match(/([0-9.]*)s/); - const duration = durationMiliStr ? Number(durationMiliStr[1]) : durationSecStr ? Number(durationSecStr[1]) * 1000 : 0; - this.goTo(scrollTop, duration); - } - }, - { fireImmediately: true } - ); - quickScroll = false; - } goTo = (scrollTop: number, duration: number) => { if (this._outerRef.current) { @@ -277,15 +282,10 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps, WebDocum } } - componentWillUnmount() { - Object.values(this._disposers).forEach(disposer => disposer?.()); - this._iframe?.removeEventListener('wheel', this.iframeWheel, true); - } - @action forward = () => { - const future = Cast(this.dataDoc[this.fieldKey + "-future"], listSpec("string"), null); - const history = Cast(this.dataDoc[this.fieldKey + "-history"], listSpec("string"), null); + const future = Cast(this.dataDoc[this.fieldKey + "-future"], listSpec("string"), []); + const history = Cast(this.dataDoc[this.fieldKey + "-history"], listSpec("string"), []); if (future.length) { history.push(this._url); this.dataDoc[this.fieldKey] = new WebField(new URL(this._url = future.pop()!)); @@ -297,8 +297,8 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps, WebDocum @action back = () => { - const future = Cast(this.dataDoc[this.fieldKey + "-future"], listSpec("string"), null); - const history = Cast(this.dataDoc[this.fieldKey + "-history"], listSpec("string"), null); + const future = Cast(this.dataDoc[this.fieldKey + "-future"], listSpec("string")); + const history = Cast(this.dataDoc[this.fieldKey + "-history"], listSpec("string"), []); if (history.length) { if (future === undefined) this.dataDoc[this.fieldKey + "-future"] = new List<string>([this._url]); else future.push(this._url); @@ -309,17 +309,18 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps, WebDocum return false; } - urlHash(s: string) { + urlHash = (s: string) => { return s.split('').reduce((a: any, b: any) => { a = ((a << 5) - a) + b.charCodeAt(0); return a & a; }, 0); } @action - submitURL = (newUrl: string) => { + submitURL = (newUrl?: string) => { + if (!newUrl) return; if (!newUrl.startsWith("http")) newUrl = "http://" + newUrl; try { - const future = Cast(this.dataDoc[this.fieldKey + "-future"], listSpec("string"), null); - const history = Cast(this.dataDoc[this.fieldKey + "-history"], listSpec("string"), null); - const url = Cast(this.dataDoc[this.fieldKey], WebField, null)?.url.toString(); + 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) { if (history === undefined) { this.dataDoc[this.fieldKey + "-history"] = new List<string>([url]); @@ -338,7 +339,6 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps, WebDocum return true; } - menuControls = () => this.urlEditor; onWebUrlDrop = (e: React.DragEvent) => { const { dataTransfer } = e; const html = dataTransfer.getData("text/html"); @@ -373,12 +373,8 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps, WebDocum <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"></FontAwesomeIcon> - </button> - <button className="submitUrl" onClick={this.forward}> - <FontAwesomeIcon icon="caret-right" size="lg"></FontAwesomeIcon> - </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> ); @@ -393,6 +389,20 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps, WebDocum cm.addItem({ description: "Options...", subitems: funcs, icon: "asterisk" }); } + @action + onMarqueeDown = (e: React.PointerEvent) => { + if (!e.altKey && e.button === 0 && this.active(true)) { + this._marqueeing = [e.clientX, e.clientY]; + this.props.select(false); + } + } + @action finishMarquee = (x?: number, y?: number) => { + this._marqueeing = undefined; + this._isAnnotating = false; + this._iframeClick = undefined; + x !== undefined && y !== undefined && this._setPreviewCursor?.(x, y, false); + } + @computed get urlContent() { const field = this.dataDoc[this.props.fieldKey]; @@ -401,7 +411,6 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps, WebDocum view = <span className="webBox-htmlSpan" dangerouslySetInnerHTML={{ __html: field.html }} />; } else if (field instanceof WebField) { const url = this.layoutDoc.useCors ? Utils.CorsProxy(field.url.href) : field.url.href; - // view = <iframe className="webBox-iframe" src={url} onLoad={e => { e.currentTarget.before((e.currentTarget.contentDocument?.body || e.currentTarget.contentDocument)?.children[0]!); e.currentTarget.remove(); }} 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} @@ -416,101 +425,27 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps, WebDocum return view; } - anchorMenuClick = (anchor: Doc) => { - this.Document._showSidebar = true; - const startup = StrListCast(this.rootDoc.docFilters).map(filter => filter.split(":")[0]).join(" "); - const target = Docs.Create.TextDocument(startup, { - title: "anno", - annotationOn: this.rootDoc, _width: 200, _height: 50, _fitWidth: true, _autoHeight: true, _fontSize: StrCast(Doc.UserDoc().fontSize), - _fontFamily: StrCast(Doc.UserDoc().fontFamily) - }); - FormattedTextBox.SelectOnLoad = target[Id]; - FormattedTextBox.DontSelectInitialText = true; - this.allTags.map(tag => target[tag] = tag); - DocUtils.MakeLink({ doc: anchor }, { doc: target }, "inline markup", "annotation"); - this.sidebarAddDocument(target); - } - toggleSidebar = () => { - if (this.layoutDoc.nativeWidth === this.layoutDoc[this.fieldKey + "-nativeWidth"]) { - this.layoutDoc.nativeWidth = 250 + NumCast(this.layoutDoc[this.fieldKey + "-nativeWidth"]); - } else { - this.layoutDoc.nativeWidth = NumCast(this.layoutDoc[this.fieldKey + "-nativeWidth"]); - } - this.layoutDoc._width = NumCast(this.layoutDoc._nativeWidth) * (NumCast(this.layoutDoc[this.fieldKey + "-nativeWidth"]) / NumCast(this.layoutDoc[this.fieldKey + "-nativeHeight"])); - } - sidebarKey = () => this.fieldKey + "-sidebar"; - sidebarFiltersHeight = () => 50; - sidebarTransform = () => this.props.ScreenToLocalTransform().translate(Doc.NativeWidth(this.dataDoc), 0).scale(this.props.scaling?.() || 1); + sidebarAddDocument = (doc: Doc | Doc[], sidebarKey?: string) => { + if (!this.layoutDoc._showSidebar) this.toggleSidebar(); + return this.addDocument(doc, sidebarKey); + } + toggleSidebar = action(() => { + const nativeWidth = NumCast(this.layoutDoc[this.fieldKey + "-nativeWidth"]); + const ratio = ((!this.layoutDoc.nativeWidth || this.layoutDoc.nativeWidth === nativeWidth ? 250 : 0) + nativeWidth) / nativeWidth; + const curNativeWidth = NumCast(this.layoutDoc.nativeWidth, nativeWidth); + this.layoutDoc.nativeWidth = nativeWidth * ratio; + this.layoutDoc._width = this.layoutDoc[WidthSym]() * nativeWidth * ratio / curNativeWidth; + this.layoutDoc._showSidebar = nativeWidth !== this.layoutDoc._nativeWidth; + }); sidebarWidth = () => !this.layoutDoc._showSidebar ? 0 : (NumCast(this.layoutDoc.nativeWidth) - Doc.NativeWidth(this.dataDoc)) * this.props.PanelWidth() / NumCast(this.layoutDoc.nativeWidth); - sidebarHeight = () => this.props.PanelHeight() - this.sidebarFiltersHeight() - 20; - sidebarAddDocument = (doc: Doc | Doc[]) => this.addDocument(doc, this.sidebarKey()); - sidebarMoveDocument = (doc: Doc | Doc[], targetCollection: Doc | undefined, addDocument: (doc: Doc | Doc[]) => boolean) => this.moveDocument(doc, targetCollection, addDocument, this.sidebarKey()); - sidebarRemDocument = (doc: Doc | Doc[]) => this.removeDocument(doc, this.sidebarKey()); - sidebarDocFilters = () => [...StrListCast(this.layoutDoc._docFilters), ...StrListCast(this.layoutDoc[this.sidebarKey() + "-docFilters"])]; - @computed get allTags() { - const keys = new Set<string>(); - DocListCast(this.rootDoc[this.sidebarKey()]).forEach(doc => SearchBox.documentKeys(doc).forEach(key => keys.add(key))); - return Array.from(keys.keys()).filter(key => key[0]).filter(key => !key.startsWith("_") && (key[0] === "#" || key[0] === key[0].toUpperCase())).sort(); - } - renderTag = (tag: string) => { - const active = StrListCast(this.rootDoc[this.sidebarKey() + "-docFilters"]).includes(`${tag}:${tag}:check`); - return <div className={`webBox-filterTag${active ? "-active" : ""}`} - onClick={e => Doc.setDocFilter(this.rootDoc, tag, tag, "check", true, this.sidebarKey())}> - {tag} - </div>; - } - @computed get sidebarOverlay() { - return !this.layoutDoc._showSidebar ? (null) : - <div style={{ - position: "absolute", pointerEvents: this.active() ? "all" : undefined, top: 0, right: 0, - background: this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.WidgetColor), - width: `${this.sidebarWidth()}px`, - height: "100%" - }}> - <div style={{ width: "100%", height: this.sidebarHeight(), position: "relative" }}> - <CollectionStackingView {...OmitKeys(this.props, ["NativeWidth", "NativeHeight", "setContentView"]).omit} - NativeWidth={returnZero} - NativeHeight={returnZero} - PanelHeight={this.sidebarHeight} - PanelWidth={this.sidebarWidth} - xMargin={0} - yMargin={0} - docFilters={this.sidebarDocFilters} - chromeStatus={"enabled"} - scaleField={this.sidebarKey() + "-scale"} - isAnnotationOverlay={false} - select={emptyFunction} - active={this.annotationsActive} - scaling={returnOne} - whenActiveChanged={this.whenActiveChanged} - childHideDecorationTitle={returnTrue} - removeDocument={this.sidebarRemDocument} - moveDocument={this.sidebarMoveDocument} - addDocument={this.sidebarAddDocument} - CollectionView={undefined} - ScreenToLocalTransform={this.sidebarTransform} - renderDepth={this.props.renderDepth + 1} - viewType={CollectionViewType.Stacking} - fieldKey={this.sidebarKey()} - pointerEvents={"all"} - /> - </div> - <div className="webBox-tagList" style={{ height: this.sidebarFiltersHeight(), width: this.sidebarWidth() }}> - {this.allTags.map(tag => this.renderTag(tag))} - </div> - </div>; - } - @computed - get content() { - return <div className={"webBox-cont" + (this.active() && CurrentUserUtils.SelectedTool === InkTool.None && !DocumentDecorations.Instance?.Interacting ? "-interactive" : "")} + @computed get content() { + return <div className={"webBox-cont" + (!this.props.docViewPath().lastElement()?.docView?._pendingDoubleClick && this.active() && CurrentUserUtils.SelectedTool === InkTool.None && !DocumentDecorations.Instance?.Interacting ? "-interactive" : "")} style={{ width: NumCast(this.layoutDoc[this.fieldKey + "-contentWidth"]) || `${100 / (this.props.scaling?.() || 1)}%`, }}> {this.urlContent} </div>; } - showInfo = action((anno: Opt<Doc>) => this._overlayAnnoInfo = anno); - @computed get allAnnotations() { return DocListCast(this.dataDoc[this.annotationKey]); } @computed get annotationLayer() { TraceMobx(); return <div className="webBox-annotationLayer" style={{ height: Doc.NativeHeight(this.Document) || undefined }} ref={this._annotationLayer}> @@ -520,20 +455,8 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps, WebDocum </div>; } - @action - onMarqueeDown = (e: React.PointerEvent) => { - if (!e.altKey && e.button === 0 && this.active(true)) { - this._marqueeing = [e.clientX, e.clientY]; - this.props.select(false); - } - } + showInfo = action((anno: Opt<Doc>) => this._overlayAnnoInfo = anno); setPreviewCursor = (func?: (x: number, y: number, drag: boolean) => void) => this._setPreviewCursor = func; - @action finishMarquee = (x?: number, y?: number) => { - this._marqueeing = undefined; - this._isAnnotating = false; - x !== undefined && y !== undefined && this._setPreviewCursor?.(x, y, false); - } - panelWidth = () => this.props.PanelWidth() / (this.props.scaling?.() || 1) - this.sidebarWidth(); // (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)); @@ -541,7 +464,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps, WebDocum const inactiveLayer = this.props.layerProvider?.(this.layoutDoc) === false; const scale = this.props.scaling?.() || 1; return ( - <div className="webBox" ref={this._mainCont} style={{ pointerEvents: this.active() || SnappingManager.GetIsDragging() ? undefined : "none" }} > + <div className="webBox" ref={this._mainCont} style={{ pointerEvents: this.annotationsActive() ? "all" : this.active() || SnappingManager.GetIsDragging() ? undefined : "none" }} > <div className={`webBox-container`} style={{ pointerEvents: inactiveLayer ? "none" : undefined }} onContextMenu={this.specificContextMenu}> @@ -553,8 +476,8 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps, WebDocum transform: `scale(${scale})`, pointerEvents: inactiveLayer ? "none" : undefined }} - onWheel={this.onWheel} - onScroll={this.onScroll} + 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={{ @@ -563,28 +486,34 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps, WebDocum }}> {this.content} <CollectionFreeFormView {...OmitKeys(this.props, ["NativeWidth", "NativeHeight", "setContentView"]).omit} - renderDepth={this.props.renderDepth + 1} - CollectionView={undefined} - fieldKey={this.annotationKey} isAnnotationOverlay={true} - scaling={returnOne} - pointerEvents={this._isAnnotating || SnappingManager.GetIsDragging() ? "all" : "none"} + fieldKey={this.annotationKey} + setPreviewCursor={this.setPreviewCursor} PanelWidth={this.panelWidth} PanelHeight={this.panelHeight} - ScreenToLocalTransform={this.scrollXf} - setPreviewCursor={this.setPreviewCursor} - removeDocument={this.removeDocument} - moveDocument={this.moveDocument} - addDocument={this.addDocument} + dropAction={"alias"} select={emptyFunction} active={this.active} - whenActiveChanged={this.whenActiveChanged} /> + ContentScaling={returnOne} + bringToFront={emptyFunction} + whenActiveChanged={this.whenActiveChanged} + removeDocument={this.removeDocument} + moveDocument={this.moveDocument} + addDocument={this.sidebarAddDocument} + CollectionView={undefined} + ScreenToLocalTransform={this.scrollXf} + renderDepth={this.props.renderDepth + 1} + scaling={returnOne} + childPointerEvents={true} + pointerEvents={this._isAnnotating || SnappingManager.GetIsDragging() ? "all" : "none"} /> {this.annotationLayer} </div> </div> {!this._marqueeing || !this._mainCont.current || !this._annotationLayer.current ? (null) : <MarqueeAnnotator rootDoc={this.rootDoc} - anchorMenuClick={this.anchorMenuClick} + iframe={this.isFirefox() ? this.iframeClick : undefined} + iframeScaling={this.isFirefox() ? this.iframeScaling : undefined} + anchorMenuClick={this._sidebarRef.current?.anchorMenuClick} scrollTop={0} down={this._marqueeing} scaling={returnOne} addDocument={this.addDocument} @@ -594,11 +523,23 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps, WebDocum annotationLayer={this._annotationLayer.current} mainCont={this._mainCont.current} />} </div > - <button className="webBox-overlayButton-sidebar" key="sidebar" title="Toggle Sidebar" style={{ right: this.sidebarWidth() + 7 }} + <button className="webBox-overlayButton-sidebar" key="sidebar" title="Toggle Sidebar" + style={{ right: this.sidebarWidth() + 7, display: !this.active() ? "none" : undefined }} onPointerDown={e => e.stopPropagation()} onClick={e => this.toggleSidebar()} > <FontAwesomeIcon style={{ color: "white" }} icon={"chevron-left"} size="sm" /> </button> - {this.sidebarOverlay} + <SidebarAnnos ref={this._sidebarRef} + {...this.props} + fieldKey={this.annotationKey} + annotationsActive={this.annotationsActive} + rootDoc={this.rootDoc} + layoutDoc={this.layoutDoc} + dataDoc={this.dataDoc} + sidebarAddDocument={this.sidebarAddDocument} + moveDocument={this.moveDocument} + removeDocument={this.removeDocument} + active={this.active} + /> </div>); } }
\ No newline at end of file diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx index d64db1ee1..abfc63b40 100644 --- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx +++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx @@ -61,7 +61,6 @@ import { RichTextRules } from "./RichTextRules"; import { schema } from "./schema_rts"; import { SummaryView } from "./SummaryView"; import applyDevTools = require("prosemirror-dev-tools"); - import React = require("react"); const translateGoogleApi = require("translate-google-api"); @@ -113,7 +112,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp private _downEvent: any; private _downX = 0; private _downY = 0; - private _break = false; + private _break = true; public ProseRef?: HTMLDivElement; public get EditorView() { return this._editorView; } public get SidebarKey() { return this.fieldKey + "-sidebar"; } @@ -125,8 +124,10 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp @computed get scrollHeight() { return NumCast(this.rootDoc[this.fieldKey + "-scrollHeight"]); } @computed get sidebarHeight() { return NumCast(this.rootDoc[this.SidebarKey + "-height"]); } @computed get titleHeight() { return this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.HeaderMargin) || 0; } - @computed get _recording() { return this.dataDoc?.audioState === "recording"; } - set _recording(value) { this.dataDoc.audioState = value ? "recording" : undefined; } + @computed get _recording() { return this.dataDoc?.mediaState === "recording"; } + set _recording(value) { + !this.dataDoc.recordingSource && (this.dataDoc.mediaState = value ? "recording" : undefined); + } @computed get config() { this._keymap = buildKeymap(schema, this.props); this._rules = new RichTextRules(this.props.Document, this); @@ -250,6 +251,18 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp json?.replace(/"selection":.*/, "") : json?.replace(/"selection":"\"storedMarks\""/, "\"storedMarks\""); if (effectiveAcl === AclEdit || effectiveAcl === AclAdmin) { + const accumTags = [] as string[]; + state.tr.doc.nodesBetween(0, state.doc.content.size, (node: any, pos: number, parent: any) => { + if (node.type === schema.nodes.dashField && node.attrs.fieldKey.startsWith("#")) { + accumTags.push(node.attrs.fieldKey); + } + }); + const curTags = Object.keys(this.dataDoc).filter(key => key.startsWith("#")); + const added = accumTags.filter(tag => !curTags.includes(tag)); + const removed = curTags.filter(tag => !accumTags.includes(tag)); + removed.forEach(r => this.dataDoc[r] = undefined); + added.forEach(a => this.dataDoc[a] = a); + let unchanged = true; if (this._applyingChange !== this.fieldKey && removeSelection(json) !== removeSelection(curProto?.Data)) { this._applyingChange = this.fieldKey; @@ -291,11 +304,13 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp insertTime = () => { let linkTime; let linkAnchor; + let link; DocListCast(this.dataDoc.links).forEach((l, i) => { const anchor = (l.anchor1 as Doc).annotationOn ? l.anchor1 as Doc : (l.anchor2 as Doc).annotationOn ? (l.anchor2 as Doc) : undefined; - if (anchor && (anchor.annotationOn as Doc).audioState === "recording") { + if (anchor && (anchor.annotationOn as Doc).mediaState === "recording") { linkTime = NumCast(anchor._timecodeToShow /* audioStart */); linkAnchor = anchor; + link = l; } }); if (this._editorView && linkTime) { @@ -622,6 +637,19 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp this._downX = this._downY = Number.NaN; } + breakupDictation = () => { + if (this._editorView && this._recording) { + this.stopDictation(true); + this._break = true; + const state = this._editorView.state; + const to = state.selection.to; + const updated = TextSelection.create(state.doc, to, to); + this._editorView.dispatch(state.tr.setSelection(updated).insertText("\n", to)); + if (this._recording) { + this.recordDictation(); + } + } + } recordDictation = () => { DictationManager.Controls.listen({ interimHandler: this.setDictationContent, @@ -635,28 +663,30 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp stopDictation = (abort: boolean) => DictationManager.Controls.stop(!abort); setDictationContent = (value: string) => { - if (this._editorView) { - const state = this._editorView.state; - const now = Date.now(); - let mark = schema.marks.user_mark.create({ userid: Doc.CurrentUserEmail, modified: Math.floor(now / 1000) }); - if (!this._break && state.selection.to !== state.selection.from) { - for (let i = state.selection.from; i <= state.selection.to; i++) { - const pos = state.doc.resolve(i); - const um = Array.from(pos.marks()).find(m => m.type === schema.marks.user_mark); - if (um) { - mark = um; - break; - } - } + if (this._editorView && this._recordingStart) { + if (this._break) { + const textanchor = Docs.Create.TextanchorDocument({ title: "dictation anchor" }); + this.addDocument(textanchor); + const link = DocUtils.MakeLinkToActiveAudio(textanchor, false).lastElement(); + link && (Doc.GetProto(link).isDictation = true); + if (!link) return; + const audioanchor = Cast(link.anchor2, Doc, null); + if (!audioanchor) return; + audioanchor.backgroundColor = "tan"; + const audiotag = this._editorView.state.schema.nodes.audiotag.create({ + timeCode: NumCast(audioanchor._timecodeToShow), + audioId: audioanchor[Id], + textId: textanchor[Id] + }); + Doc.GetProto(textanchor).title = "dictation:" + audiotag.attrs.timeCode; + const tr = this._editorView.state.tr.insert(this._editorView.state.doc.content.size, audiotag); + const tr2 = tr.setSelection(TextSelection.create(tr.doc, tr.doc.content.size)); + this._editorView.dispatch(tr.setSelection(TextSelection.create(tr2.doc, tr2.doc.content.size))); } - const from = state.selection.from; + const from = this._editorView.state.selection.from; this._break = false; - if (this.props.Document.recordingStart) { - const recordingStart = DateCast(this.props.Document.recordingStart)?.date.getTime(); - value = "" + (mark.attrs.modified * 1000 - recordingStart) / 1000 + value; - } - const tr = state.tr.insertText(value).addMark(from, from + value.length + 1, mark); - this._editorView.dispatch(tr.setSelection(TextSelection.create(tr.doc, from, from + value.length + 1))); + const tr = this._editorView.state.tr.insertText(value); + this._editorView.dispatch(tr.setSelection(TextSelection.create(tr.doc, from, tr.doc.content.size)).scrollIntoView()); } } @@ -667,7 +697,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp const splitter = state.schema.marks.splitter.create({ id: Utils.GenerateGuid() }); let tr = state.tr.addMark(sel.from, sel.to, splitter); if (sel.from !== sel.to) { - const anchor = anchorDoc ?? Docs.Create.TextanchorDocument(); + const anchor = anchorDoc ?? Docs.Create.TextanchorDocument({ title: this._editorView?.state.doc.textBetween(sel.from, sel.to) }); const href = targetHref ?? Utils.prepend("/doc/" + anchor[Id]); if (anchor !== anchorDoc) this.addDocument(anchor); tr.doc.nodesBetween(sel.from, sel.to, (node: any, pos: number, parent: any) => { @@ -681,7 +711,6 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp this.dataDoc[ForceServerWrite] = this.dataDoc[UpdatingFromServer] = true; // need to allow permissions for adding links to readonly/augment only documents this._editorView!.dispatch(tr.removeMark(sel.from, sel.to, splitter)); this.dataDoc[UpdatingFromServer] = this.dataDoc[ForceServerWrite] = false; - Doc.GetProto(anchor).title = this._editorView?.state.doc.textBetween(sel.from, sel.to); return anchor; } return anchorDoc ?? this.rootDoc; @@ -689,14 +718,14 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp return anchorDoc ?? this.rootDoc; } - scrollFocus = (doc: Doc, smooth: boolean) => { - const anchorId = doc[Id]; + scrollFocus = (textAnchor: Doc, smooth: boolean) => { + const textAnchorId = textAnchor[Id]; const findAnchorFrag = (frag: Fragment, editor: EditorView) => { const nodes: Node[] = []; let hadStart = start !== 0; frag.forEach((node, index) => { const examinedNode = findAnchorNode(node, editor); - if (examinedNode?.node.textContent) { + if (examinedNode?.node && (examinedNode.node.textContent || examinedNode.node.type === this._editorView?.state.schema.nodes.audiotag)) { nodes.push(examinedNode.node); !hadStart && (start = index + examinedNode.start); hadStart = true; @@ -705,31 +734,38 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp return { frag: Fragment.fromArray(nodes), start }; }; const findAnchorNode = (node: Node, editor: EditorView) => { + if (node.type === this._editorView?.state.schema.nodes.audiotag) { + if (node.attrs.textId === textAnchorId) { + return { node, start: 0 }; + } + return undefined; + } if (!node.isText) { const content = findAnchorFrag(node.content, editor); return { node: node.copy(content.frag), start: content.start }; } const marks = [...node.marks]; const linkIndex = marks.findIndex(mark => mark.type === editor.state.schema.marks.linkAnchor); - return linkIndex !== -1 && marks[linkIndex].attrs.allAnchors.find((item: { href: string }) => anchorId === item.href.replace(/.*\/doc\//, "")) ? { node, start: 0 } : undefined; + return linkIndex !== -1 && marks[linkIndex].attrs.allAnchors.find((item: { href: string }) => textAnchorId === item.href.replace(/.*\/doc\//, "")) ? { node, start: 0 } : undefined; }; let start = 0; - if (this._editorView && anchorId) { + if (this._editorView && textAnchorId) { const editor = this._editorView; const ret = findAnchorFrag(editor.state.doc.content, editor); - if (ret.frag.size > 2 && ret.start >= 0) { + const content = (ret.frag as any)?.content; + if ((ret.frag.size > 2 || (content?.length && content[0].type === this._editorView.state.schema.nodes.audiotag)) && ret.start >= 0) { smooth && (this._focusSpeed = 500); let selection = TextSelection.near(editor.state.doc.resolve(ret.start)); // default to near the start if (ret.frag.firstChild) { selection = TextSelection.between(editor.state.doc.resolve(ret.start), editor.state.doc.resolve(ret.start + ret.frag.firstChild.nodeSize)); // bcz: looks better to not have the target selected } editor.dispatch(editor.state.tr.setSelection(new TextSelection(selection.$from, selection.$from)).scrollIntoView()); - const escAnchorId = anchorId[0] >= '0' && anchorId[0] <= '9' ? `\\3${anchorId[0]} ${anchorId.substr(1)}` : anchorId; - addStyleSheetRule(FormattedTextBox._highlightStyleSheet, `${escAnchorId}`, { background: "yellow" }); + const escAnchorId = textAnchorId[0] >= '0' && textAnchorId[0] <= '9' ? `\\3${textAnchorId[0]} ${textAnchorId.substr(1)}` : textAnchorId; + addStyleSheetRule(FormattedTextBox._highlightStyleSheet, `${escAnchorId}`, { background: "yellow", transform: "scale(3)", "transform-origin": "left bottom" }); setTimeout(() => this._focusSpeed = undefined, this._focusSpeed); - setTimeout(() => clearStyleSheetRules(FormattedTextBox._highlightStyleSheet), Math.max(this._focusSpeed || 0, 1500)); + setTimeout(() => clearStyleSheetRules(FormattedTextBox._highlightStyleSheet), Math.max(this._focusSpeed || 0, 3000)); } } @@ -748,9 +784,10 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp this.props.setContentView?.(this); // this tells the DocumentView that this AudioBox is the "content" of the document. this allows the DocumentView to indirectly call getAnchor() on the AudioBox when making a link. this.props.contentsActive?.(this.active); this._cachedLinks = DocListCast(this.Document.links); + this._disposers.breakupDictation = reaction(() => DocumentManager.Instance.RecordingEvent, this.breakupDictation); this._disposers.autoHeight = reaction(() => this.autoHeight, autoHeight => autoHeight && this.tryUpdateScrollHeight()); - this._disposers.autoHeight = reaction(() => ({ scrollHeight: this.scrollHeight, width: NumCast(this.layoutDoc._width) }), - ({ width, scrollHeight }) => width && this.autoHeight && this.resetNativeHeight(scrollHeight) + this._disposers.scrollHeight = reaction(() => ({ scrollHeight: this.scrollHeight, autoHeight: this.autoHeight, width: NumCast(this.layoutDoc._width) }), + ({ width, scrollHeight, autoHeight }) => width && autoHeight && this.resetNativeHeight(scrollHeight) ); this._disposers.componentHeights = reaction( // set the document height when one of the component heights changes and autoHeight is on () => ({ sidebarHeight: this.sidebarHeight, textHeight: this.textHeight, autoHeight: this.autoHeight }), @@ -817,7 +854,6 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp this._disposers.selected = reaction(() => this.props.isSelected(), action((selected) => { - this._recording = false; if (RichTextMenu.Instance?.view === this._editorView && !selected) { RichTextMenu.Instance?.updateMenu(undefined, undefined, undefined); } @@ -826,14 +862,13 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp if (!this.props.dontRegisterView) { this._disposers.record = reaction(() => this._recording, () => { + this.stopDictation(true); if (this._recording) { - setTimeout(action(() => { - this.stopDictation(true); - setTimeout(() => this.recordDictation(), 500); - }), 500); - } else setTimeout(() => this.stopDictation(true), 0); - } + this.recordDictation(); + } + }, ); + if (this._recording) setTimeout(() => this.recordDictation()); } var quickScroll: string | undefined = ""; this._disposers.scroll = reaction(() => NumCast(this.layoutDoc._scrollTop), @@ -1034,11 +1069,11 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp this._editorView = new EditorView(this.ProseRef, { state: rtfField?.Data ? EditorState.fromJSON(config, JSON.parse(rtfField.Data)) : EditorState.create(config), handleScrollToSelection: (editorView) => { - const docPos = editorView.coordsAtPos(editorView.state.selection.from); + const docPos = editorView.coordsAtPos(editorView.state.selection.to); const viewRect = self._ref.current!.getBoundingClientRect(); const scrollRef = self._scrollRef.current; - if ((docPos.top < viewRect.top || docPos.top > viewRect.bottom) && scrollRef) { - const scrollPos = scrollRef.scrollTop + (docPos.top - viewRect.top) * self.props.ScreenToLocalTransform().Scale; + if ((docPos.bottom < viewRect.top || docPos.bottom > viewRect.bottom) && scrollRef) { + const scrollPos = scrollRef.scrollTop + (docPos.bottom - viewRect.top) * self.props.ScreenToLocalTransform().Scale; if (this._focusSpeed !== undefined) { scrollPos && smoothScroll(this._focusSpeed, scrollRef, scrollPos); } else { @@ -1107,26 +1142,26 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp if ((e.target as any).tagName === "AUDIOTAG") { e.preventDefault(); e.stopPropagation(); - const time = (e.target as any)?.dataset?.timecode || 0; - const audioid = (e.target as any)?.dataset?.audioid || 0; - DocServer.GetRefField(audioid).then(anchor => { + DocServer.GetRefField((e.target as any)?.dataset?.audioid || 0).then(anchor => { if (anchor instanceof Doc) { + const timecode = NumCast(anchor.timecodeToShow, 0); const audiodoc = anchor.annotationOn as Doc; - audiodoc._triggerAudio = Number(time); - !DocumentManager.Instance.getDocumentView(audiodoc) && this.props.addDocTab(audiodoc, "add:bottom"); + const func = () => { + const docView = DocumentManager.Instance.getDocumentView(audiodoc); + if (!docView) { + this.props.addDocTab(audiodoc, "add:bottom"); + setTimeout(func); + } + else docView.ComponentView?.playFrom?.(timecode, Cast(anchor.timecodeToHide, "number", null)); // bcz: would be nice to find the next audio tag in the doc and play until that + }; + func(); } }); } if (this._recording && !e.ctrlKey && e.button === 0) { - this.stopDictation(true); - this._break = true; - const state = this._editorView!.state; - const to = state.selection.to; - const updated = TextSelection.create(state.doc, to, to); - this._editorView!.dispatch(this._editorView!.state.tr.setSelection(updated).insertText("\n", to)); + this.breakupDictation(); e.preventDefault(); e.stopPropagation(); - if (this._recording) setTimeout(() => this.recordDictation(), 500); } this._downX = e.clientX; this._downY = e.clientY; @@ -1239,10 +1274,10 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp e.stopPropagation(); return; } - this.props.isSelected(true) && ((e.nativeEvent as any).formattedHandled = true); - if (this.props.isSelected(true)) { // if text box is selected, then it consumes all click events - // e.stopPropagation(); // bcz: not sure why this was here. We need to allow the DocumentView to get clicks to process doubleClicks + (e.nativeEvent as any).formattedHandled = true; + if (this.ProseRef?.children[0] !== e.nativeEvent.target) e.stopPropagation(); // if you double click on text, then it will be selected instead of sending a double click to DocumentView & opening a lightbox. Also,if a text box has isLinkButton, this will prevent link following if you've selected the document to edit it. + // e.stopPropagation(); // bcz: not sure why this was here. We need to allow the DocumentView to get clicks to process doubleClicks (see above comment) this.hitBulletTargets(e.clientX, e.clientY, !this._editorView?.state.selection.empty || this._forceUncollapse, false, this._forceDownNode, e.shiftKey); } this._forceUncollapse = !(this._editorView!.root as any).getSelection().isCollapsed; @@ -1383,14 +1418,16 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp } tryUpdateScrollHeight() { if (!LightboxView.LightboxDoc || LightboxView.IsLightboxDocView(this.props.docViewPath())) { - const proseHeight = this.ProseRef?.scrollHeight || 0; - const scrollHeight = this.ProseRef && Math.min(NumCast(this.layoutDoc.docMaxAutoHeight, proseHeight), proseHeight); - if (scrollHeight && this.props.renderDepth && !this.props.dontRegisterView) { // if top === 0, then the text box is growing upward (as the overlay caption) which doesn't contribute to the height computation - const setScrollHeight = () => this.rootDoc[this.fieldKey + "-scrollHeight"] = scrollHeight; - if (this.rootDoc === this.layoutDoc.doc || this.layoutDoc.resolvedDataDoc) { - setScrollHeight(); - } else setTimeout(setScrollHeight, 10); // if we have a template that hasn't been resolved yet, we can't set the height or we'd be setting it on the unresolved template. So set a timeout and hope its arrived... - } + setTimeout(() => { // bcz: don't know why this is needed, but without it, the size of the textbox is too big as it includes the size of the title header. after the timeout, the size seems to get computed correctly. + const proseHeight = this.ProseRef?.scrollHeight || 0; + const scrollHeight = this.ProseRef && Math.min(NumCast(this.layoutDoc.docMaxAutoHeight, proseHeight), proseHeight); + if (scrollHeight && this.props.renderDepth && !this.props.dontRegisterView) { // if top === 0, then the text box is growing upward (as the overlay caption) which doesn't contribute to the height computation + const setScrollHeight = () => this.rootDoc[this.fieldKey + "-scrollHeight"] = scrollHeight; + if (this.rootDoc === this.layoutDoc.doc || this.layoutDoc.resolvedDataDoc) { + setScrollHeight(); + } else setTimeout(setScrollHeight, 10); // if we have a template that hasn't been resolved yet, we can't set the height or we'd be setting it on the unresolved template. So set a timeout and hope its arrived... + } + }); } } fitToBox = () => this.props.Document._fitToBox; @@ -1427,7 +1464,6 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp PanelWidth={this.sidebarWidth} xMargin={0} yMargin={0} - chromeStatus={"enabled"} scaleField={this.SidebarKey + "-scale"} isAnnotationOverlay={false} select={emptyFunction} diff --git a/src/client/views/nodes/formattedText/RichTextRules.ts b/src/client/views/nodes/formattedText/RichTextRules.ts index 4b9b78211..3fd7d61fa 100644 --- a/src/client/views/nodes/formattedText/RichTextRules.ts +++ b/src/client/views/nodes/formattedText/RichTextRules.ts @@ -90,7 +90,7 @@ export class RichTextRules { textDoc.inlineTextCount = numInlines + 1; const inlineFieldKey = "inline" + numInlines; // which field on the text document this annotation will write to const inlineLayoutKey = "layout_" + inlineFieldKey; // the field holding the layout string that will render the inline annotation - const textDocInline = Docs.Create.TextDocument("", { layoutKey: inlineLayoutKey, _width: 75, _height: 35, annotationOn: textDoc, _autoHeight: true, _fontSize: "9px", title: "inline comment" }); + const textDocInline = Docs.Create.TextDocument("", { _layoutKey: inlineLayoutKey, _width: 75, _height: 35, annotationOn: textDoc, _autoHeight: true, _fontSize: "9px", title: "inline comment" }); textDocInline.title = inlineFieldKey; // give the annotation its own title textDocInline["title-custom"] = true; // And make sure that it's 'custom' so that editing text doesn't change the title of the containing doc textDocInline.isTemplateForField = inlineFieldKey; // this is needed in case the containing text doc is converted to a template at some point diff --git a/src/client/views/nodes/formattedText/nodes_rts.ts b/src/client/views/nodes/formattedText/nodes_rts.ts index df93fc117..2fe0a67cb 100644 --- a/src/client/views/nodes/formattedText/nodes_rts.ts +++ b/src/client/views/nodes/formattedText/nodes_rts.ts @@ -28,14 +28,17 @@ export const nodes: { [index: string]: NodeSpec } = { group: "block", attrs: { timeCode: { default: 0 }, - audioId: { default: "" } + audioId: { default: "" }, + textId: { default: "" } }, toDOM(node) { return ['audiotag', { + class: node.attrs.textId, // style: see FormattedTextBox.scss "data-timecode": node.attrs.timeCode, "data-audioid": node.attrs.audioId, + "data-textid": node.attrs.textId, }, formatAudioTime(node.attrs.timeCode.toString()) ]; @@ -45,7 +48,8 @@ export const nodes: { [index: string]: NodeSpec } = { tag: "audiotag", getAttrs(dom: any) { return { timeCode: dom.getAttribute("data-timecode"), - audioId: dom.getAttribute("data-audioid") + audioId: dom.getAttribute("data-audioid"), + textId: dom.getAttribute("data-textid") }; } }, |
