diff options
Diffstat (limited to 'src/client/views/nodes')
| -rw-r--r-- | src/client/views/nodes/AudioBox.scss | 7 | ||||
| -rw-r--r-- | src/client/views/nodes/AudioBox.tsx | 111 | ||||
| -rw-r--r-- | src/client/views/nodes/DocumentContentsView.tsx | 23 | ||||
| -rw-r--r-- | src/client/views/nodes/DocumentView.tsx | 358 | ||||
| -rw-r--r-- | src/client/views/nodes/FormattedTextBox.tsx | 93 | ||||
| -rw-r--r-- | src/client/views/nodes/ImageBox.tsx | 7 | ||||
| -rw-r--r-- | src/client/views/nodes/PDFBox.tsx | 22 | ||||
| -rw-r--r-- | src/client/views/nodes/PresBox.tsx | 6 | ||||
| -rw-r--r-- | src/client/views/nodes/RadialMenu.tsx | 83 | ||||
| -rw-r--r-- | src/client/views/nodes/VideoBox.tsx | 3 | ||||
| -rw-r--r-- | src/client/views/nodes/WebBox.scss | 15 | ||||
| -rw-r--r-- | src/client/views/nodes/WebBox.tsx | 138 |
12 files changed, 631 insertions, 235 deletions
diff --git a/src/client/views/nodes/AudioBox.scss b/src/client/views/nodes/AudioBox.scss index 3b19a6dba..83cdf3574 100644 --- a/src/client/views/nodes/AudioBox.scss +++ b/src/client/views/nodes/AudioBox.scss @@ -9,7 +9,6 @@ width:20px; height:100%; display:inline-block; - background: gray; } .audiobox-control, .audiobox-control-interactive { top:0; @@ -30,6 +29,8 @@ } .audiobox-record-interactive { pointer-events: all; + width:100%; + height:100%; } .audiobox-controls { width:100%; @@ -37,7 +38,6 @@ position: relative; display: flex; padding-left: 2px; - border: gray solid 3px; .audiobox-player { margin-top:auto; margin-bottom:auto; @@ -74,6 +74,7 @@ margin-left:-2.55px; background:gray; border-radius: 100%; + opacity:0.9; background-color: transparent; box-shadow: black 2px 2px 1px; .docuLinkBox-cont { @@ -100,7 +101,7 @@ } } .audiobox-linker:hover, .audiobox-linker-mini:hover { - transform:scale(1.5); + opacity:1; } .audiobox-marker-container, .audiobox-marker-minicontainer { position:absolute; diff --git a/src/client/views/nodes/AudioBox.tsx b/src/client/views/nodes/AudioBox.tsx index 9c57b6d7f..80d59af0b 100644 --- a/src/client/views/nodes/AudioBox.tsx +++ b/src/client/views/nodes/AudioBox.tsx @@ -17,6 +17,8 @@ import { ContextMenu } from "../ContextMenu"; import { Id } from "../../../new_fields/FieldSymbols"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { DocumentView } from "./DocumentView"; +import { Docs } from "../../documents/Documents"; +import { ComputedField } from "../../../new_fields/ScriptField"; // testing testing @@ -48,37 +50,45 @@ export class AudioBox extends DocExtendableComponent<FieldViewProps, AudioDocume _recordStart = 0; _stream: MediaStream | undefined; + public static START = 0; @observable private static _scrubTime = 0; - @observable private _audioState: "unrecorded" | "recording" | "recorded" = "unrecorded"; - @observable private _playing = false; - public static SetScrubTime = action((timeInMillisFrom1970: number) => AudioBox._scrubTime = timeInMillisFrom1970); + @computed get audioState(): undefined | "recording" | "paused" | "playing" { return this.dataDoc.audioState as (undefined | "recording" | "paused" | "playing"); } + set audioState(value) { this.dataDoc.audioState = value; } + public static SetScrubTime = (timeInMillisFrom1970: number) => { runInAction(() => AudioBox._scrubTime = 0); runInAction(() => AudioBox._scrubTime = timeInMillisFrom1970); }; public static ActiveRecordings: Doc[] = []; + @computed get recordingStart() { return Cast(this.dataDoc[this.props.fieldKey + "-recordingStart"], DateField)?.date.getTime(); } + async slideTemplate() { return (await Cast((await Cast(Doc.UserDoc().slidesBtn, Doc) as Doc).dragFactory, Doc) as Doc); } + + componentWillUnmount() { + this._reactionDisposer?.(); + this._linkPlayDisposer?.(); + this._scrubbingDisposer?.(); + } componentDidMount() { - runInAction(() => this._audioState = this.path ? "recorded" : "unrecorded"); + runInAction(() => this.audioState = this.path ? "paused" : undefined); this._linkPlayDisposer = reaction(() => this.layoutDoc.scrollToLinkID, scrollLinkId => { - scrollLinkId && DocListCast(this.dataDoc.links).filter(l => l[Id] === scrollLinkId).map(l => { - const la1 = l.anchor1 as Doc; - const linkTime = Doc.AreProtosEqual(la1, this.dataDoc) ? NumCast(l.anchor1Timecode) : NumCast(l.anchor2Timecode); - setTimeout(() => { this.playFrom(linkTime); Doc.linkFollowHighlight(l); }, 250); - }); - scrollLinkId && Doc.SetInPlace(this.layoutDoc, "scrollToLinkID", undefined, false); + if (scrollLinkId) { + DocListCast(this.dataDoc.links).filter(l => l[Id] === scrollLinkId).map(l => { + const linkTime = Doc.AreProtosEqual(l.anchor1 as Doc, this.dataDoc) ? NumCast(l.anchor1Timecode) : NumCast(l.anchor2Timecode); + setTimeout(() => { this.playFromTime(linkTime); Doc.linkFollowHighlight(l); }, 250); + }); + Doc.SetInPlace(this.layoutDoc, "scrollToLinkID", undefined, false); + } }, { fireImmediately: true }); this._reactionDisposer = reaction(() => SelectionManager.SelectedDocuments(), selected => { const sel = selected.length ? selected[0].props.Document : undefined; - this.Document.playOnSelect && sel && !Doc.AreProtosEqual(sel, this.props.Document) && this.playFrom(DateCast(sel.creationTime).date.getTime()); + this.Document.playOnSelect && this.recordingStart && sel && sel.creationDate && !Doc.AreProtosEqual(sel, this.props.Document) && this.playFromTime(DateCast(sel.creationDate).date.getTime()); + this.Document.playOnSelect && this.recordingStart && !sel && this.pause(); }); - this._scrubbingDisposer = reaction(() => AudioBox._scrubTime, timeInMillisecondsFrom1970 => { - const start = DateCast(this.dataDoc[this.props.fieldKey + "-recordingStart"]); - start && this.playFrom((timeInMillisecondsFrom1970 - start.date.getTime()) / 1000); - }); + this._scrubbingDisposer = reaction(() => AudioBox._scrubTime, (time) => this.Document.playOnSelect && this.playFromTime(AudioBox._scrubTime)); } timecodeChanged = () => { const htmlEle = this._ele; - if (this._audioState === "recorded" && htmlEle) { + if (this.audioState !== "recording" && htmlEle) { htmlEle.duration && htmlEle.duration !== Infinity && runInAction(() => this.dataDoc.duration = htmlEle.duration); DocListCast(this.dataDoc.links).map(l => { let la1 = l.anchor1 as Doc; @@ -97,32 +107,33 @@ export class AudioBox extends DocExtendableComponent<FieldViewProps, AudioDocume pause = action(() => { this._ele!.pause(); - this._playing = false; + this.audioState = "paused"; }); + playFromTime = (absoluteTime: number) => { + this.recordingStart && this.playFrom((absoluteTime - this.recordingStart) / 1000); + } playFrom = (seekTimeInSeconds: number) => { if (this._ele && AudioBox.Enabled) { if (seekTimeInSeconds < 0) { - this.pause(); + if (seekTimeInSeconds > -1) { + setTimeout(() => this.playFrom(0), -seekTimeInSeconds * 1000); + } else { + this.pause(); + } } else if (seekTimeInSeconds <= this._ele.duration) { this._ele.currentTime = seekTimeInSeconds; this._ele.play(); - runInAction(() => this._playing = true); + runInAction(() => this.audioState = "playing"); } else { this.pause(); } } } - componentWillUnmount() { - this._reactionDisposer && this._reactionDisposer(); - this._linkPlayDisposer && this._linkPlayDisposer(); - this._scrubbingDisposer && this._scrubbingDisposer(); - } - updateRecordTime = () => { - if (this._audioState === "recording") { + if (this.audioState === "recording") { setTimeout(this.updateRecordTime, 30); this.Document.currentTimecode = (new Date().getTime() - this._recordStart) / 1000; } @@ -136,6 +147,7 @@ export class AudioBox extends DocExtendableComponent<FieldViewProps, AudioDocume self._stream = stream; self._recorder = new MediaRecorder(stream); self.dataDoc[self.props.fieldKey + "-recordingStart"] = new DateField(new Date()); + AudioBox.START = new DateField(new Date()).date.getTime(); AudioBox.ActiveRecordings.push(self.props.Document); self._recorder.ondataavailable = async function (e: any) { const formData = new FormData(); @@ -149,8 +161,9 @@ export class AudioBox extends DocExtendableComponent<FieldViewProps, AudioDocume // upload to server with known URL self.props.Document[self.props.fieldKey] = new AudioField(url); }; - runInAction(() => self._audioState = "recording"); self._recordStart = new Date().getTime(); + console.log("RECORD START = " + self._recordStart); + runInAction(() => self.audioState = "recording"); setTimeout(self.updateRecordTime, 0); self._recorder.start(); setTimeout(() => { @@ -170,7 +183,7 @@ export class AudioBox extends DocExtendableComponent<FieldViewProps, AudioDocume stopRecording = action(() => { this._recorder.stop(); this.dataDoc.duration = (new Date().getTime() - this._recordStart) / 1000; - this._audioState = "recorded"; + this.audioState = "paused"; this._stream?.getAudioTracks()[0].stop(); const ind = AudioBox.ActiveRecordings.indexOf(this.props.Document); ind !== -1 && (AudioBox.ActiveRecordings.splice(ind, 1)); @@ -188,14 +201,25 @@ export class AudioBox extends DocExtendableComponent<FieldViewProps, AudioDocume e.stopPropagation(); } onStop = (e: any) => { - this.stopRecording(); - this._ele!.currentTime = 0; + this.Document.playOnSelect = !this.Document.playOnSelect; + e.stopPropagation(); + } + onFile = (e: any) => { + const newDoc = Docs.Create.TextDocument("", { + title: "", _chromeStatus: "disabled", + x: NumCast(this.props.Document.x), y: NumCast(this.props.Document.y) + NumCast(this.props.Document._height) + 10, + _width: NumCast(this.props.Document._width), _height: 3 * NumCast(this.props.Document._height) + }); + Doc.GetProto(newDoc).recordingSource = this.dataDoc; + Doc.GetProto(newDoc).recordingStart = 0; + Doc.GetProto(newDoc).audioState = ComputedField.MakeFunction("this.recordingSource.audioState"); + this.props.addDocument?.(newDoc); e.stopPropagation(); } setRef = (e: HTMLAudioElement | null) => { - e && e.addEventListener("timeupdate", this.timecodeChanged); - e && e.addEventListener("ended", this.pause); + e?.addEventListener("timeupdate", this.timecodeChanged); + e?.addEventListener("ended", this.pause); this._ele = e; } @@ -218,21 +242,26 @@ export class AudioBox extends DocExtendableComponent<FieldViewProps, AudioDocume const interactive = this.active() ? "-interactive" : ""; return <div className={`audiobox-container`} onContextMenu={this.specificContextMenu} onClick={!this.path ? this.recordClick : undefined}> - <div className="audiobox-handle"></div> {!this.path ? - <button className={`audiobox-record${interactive}`} style={{ backgroundColor: this._audioState === "recording" ? "red" : "black" }}> - {this._audioState === "recording" ? "STOP" : "RECORD"} - </button> : + <div style={{ display: "flex", width: "100%", alignItems: "center" }}> + <div className="audiobox-playhead" style={{ alignItems: "center", display: "inherit", background: "dimgray" }} onClick={this.onFile}> + <FontAwesomeIcon style={{ width: "30px", background: this.Document.playOnSelect ? "yellow" : "dimGray" }} icon="file-alt" size={this.props.PanelHeight() < 36 ? "1x" : "2x"} /> + </div> + <button className={`audiobox-record${interactive}`} style={{ position: "relative", backgroundColor: this.audioState === "recording" ? "red" : "black" }}> + {this.audioState === "recording" ? "STOP" : "RECORD"} + </button> + </div> : <div className="audiobox-controls"> <div className="audiobox-player" onClick={this.onPlay}> - <div className="audiobox-playhead"> <FontAwesomeIcon style={{ width: "100%" }} icon={this._playing ? "pause" : "play"} size={this.props.PanelHeight() < 36 ? "1x" : "2x"} /></div> - <div className="audiobox-playhead" onClick={this.onStop}><FontAwesomeIcon style={{ width: "100%" }} icon="stop" size={this.props.PanelHeight() < 36 ? "1x" : "2x"} /></div> + <div className="audiobox-playhead"> <FontAwesomeIcon style={{ width: "100%" }} icon={this.audioState === "paused" ? "play" : "pause"} size={this.props.PanelHeight() < 36 ? "1x" : "2x"} /></div> + <div className="audiobox-playhead" onClick={this.onStop}><FontAwesomeIcon style={{ width: "100%", background: this.Document.playOnSelect ? "yellow" : "dimGray" }} icon="hand-point-left" size={this.props.PanelHeight() < 36 ? "1x" : "2x"} /></div> <div className="audiobox-timeline" onClick={e => e.stopPropagation()} onPointerDown={e => { if (e.button === 0 && !e.ctrlKey) { const rect = (e.target as any).getBoundingClientRect(); + const wasPaused = this.audioState === "paused"; this._ele!.currentTime = this.Document.currentTimecode = (e.clientX - rect.x) / rect.width * NumCast(this.dataDoc.duration); - this.pause(); + wasPaused && this.pause(); e.stopPropagation(); } }} > @@ -249,12 +278,12 @@ export class AudioBox extends DocExtendableComponent<FieldViewProps, AudioDocume <div className={this.props.PanelHeight() < 32 ? "audiobox-marker-minicontainer" : "audiobox-marker-container"} key={l[Id]} style={{ left: `${linkTime / NumCast(this.dataDoc.duration, 1) * 100}%` }}> <div className={this.props.PanelHeight() < 32 ? "audioBox-linker-mini" : "audioBox-linker"} key={"linker" + i}> <DocumentView {...this.props} Document={l} layoutKey={Doc.LinkEndpoint(l, la2)} + ContainingCollectionDoc={this.props.Document} parentActive={returnTrue} bringToFront={emptyFunction} zoomToScale={emptyFunction} getScale={returnOne} backgroundColor={returnTransparent} /> </div> <div key={i} className="audiobox-marker" onPointerEnter={() => Doc.linkFollowHighlight(la1)} - onPointerDown={e => { if (e.button === 0 && !e.ctrlKey) { this.playFrom(linkTime); e.stopPropagation(); } }} - onClick={e => { if (e.button === 0 && !e.ctrlKey) { this.pause(); e.stopPropagation(); } }} /> + onPointerDown={e => { if (e.button === 0 && !e.ctrlKey) { const wasPaused = this.audioState === "paused"; this.playFrom(linkTime); wasPaused && this.pause(); e.stopPropagation(); } }} /> </div>; })} <div className="audiobox-current" style={{ left: `${NumCast(this.Document.currentTimecode) / NumCast(this.dataDoc.duration, 1) * 100}%` }} /> diff --git a/src/client/views/nodes/DocumentContentsView.tsx b/src/client/views/nodes/DocumentContentsView.tsx index bfda13eb3..dcb6d4a31 100644 --- a/src/client/views/nodes/DocumentContentsView.tsx +++ b/src/client/views/nodes/DocumentContentsView.tsx @@ -26,14 +26,14 @@ import { PDFBox } from "./PDFBox"; import { PresBox } from "./PresBox"; import { QueryBox } from "./QueryBox"; import { ColorBox } from "./ColorBox"; +import { DashWebRTCVideo } from "../webcam/DashWebRTCVideo"; import { DocuLinkBox } from "./DocuLinkBox"; import { PresElementBox } from "../presentationview/PresElementBox"; import { VideoBox } from "./VideoBox"; import { WebBox } from "./WebBox"; import { InkingStroke } from "../InkingStroke"; import React = require("react"); -import { DashWebRTCVideo } from "../webcam/DashWebRTCVideo"; - +import { RecommendationsBox } from "../RecommendationsBox"; import { TraceMobx } from "../../../new_fields/util"; const JsxParser = require('react-jsx-parser').default; //TODO Why does this need to be imported like this? @@ -73,21 +73,11 @@ export class DocumentContentsView extends React.Component<DocumentViewProps & { } get dataDoc() { - if (this.props.DataDoc === undefined && typeof Doc.LayoutField(this.props.Document) !== "string") { - // if there is no dataDoc (ie, we're not rendering a template layout), but this document has a layout document (not a layout string), - // then we render the layout document as a template and use this document as the data context for the template layout. - const proto = Doc.GetProto(this.props.Document); - return proto instanceof Promise ? undefined : proto; - } - return this.props.DataDoc instanceof Promise ? undefined : this.props.DataDoc; + const proto = this.props.DataDoc || Doc.GetProto(this.props.Document); + return proto instanceof Promise ? undefined : proto; } get layoutDoc() { - if (this.props.LayoutDoc || (this.props.DataDoc === undefined && typeof Doc.LayoutField(this.props.Document) !== "string")) { - // if there is no dataDoc (ie, we're not rendering a template layout), but this document has a layout document (not a layout string), - // then we render the layout document as a template and use this document as the data context for the template layout. - return Doc.expandTemplateLayout(this.props.LayoutDoc?.() || Doc.Layout(this.props.Document), this.props.Document); - } - return Doc.Layout(this.props.Document); + return Doc.expandTemplateLayout(this.props.LayoutDoc?.() || Doc.Layout(this.props.Document), this.props.Document); } CreateBindings(): JsxBindings { @@ -108,7 +98,8 @@ export class DocumentContentsView extends React.Component<DocumentViewProps & { FormattedTextBox, ImageBox, DirectoryImportBox, FontIconBox, ButtonBox, SliderBox, FieldView, CollectionFreeFormView, CollectionDockingView, CollectionSchemaView, CollectionView, WebBox, KeyValueBox, PDFBox, VideoBox, AudioBox, HistogramBox, PresBox, YoutubeBox, PresElementBox, QueryBox, - ColorBox, DashWebRTCVideo, DocuLinkBox, InkingStroke, DocumentBox, LinkBox + ColorBox, DashWebRTCVideo, DocuLinkBox, InkingStroke, DocumentBox, LinkBox, + RecommendationsBox, }} bindings={this.CreateBindings()} jsx={this.layout} diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx index fb97c18c3..7d2940df5 100644 --- a/src/client/views/nodes/DocumentView.tsx +++ b/src/client/views/nodes/DocumentView.tsx @@ -1,6 +1,6 @@ import { library } from '@fortawesome/fontawesome-svg-core'; import * as fa from '@fortawesome/free-solid-svg-icons'; -import { action, computed, observable, runInAction } from "mobx"; +import { action, computed, runInAction, trace, observable } from "mobx"; import { observer } from "mobx-react"; import * as rp from "request-promise"; import { Doc, DocListCast, Opt } from "../../../new_fields/Doc"; @@ -45,6 +45,12 @@ import { FormattedTextBox } from './FormattedTextBox'; import React = require("react"); import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { CollectionStackingView } from '../collections/CollectionStackingView'; +import { SchemaHeaderField } from '../../../new_fields/SchemaHeaderField'; +import { ClientRecommender } from '../../ClientRecommender'; +import { SearchUtil } from '../../util/SearchUtil'; +import { RadialMenu } from './RadialMenu'; +import { KeyphraseQueryView } from '../KeyphraseQueryView'; library.add(fa.faEdit, fa.faTrash, fa.faShare, fa.faDownload, fa.faExpandArrowsAlt, fa.faCompressArrowsAlt, fa.faLayerGroup, fa.faExternalLinkAlt, fa.faAlignCenter, fa.faCaretSquareRight, fa.faSquare, fa.faConciergeBell, fa.faWindowRestore, fa.faFolder, fa.faMapPin, fa.faLink, fa.faFingerprint, fa.faCrosshairs, fa.faDesktop, fa.faUnlock, fa.faLock, fa.faLaptopCode, fa.faMale, @@ -59,6 +65,8 @@ export interface DocumentViewProps { LibraryPath: Doc[]; fitToBox?: boolean; onClick?: ScriptField; + onPointerDown?: ScriptField; + onPointerUp?: ScriptField; dragDivName?: string; addDocument?: (doc: Doc) => boolean; removeDocument?: (doc: Doc) => boolean; @@ -92,10 +100,13 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu private _doubleTap = false; private _mainCont = React.createRef<HTMLDivElement>(); private _dropDisposer?: DragManager.DragDropDisposer; + private _showKPQuery: boolean = false; + private _queries: string = ""; private _gestureEventDisposer?: GestureUtils.GestureEventDisposer; private _titleRef = React.createRef<EditableView>(); protected multiTouchDisposer?: InteractionUtils.MultiTouchEventDisposer; + private holdDisposer?: InteractionUtils.MultiTouchEventDisposer; public get displayName() { return "DocumentView(" + this.props.Document.title + ")"; } // this makes mobx trace() statements more descriptive public get ContentDiv() { return this._mainCont.current; } @@ -104,72 +115,85 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu @computed get nativeWidth() { return this.layoutDoc._nativeWidth || 0; } @computed get nativeHeight() { return this.layoutDoc._nativeHeight || 0; } @computed get onClickHandler() { return this.props.onClick || this.layoutDoc.onClick || this.Document.onClick; } + @computed get onPointerDownHandler() { return this.props.onPointerDown ? this.props.onPointerDown : this.Document.onPointerDown; } + @computed get onPointerUpHandler() { return this.props.onPointerUp ? this.props.onPointerUp : this.Document.onPointerUp; } - private _firstX: number = 0; - private _firstY: number = 0; - - - // handle1PointerHoldStart = (e: React.TouchEvent): any => { - // this.onRadialMenu(e); - // const pt = InteractionUtils.GetMyTargetTouches(e, this.prevPoints, true)[0]; - // this._firstX = pt.pageX; - // this._firstY = pt.pageY; - // e.stopPropagation(); - // e.preventDefault(); - - // document.removeEventListener("touchmove", this.onTouch); - // document.removeEventListener("touchmove", this.handle1PointerHoldMove); - // document.addEventListener("touchmove", this.handle1PointerHoldMove); - // document.removeEventListener("touchend", this.handle1PointerHoldEnd); - // document.addEventListener("touchend", this.handle1PointerHoldEnd); - // } - - // handle1PointerHoldMove = (e: TouchEvent): void => { - // const pt = InteractionUtils.GetMyTargetTouches(me, this.prevPoints, true)[0]; - // if (Math.abs(pt.pageX - this._firstX) > 150 || Math.abs(pt.pageY - this._firstY) > 150) { - // this.handleRelease(); - // } - // document.removeEventListener("touchmove", this.handle1PointerHoldMove); - // document.addEventListener("touchmove", this.handle1PointerHoldMove); - // document.removeEventListener("touchend", this.handle1PointerHoldEnd); - // document.addEventListener("touchend", this.handle1PointerHoldEnd); - // } - - // handleRelease() { - // RadialMenu.Instance.closeMenu(); - // document.removeEventListener("touchmove", this.handle1PointerHoldMove); - // document.removeEventListener("touchend", this.handle1PointerHoldEnd); - // } - - // handle1PointerHoldEnd = (e: TouchEvent): void => { - // RadialMenu.Instance.closeMenu(); - // document.removeEventListener("touchmove", this.handle1PointerHoldMove); - // document.removeEventListener("touchend", this.handle1PointerHoldEnd); - // } - - // @action - // onRadialMenu = (e: React.TouchEvent): void => { - // const pt = InteractionUtils.GetMyTargetTouches(me, this.prevPoints, true)[0]; - - // RadialMenu.Instance.openMenu(); - - // RadialMenu.Instance.addItem({ description: "Open Fields", event: () => this.props.addDocTab(Docs.Create.KVPDocument(this.props.Document, { width: 300, height: 300 }), "onRight"), icon: "layer-group", selected: -1 }); - // RadialMenu.Instance.addItem({ description: "Delete this document", event: () => this.props.ContainingCollectionView?.removeDocument(this.props.Document), icon: "trash", selected: -1 }); - // RadialMenu.Instance.addItem({ description: "Open in a new tab", event: () => this.props.addDocTab(this.props.Document, "onRight"), icon: "folder", selected: -1 }); - // RadialMenu.Instance.addItem({ description: "Pin to Presentation", event: () => this.props.pinToPres(this.props.Document), icon: "map-pin", selected: -1 }); - - // RadialMenu.Instance.displayMenu(pt.pageX - 15, pt.pageY - 15); - // if (!SelectionManager.IsSelected(this, true)) { - // SelectionManager.SelectDoc(this, false); - // } - // e.stopPropagation(); - // } + private _firstX: number = -1; + private _firstY: number = -1; + + + + handle1PointerHoldStart = (e: Event, me: InteractionUtils.MultiTouchEvent<React.TouchEvent>): any => { + this.removeMoveListeners(); + this.removeEndListeners(); + document.removeEventListener("pointermove", this.onPointerMove); + document.removeEventListener("pointerup", this.onPointerUp); + console.log(SelectionManager.SelectedDocuments()); + console.log("START"); + if (RadialMenu.Instance._display === false) { + this.addHoldMoveListeners(); + this.addHoldEndListeners(); + this.onRadialMenu(e, me); + const pt = me.touchEvent.touches[me.touchEvent.touches.length - 1]; + this._firstX = pt.pageX; + this._firstY = pt.pageY; + } + + } + + handle1PointerHoldMove = (e: Event, me: InteractionUtils.MultiTouchEvent<TouchEvent>): void => { + + const pt = me.touchEvent.touches[me.touchEvent.touches.length - 1]; + + if (this._firstX === -1 || this._firstY === -1) { + return; + } + if (Math.abs(pt.pageX - this._firstX) > 150 || Math.abs(pt.pageY - this._firstY) > 150) { + this.handle1PointerHoldEnd(e, me); + } + } + + handle1PointerHoldEnd = (e: Event, me: InteractionUtils.MultiTouchEvent<TouchEvent>): void => { + this.removeHoldMoveListeners(); + this.removeHoldEndListeners(); + RadialMenu.Instance.closeMenu(); + this._firstX = -1; + this._firstY = -1; + SelectionManager.DeselectAll(); + me.touchEvent.stopPropagation(); + me.touchEvent.preventDefault(); + e.stopPropagation(); + if (RadialMenu.Instance.used) { + this.onContextMenu(me.touches[0]); + } + } + + @action + onRadialMenu = (e: Event, me: InteractionUtils.MultiTouchEvent<React.TouchEvent>): void => { + // console.log(InteractionUtils.GetMyTargetTouches(me, this.prevPoints, true)); + // const pt = InteractionUtils.GetMyTargetTouches(me, this.prevPoints, true)[0]; + const pt = me.touchEvent.touches[me.touchEvent.touches.length - 1]; + RadialMenu.Instance.openMenu(pt.pageX - 15, pt.pageY - 15); + + RadialMenu.Instance.addItem({ description: "Open Fields", event: () => this.props.addDocTab(Docs.Create.KVPDocument(this.props.Document, { _width: 300, _height: 300 }), "onRight"), icon: "map-pin", selected: -1 }); + RadialMenu.Instance.addItem({ description: "Delete this document", event: () => { this.props.ContainingCollectionView?.removeDocument(this.props.Document), RadialMenu.Instance.closeMenu(); }, icon: "layer-group", selected: -1 }); + RadialMenu.Instance.addItem({ description: "Open in a new tab", event: () => this.props.addDocTab(this.props.Document, "onRight"), icon: "trash", selected: -1 }); + RadialMenu.Instance.addItem({ description: "Pin to Presentation", event: () => this.props.pinToPres(this.props.Document), icon: "folder", selected: -1 }); + + // if (SelectionManager.IsSelected(this, true)) { + // SelectionManager.SelectDoc(this, false); + // } + SelectionManager.DeselectAll(); + + + } @action componentDidMount() { this._mainCont.current && (this._dropDisposer = DragManager.MakeDropTarget(this._mainCont.current, this.drop.bind(this))); this._mainCont.current && (this._gestureEventDisposer = GestureUtils.MakeGestureTarget(this._mainCont.current, this.onGesture.bind(this))); this._mainCont.current && (this.multiTouchDisposer = InteractionUtils.MakeMultiTouchTarget(this._mainCont.current, this.onTouchStart.bind(this))); + // this._mainCont.current && (this.holdDisposer = InteractionUtils.MakeHoldTouchTarget(this._mainCont.current, this.handle1PointerHoldStart.bind(this))); !this.props.dontRegisterView && DocumentManager.Instance.DocumentViews.push(this); } @@ -179,16 +203,24 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu this._dropDisposer && this._dropDisposer(); this._gestureEventDisposer && this._gestureEventDisposer(); this.multiTouchDisposer && this.multiTouchDisposer(); + this.holdDisposer && this.holdDisposer(); this._mainCont.current && (this._dropDisposer = DragManager.MakeDropTarget(this._mainCont.current, this.drop.bind(this))); this._mainCont.current && (this._gestureEventDisposer = GestureUtils.MakeGestureTarget(this._mainCont.current, this.onGesture.bind(this))); this._mainCont.current && (this.multiTouchDisposer = InteractionUtils.MakeMultiTouchTarget(this._mainCont.current, this.onTouchStart.bind(this))); + this._mainCont.current && (this.holdDisposer = InteractionUtils.MakeHoldTouchTarget(this._mainCont.current, this.handle1PointerHoldStart.bind(this))); } @action componentWillUnmount() { this._dropDisposer && this._dropDisposer(); + this._gestureEventDisposer && this._gestureEventDisposer(); + this.multiTouchDisposer && this.multiTouchDisposer(); + this.holdDisposer && this.holdDisposer(); Doc.UnBrushDoc(this.props.Document); - !this.props.dontRegisterView && DocumentManager.Instance.DocumentViews.splice(DocumentManager.Instance.DocumentViews.indexOf(this), 1); + if (!this.props.dontRegisterView) { + const index = DocumentManager.Instance.DocumentViews.indexOf(this); + index !== -1 && DocumentManager.Instance.DocumentViews.splice(index, 1); + } } startDragging(x: number, y: number, dropAction: dropActionType) { @@ -284,9 +316,9 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu } handle1PointerDown = (e: React.TouchEvent, me: InteractionUtils.MultiTouchEvent<React.TouchEvent>) => { + SelectionManager.DeselectAll(); if (this.Document.onPointerDown) return; - const touch = InteractionUtils.GetMyTargetTouches(me, this.prevPoints, true)[0]; - console.log("down"); + const touch = me.touchEvent.changedTouches.item(0); if (touch) { this._downX = touch.clientX; this._downY = touch.clientY; @@ -307,8 +339,9 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu this.removeMoveListeners(); } else if (!e.cancelBubble && (SelectionManager.IsSelected(this, true) || this.props.parentActive(true) || this.Document.onDragStart || this.Document.onClick) && !this.Document.lockedPosition && !this.Document.inOverlay) { - const touch = InteractionUtils.GetMyTargetTouches(me, this.prevPoints, true)[0]; - if (Math.abs(this._downX - touch.clientX) > 3 || Math.abs(this._downY - touch.clientY) > 3) { + + 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.Document.onDragStart || this.Document.onClick)) { this.cleanUpInteractions(); this.startDragging(this._downX, this._downY, this.Document.dropAction ? this.Document.dropAction as any : e.ctrlKey || e.altKey ? "alias" : undefined); @@ -404,6 +437,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu if (!(InteractionUtils.IsType(e, InteractionUtils.MOUSETYPE) || InkingControl.Instance.selectedTool === InkTool.Highlighter || InkingControl.Instance.selectedTool === InkTool.Pen)) { if (!InteractionUtils.IsType(e, InteractionUtils.PENTYPE)) { e.stopPropagation(); + // TODO: check here for panning/inking } return; } @@ -447,6 +481,14 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu } onPointerUp = (e: PointerEvent): void => { + this.cleanUpInteractions(); + + if (this.onPointerUpHandler && this.onPointerUpHandler.script && !InteractionUtils.IsType(e, InteractionUtils.PENTYPE)) { + this.onPointerUpHandler.script.run({ this: this.Document.isTemplateForField && this.props.DataDoc ? this.props.DataDoc : this.props.Document }, console.log); + document.removeEventListener("pointerup", this.onPointerUp); + return; + } + document.removeEventListener("pointermove", this.onPointerMove); document.removeEventListener("pointerup", this.onPointerUp); this._doubleTap = (Date.now() - this._lastTap < 300 && e.button === 0 && Math.abs(e.clientX - this._downX) < 2 && Math.abs(e.clientY - this._downY) < 2); @@ -476,28 +518,20 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu if (doc[customName] === undefined) { const _width = NumCast(doc._width); const _height = NumCast(doc._height); - const options = { title: "data", _width, x: -_width / 2, y: - _height / 2, _showSidebar: false }; + const options = { title: "data", backgroundColor: StrCast(doc.backgroundColor), _autoHeight: true, _width, x: -_width / 2, y: - _height / 2, _showSidebar: false }; - const field = doc.data; let fieldTemplate: Opt<Doc>; - if (field instanceof RichTextField || typeof (field) === "string") { + if (doc.data instanceof RichTextField || typeof (doc.data) === "string") { fieldTemplate = Docs.Create.TextDocument("", options); - } else if (field instanceof PdfField) { + } else if (doc.data instanceof PdfField) { fieldTemplate = Docs.Create.PdfDocument("http://www.msn.com", options); - } else if (field instanceof VideoField) { + } else if (doc.data instanceof VideoField) { fieldTemplate = Docs.Create.VideoDocument("http://www.cs.brown.edu", options); - } else if (field instanceof AudioField) { + } else if (doc.data instanceof AudioField) { fieldTemplate = Docs.Create.AudioDocument("http://www.cs.brown.edu", options); - } else if (field instanceof ImageField) { + } else if (doc.data instanceof ImageField) { fieldTemplate = Docs.Create.ImageDocument("http://www.cs.brown.edu", options); } - - if (fieldTemplate) { - fieldTemplate.backgroundColor = doc.backgroundColor; - fieldTemplate.heading = 1; - fieldTemplate._autoHeight = true; - } - const docTemplate = docLayoutTemplate || creator(fieldTemplate ? [fieldTemplate] : [], { title: customName + "(" + doc.title + ")", isTemplateDoc: true, _width: _width + 20, _height: Math.max(100, _height + 45) }); fieldTemplate && Doc.MakeMetadataFieldTemplate(fieldTemplate, Doc.GetProto(docTemplate)); @@ -527,8 +561,30 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu e.stopPropagation(); de.complete.annoDragData.linkedToDoc = true; - DocUtils.MakeLink({ doc: de.complete.annoDragData.annotationDocument }, { doc: this.props.Document, ctx: this.props.ContainingCollectionDoc }, - `Link from ${StrCast(de.complete.annoDragData.annotationDocument.title)}`); + DocUtils.MakeLink({ doc: de.complete.annoDragData.annotationDocument }, { doc: this.props.Document, ctx: this.props.ContainingCollectionDoc }, "link"); + } + if (de.complete.docDragData) { + if (de.complete.docDragData.applyAsTemplate) { + Doc.ApplyTemplateTo(de.complete.docDragData.draggedDocuments[0], this.props.Document, "layout_custom", undefined); + e.stopPropagation(); + } + else if (de.complete.docDragData.draggedDocuments[0].type === "text") { + const text = Cast(de.complete.docDragData.draggedDocuments[0].data, RichTextField)?.Text; + if (text && text[0] === "{" && text[text.length - 1] === "}" && text.includes(":")) { + let loc = text.indexOf(":"); + let key = text.slice(1, loc); + let value = text.slice(loc + 1, text.length - 1); + console.log(key); + console.log(value); + console.log(this.props.Document); + this.props.Document[key] = value; + console.log(de.complete.docDragData.draggedDocuments[0].x); + console.log(de.complete.docDragData.draggedDocuments[0].x); + e.preventDefault(); + e.stopPropagation(); + de.complete.aborted = true; + } + } } if (de.complete.linkDragData) { e.stopPropagation(); @@ -536,7 +592,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu // const views = docs.map(d => DocumentManager.Instance.getDocumentView(d)).filter(d => d).map(d => d as DocumentView); de.complete.linkDragData.linkSourceDocument !== this.props.Document && (de.complete.linkDragData.linkDocument = DocUtils.MakeLink({ doc: de.complete.linkDragData.linkSourceDocument }, - { doc: this.props.Document, ctx: this.props.ContainingCollectionDoc }, `link from ${de.complete.linkDragData.linkSourceDocument.title} to ${this.props.Document.title}`)); // TODODO this is where in text links get passed + { doc: this.props.Document, ctx: this.props.ContainingCollectionDoc }, `link`)); // TODODO this is where in text links get passed } } @@ -562,7 +618,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu const portalLink = DocListCast(this.Document.links).find(d => d.anchor1 === this.props.Document); if (!portalLink) { const portal = Docs.Create.FreeformDocument([], { _width: (this.layoutDoc._width || 0) + 10, _height: this.layoutDoc._height || 0, title: StrCast(this.props.Document.title) + ".portal" }); - DocUtils.MakeLink({ doc: this.props.Document, ctx: this.props.ContainingCollectionDoc }, { doc: portal }, "portal link", "portal link"); + DocUtils.MakeLink({ doc: this.props.Document, ctx: this.props.ContainingCollectionDoc }, { doc: portal }, "portal to"); } this.Document.isButton = true; } @@ -618,7 +674,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu return; } e.persist(); - e.stopPropagation(); + e?.stopPropagation(); if (Math.abs(this._downX - e.clientX) > 3 || Math.abs(this._downY - e.clientY) > 3 || e.isDefaultPrevented()) { e.preventDefault(); @@ -700,6 +756,35 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu // a.download = `DocExport-${this.props.Document[Id]}.zip`; // a.click(); }); + let recommender_subitems: ContextMenuProps[] = []; + + recommender_subitems.push({ + description: "Internal recommendations", + event: () => this.recommender(e), + icon: "brain" + }); + + let ext_recommender_subitems: ContextMenuProps[] = []; + + ext_recommender_subitems.push({ + description: "arXiv", + event: () => this.externalRecommendation("arxiv"), + icon: "brain" + }); + ext_recommender_subitems.push({ + description: "Bing", + event: () => this.externalRecommendation("bing"), + icon: "brain" + }); + + recommender_subitems.push({ + description: "External recommendations", + subitems: ext_recommender_subitems, + icon: "brain" + }); + + cm.addItem({ description: "Recommender System", subitems: recommender_subitems, icon: "brain" }); + moreItems.push({ description: "Publish", event: () => DocUtils.Publish(this.props.Document, this.Document.title || "", this.props.addDocument, this.props.removeDocument), icon: "file" }); moreItems.push({ description: "Delete", event: this.deleteClicked, icon: "trash" }); @@ -754,6 +839,104 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu }); } + recommender = async (e: React.MouseEvent) => { + if (!ClientRecommender.Instance) new ClientRecommender({ title: "Client Recommender" }); + const documents: Doc[] = []; + const allDocs = await SearchUtil.GetAllDocs(); + // allDocs.forEach(doc => console.log(doc.title)); + // clears internal representation of documents as vectors + ClientRecommender.Instance.reset_docs(); + //ClientRecommender.Instance.arxivrequest("electrons"); + await Promise.all(allDocs.map((doc: Doc) => { + let isMainDoc: boolean = false; + const dataDoc = Doc.GetProto(doc); + if (doc.type === DocumentType.TEXT) { + if (dataDoc === Doc.GetProto(this.props.Document)) { + isMainDoc = true; + } + if (!documents.includes(dataDoc)) { + documents.push(dataDoc); + const extdoc = doc.data_ext as Doc; + return ClientRecommender.Instance.extractText(doc, extdoc ? extdoc : doc, true, "", isMainDoc); + } + } + if (doc.type === DocumentType.IMG) { + if (dataDoc === Doc.GetProto(this.props.Document)) { + isMainDoc = true; + } + if (!documents.includes(dataDoc)) { + documents.push(dataDoc); + const extdoc = doc.data_ext as Doc; + return ClientRecommender.Instance.extractText(doc, extdoc ? extdoc : doc, true, "", isMainDoc, true); + } + } + })); + const doclist = ClientRecommender.Instance.computeSimilarities("cosine"); + let recDocs: { preview: Doc, score: number }[] = []; + // tslint:disable-next-line: prefer-for-of + for (let i = 0; i < doclist.length; i++) { + recDocs.push({ preview: doclist[i].actualDoc, score: doclist[i].score }); + } + + const data = recDocs.map(unit => { + unit.preview.score = unit.score; + return unit.preview; + }); + + console.log(recDocs.map(doc => doc.score)); + + const title = `Showing ${data.length} recommendations for "${StrCast(this.props.Document.title)}"`; + const recommendations = Docs.Create.RecommendationsDocument(data, { title }); + recommendations.documentIconHeight = 150; + recommendations.sourceDoc = this.props.Document; + recommendations.sourceDocContext = this.props.ContainingCollectionView!.props.Document; + CollectionDockingView.AddRightSplit(recommendations, undefined); + + // RecommendationsBox.Instance.displayRecommendations(e.pageX + 100, e.pageY); + } + + @action + externalRecommendation = async (api: string) => { + if (!ClientRecommender.Instance) new ClientRecommender({ title: "Client Recommender" }); + ClientRecommender.Instance.reset_docs(); + const doc = Doc.GetDataDoc(this.props.Document); + const extdoc = doc.data_ext as Doc; + const recs_and_kps = await ClientRecommender.Instance.extractText(doc, extdoc ? extdoc : doc, false, api); + let recs: any; + let kps: any; + if (recs_and_kps) { + recs = recs_and_kps.recs; + kps = recs_and_kps.keyterms; + } + else { + console.log("recommender system failed :("); + return; + } + console.log("ibm keyterms: ", kps.toString()); + const headers = [new SchemaHeaderField("title"), new SchemaHeaderField("href")]; + const bodies: Doc[] = []; + const titles = recs.title_vals; + const urls = recs.url_vals; + for (let i = 0; i < 5; i++) { + const body = Docs.Create.FreeformDocument([], { title: titles[i] }); + body.href = urls[i]; + bodies.push(body); + } + CollectionDockingView.AddRightSplit(Docs.Create.SchemaDocument(headers, bodies, { title: `Showing External Recommendations for "${StrCast(doc.title)}"` }), undefined); + this._showKPQuery = true; + this._queries = kps.toString(); + } + + onPointerEnter = (e: React.PointerEvent): void => { Doc.BrushDoc(this.props.Document); }; + onPointerLeave = (e: React.PointerEvent): void => { Doc.UnBrushDoc(this.props.Document); }; + + // does Document set a layout prop + // does Document set a layout prop + setsLayoutProp = (prop: string) => this.props.Document[prop] !== this.props.Document["default" + prop[0].toUpperCase() + prop.slice(1)] && this.props.Document["default" + prop[0].toUpperCase() + prop.slice(1)]; + // get the a layout prop by first choosing the prop from Document, then falling back to the layout doc otherwise. + getLayoutPropStr = (prop: string) => StrCast(this.setsLayoutProp(prop) ? this.props.Document[prop] : this.layoutDoc[prop]); + getLayoutPropNum = (prop: string) => NumCast(this.setsLayoutProp(prop) ? this.props.Document[prop] : this.layoutDoc[prop]); + isSelected = (outsideReaction?: boolean) => SelectionManager.IsSelected(this, outsideReaction); select = (ctrlPressed: boolean) => { SelectionManager.SelectDoc(this, ctrlPressed); }; @@ -920,7 +1103,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu const highlightStyles = ["solid", "dashed", "solid", "solid", "solid", "solid", "solid"]; let highlighting = fullDegree && this.layoutDoc.type !== DocumentType.FONTICON && this.layoutDoc._viewType !== CollectionViewType.Linear; highlighting = highlighting && this.props.focus !== emptyFunction; // bcz: hack to turn off highlighting onsidebar panel documents. need to flag a document as not highlightable in a more direct way - return <div id={this.props.Document[Id]} className={`documentView-node${this.topMost ? "-topmost" : ""}`} ref={this._mainCont} onKeyDown={this.onKeyDown} + return <div className={`documentView-node${this.topMost ? "-topmost" : ""}`} ref={this._mainCont} onKeyDown={this.onKeyDown} onContextMenu={this.onContextMenu} onPointerDown={this.onPointerDown} onClick={this.onClick} onPointerEnter={e => Doc.BrushDoc(this.props.Document)} onPointerLeave={e => Doc.UnBrushDoc(this.props.Document)} style={{ @@ -944,11 +1127,12 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu </> : this.innards} </div>; + { this._showKPQuery ? <KeyphraseQueryView keyphrases={this._queries}></KeyphraseQueryView> : undefined } } } -Scripting.addGlobal(function toggleDetail(doc: any, layoutKey: string) { +Scripting.addGlobal(function toggleDetail(doc: any, layoutKey: string, otherKey: string="layout") { const dv = DocumentManager.Instance.getDocumentView(doc); - if (dv?.props.Document.layoutKey === layoutKey) dv?.switchViews(false, ""); + if (dv?.props.Document.layoutKey === layoutKey) dv?.switchViews(otherKey !== "layout", otherKey.replace("layout_", "")); else dv?.switchViews(true, layoutKey.replace("layout_", "")); });
\ No newline at end of file diff --git a/src/client/views/nodes/FormattedTextBox.tsx b/src/client/views/nodes/FormattedTextBox.tsx index ad804209b..f18183600 100644 --- a/src/client/views/nodes/FormattedTextBox.tsx +++ b/src/client/views/nodes/FormattedTextBox.tsx @@ -20,7 +20,7 @@ import { InkTool } from '../../../new_fields/InkField'; import { RichTextField } from "../../../new_fields/RichTextField"; import { RichTextUtils } from '../../../new_fields/RichTextUtils'; import { createSchema, makeInterface } from "../../../new_fields/Schema"; -import { Cast, NumCast, StrCast } from "../../../new_fields/Types"; +import { Cast, NumCast, StrCast, BoolCast } from "../../../new_fields/Types"; import { TraceMobx } from '../../../new_fields/util'; import { addStyleSheet, addStyleSheetRule, clearStyleSheetRules, emptyFunction, numberRange, returnOne, Utils } from '../../../Utils'; import { GoogleApiClientUtils, Pulls, Pushes } from '../../apis/google_docs/GoogleApiClientUtils'; @@ -82,6 +82,7 @@ export class FormattedTextBox extends DocAnnotatableComponent<(FieldViewProps & private _lastY = 0; private _undoTyping?: UndoManager.Batch; private _searchReactionDisposer?: Lambda; + private _recordReactionDisposer: Opt<IReactionDisposer>; private _scrollToRegionReactionDisposer: Opt<IReactionDisposer>; private _reactionDisposer: Opt<IReactionDisposer>; private _heightReactionDisposer: Opt<IReactionDisposer>; @@ -92,6 +93,9 @@ export class FormattedTextBox extends DocAnnotatableComponent<(FieldViewProps & private _scrollDisposer: Opt<IReactionDisposer>; private dropDisposer?: DragManager.DragDropDisposer; + @computed get _recording() { return this.dataDoc.audioState === "recording"; } + set _recording(value) { this.dataDoc.audioState = value ? "recording" : undefined; } + @observable private _entered = false; public static FocusedBox: FormattedTextBox | undefined; @@ -150,7 +154,7 @@ export class FormattedTextBox extends DocAnnotatableComponent<(FieldViewProps & this.dataDoc[key] = doc || Docs.Create.FreeformDocument([], { title: value, _width: 500, _height: 500 }, value); DocUtils.Publish(this.dataDoc[key] as Doc, value, this.props.addDocument, this.props.removeDocument); if (linkDoc) { (linkDoc as Doc).anchor2 = this.dataDoc[key] as Doc; } - else DocUtils.MakeLink({ doc: this.dataDoc, ctx: this.props.ContainingCollectionDoc }, { doc: this.dataDoc[key] as Doc }, "Ref:" + value, "link to named target", id); + else DocUtils.MakeLink({ doc: this.dataDoc, ctx: this.props.ContainingCollectionDoc }, { doc: this.dataDoc[key] as Doc }, "link to named target", id); }); }); }); @@ -186,7 +190,7 @@ export class FormattedTextBox extends DocAnnotatableComponent<(FieldViewProps & (tx.storedMarks && !this._editorView.state.storedMarks) && (this._editorView.state.storedMarks = tx.storedMarks); const tsel = this._editorView.state.selection.$from; - tsel.marks().filter(m => m.type === this._editorView!.state.schema.marks.user_mark).map(m => AudioBox.SetScrubTime(Math.max(0, m.attrs.modified * 5000 - 1000))); + tsel.marks().filter(m => m.type === this._editorView!.state.schema.marks.user_mark).map(m => AudioBox.SetScrubTime(Math.max(0, m.attrs.modified * 1000))); const curText = state.doc.textBetween(0, state.doc.content.size, "\n\n"); const curTemp = Cast(this.props.Document[this.props.fieldKey + "-textTemplate"], RichTextField); if (!this._applyingChange) { @@ -207,7 +211,7 @@ export class FormattedTextBox extends DocAnnotatableComponent<(FieldViewProps & } updateTitle = () => { - if ((this.props.Document.isTemplateForField === "data" || !this.props.Document.isTemplateForField) && // only update the title if the data document's data field is changing + if ((this.props.Document.isTemplateForField === "text" || !this.props.Document.isTemplateForField) && // only update the title if the data document's data field is changing StrCast(this.dataDoc.title).startsWith("-") && this._editorView && !this.Document.customTitle) { const str = this._editorView.state.doc.textContent; const titlestr = str.substr(0, Math.min(40, str.length)); @@ -387,7 +391,7 @@ export class FormattedTextBox extends DocAnnotatableComponent<(FieldViewProps & !this.props.Document.expandedTemplate && funcs.push({ description: "Make Template", event: () => { this.props.Document.isTemplateDoc = true; Doc.AddDocToList(Cast(Doc.UserDoc().noteTypes, Doc, null), "data", this.props.Document); }, icon: "eye" }); funcs.push({ description: "Toggle Single Line", event: () => this.props.Document._singleLine = !this.props.Document._singleLine, icon: "expand-arrows-alt" }); funcs.push({ description: "Toggle Sidebar", event: () => this.props.Document._showSidebar = !this.props.Document._showSidebar, icon: "expand-arrows-alt" }); - funcs.push({ description: "Record Bullet", event: () => this.recordBullet(), icon: "expand-arrows-alt" }); + funcs.push({ description: "Toggle Audio", event: () => this.props.Document._showAudio = !this.props.Document._showAudio, icon: "expand-arrows-alt" }); funcs.push({ description: "Toggle Menubar", event: () => this.toggleMenubar(), icon: "expand-arrows-alt" }); ["My Text", "Text from Others", "Todo Items", "Important Items", "Ignore Items", "Disagree Items", "By Recent Minute", "By Recent Hour"].forEach(option => funcs.push({ @@ -405,12 +409,7 @@ export class FormattedTextBox extends DocAnnotatableComponent<(FieldViewProps & ContextMenu.Instance.addItem({ description: "Text Funcs...", subitems: funcs, icon: "asterisk" }); } - @observable _recording = false; - recordDictation = () => { - //this._editorView!.focus(); - if (this._recording) return; - runInAction(() => this._recording = true); DictationManager.Controls.listen({ interimHandler: this.setCurrentBulletContent, continuous: { indefinite: false }, @@ -418,13 +417,10 @@ export class FormattedTextBox extends DocAnnotatableComponent<(FieldViewProps & if (results && [DictationManager.Controls.Infringed].includes(results)) { DictationManager.Controls.stop(); } - this._editorView!.focus(); + //this._editorView!.focus(); }); } - stopDictation = (abort: boolean) => { - runInAction(() => this._recording = false); - DictationManager.Controls.stop(!abort); - } + stopDictation = (abort: boolean) => { DictationManager.Controls.stop(!abort); } @action toggleMenubar = () => { @@ -449,12 +445,28 @@ export class FormattedTextBox extends DocAnnotatableComponent<(FieldViewProps & setCurrentBulletContent = (value: string) => { if (this._editorView) { let state = this._editorView.state; + let now = Date.now(); + if (NumCast(this.props.Document.recordingStart, -1) === 0) { + this.props.Document.recordingStart = now = AudioBox.START; + } + console.log("NOW = " + (now - AudioBox.START) / 1000); + 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; + } + } + } + this._break = false; + console.log("start = " + (mark.attrs.modified * 1000 - AudioBox.START) / 1000); + value = "" + (mark.attrs.modified * 1000 - AudioBox.START) / 1000 + value; const from = state.selection.from; - const to = state.selection.to; - this._editorView.dispatch(state.tr.insertText(value, from, to)); - state = this._editorView.state; - const updated = TextSelection.create(state.doc, from, from + value.length); - this._editorView.dispatch(state.tr.setSelection(updated)); + const inserted = state.tr.insertText(value).addMark(from, from + value.length + 1, mark); + this._editorView.dispatch(inserted.setSelection(TextSelection.create(inserted.doc, from, from + value.length + 1))); } } @@ -558,6 +570,17 @@ export class FormattedTextBox extends DocAnnotatableComponent<(FieldViewProps & search => search ? this.highlightSearchTerms([Doc.SearchQuery()]) : this.unhighlightSearchTerms(), { fireImmediately: true }); + this._recordReactionDisposer = reaction(() => this._recording, + () => { + if (this._recording) { + setTimeout(action(() => { + this.stopDictation(true); + setTimeout(() => this.recordDictation(), 500); + }), 500); + } else setTimeout(() => this.stopDictation(true), 0); + } + ); + this._scrollToRegionReactionDisposer = reaction( () => StrCast(this.layoutDoc.scrollToLinkID), async (scrollToLinkID) => { @@ -723,7 +746,7 @@ export class FormattedTextBox extends DocAnnotatableComponent<(FieldViewProps & targetAnnotations?.push(pdfRegion); }); - const link = DocUtils.MakeLink({ doc: this.props.Document, ctx: this.props.ContainingCollectionDoc }, { doc: pdfRegion, ctx: pdfDoc }, "note on " + pdfDoc.title, "pasted PDF link"); + const link = DocUtils.MakeLink({ doc: this.props.Document, ctx: this.props.ContainingCollectionDoc }, { doc: pdfRegion, ctx: pdfDoc }, "PDF pasted"); if (link) { cbe.clipboardData!.setData("dash/linkDoc", link[Id]); const linkId = link[Id]; @@ -807,7 +830,7 @@ export class FormattedTextBox extends DocAnnotatableComponent<(FieldViewProps & } (selectOnLoad /* || !rtfField?.Text*/) && this._editorView!.focus(); // add user mark for any first character that was typed since the user mark that gets set in KeyPress won't have been called yet. - this._editorView!.state.storedMarks = [...(this._editorView!.state.storedMarks ? this._editorView!.state.storedMarks : []), schema.marks.user_mark.create({ userid: Doc.CurrentUserEmail, modified: Math.round(Date.now() / 1000 / 5) })]; + this._editorView!.state.storedMarks = [...(this._editorView!.state.storedMarks ? this._editorView!.state.storedMarks : []), schema.marks.user_mark.create({ userid: Doc.CurrentUserEmail, modified: Math.floor(Date.now() / 1000) })]; } getFont(font: string) { switch (font) { @@ -831,6 +854,7 @@ export class FormattedTextBox extends DocAnnotatableComponent<(FieldViewProps & this._pullReactionDisposer?.(); this._heightReactionDisposer?.(); this._searchReactionDisposer?.(); + this._recordReactionDisposer?.(); this._buttonBarReactionDisposer?.(); this._editorView?.destroy(); } @@ -838,7 +862,19 @@ export class FormattedTextBox extends DocAnnotatableComponent<(FieldViewProps & static _downEvent: any; _downX = 0; _downY = 0; + _break = false; onPointerDown = (e: React.PointerEvent): void => { + if (this._recording && !e.ctrlKey && e.button === 0) { + this.stopDictation(true); + this._break = true; + let 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)); + e.preventDefault(); + e.stopPropagation(); + if (this._recording) setTimeout(() => this.recordDictation(), 500); + } this._downX = e.clientX; this._downY = e.clientY; this.doLinkOnDeselect(); @@ -955,7 +991,6 @@ export class FormattedTextBox extends DocAnnotatableComponent<(FieldViewProps & this.props.select(e.ctrlKey); this.hitBulletTargets(e.clientX, e.clientY, e.shiftKey, false); } - if (this._recording) setTimeout(() => { this.stopDictation(true); setTimeout(() => this.recordDictation(), 500); }, 500); } // this hackiness handles clicking on the list item bullets to do expand/collapse. the bullets are ::before pseudo elements so there's no real way to hit test against them. @@ -1016,7 +1051,7 @@ export class FormattedTextBox extends DocAnnotatableComponent<(FieldViewProps & richTextMenuPlugin() { return new Plugin({ view(newView) { - RichTextMenu.Instance.changeView(newView); + RichTextMenu.Instance && RichTextMenu.Instance.changeView(newView); return RichTextMenu.Instance; } }); @@ -1062,17 +1097,13 @@ export class FormattedTextBox extends DocAnnotatableComponent<(FieldViewProps & if (e.key === "Tab" || e.key === "Enter") { e.preventDefault(); } - const mark = e.key !== " " && this._lastTimedMark ? this._lastTimedMark : schema.marks.user_mark.create({ userid: Doc.CurrentUserEmail, modified: Math.round(Date.now() / 1000 / 5) }); + const mark = e.key !== " " && this._lastTimedMark ? this._lastTimedMark : schema.marks.user_mark.create({ userid: Doc.CurrentUserEmail, modified: Math.floor(Date.now() / 1000) }); this._lastTimedMark = mark; this._editorView!.dispatch(this._editorView!.state.tr.removeStoredMark(schema.marks.user_mark.create({})).addStoredMark(mark)); if (!this._undoTyping) { this._undoTyping = UndoManager.StartBatch("undoTyping"); } - if (this._recording) { - this.stopDictation(true); - setTimeout(() => this.recordDictation(), 250); - } } onscrolled = (ev: React.UIEvent) => { @@ -1169,8 +1200,8 @@ export class FormattedTextBox extends DocAnnotatableComponent<(FieldViewProps & </div>} {!this.props.Document._showAudio ? (null) : <div className="formattedTextBox-dictation" - onClick={e => { - this._recording ? this.stopDictation(true) : this.recordDictation(); + onPointerDown={e => { + runInAction(() => this._recording = !this._recording); setTimeout(() => this._editorView!.focus(), 500); e.stopPropagation(); }} > diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx index c46191270..e5848614c 100644 --- a/src/client/views/nodes/ImageBox.tsx +++ b/src/client/views/nodes/ImageBox.tsx @@ -1,6 +1,6 @@ import { library } from '@fortawesome/fontawesome-svg-core'; import { faEye } from '@fortawesome/free-regular-svg-icons'; -import { faAsterisk, faFileAudio, faImage, faPaintBrush } from '@fortawesome/free-solid-svg-icons'; +import { faAsterisk, faFileAudio, faImage, faPaintBrush, faBrain } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { action, computed, observable, runInAction, trace } from 'mobx'; import { observer } from "mobx-react"; @@ -22,6 +22,8 @@ import FaceRectangles from './FaceRectangles'; import { FieldView, FieldViewProps } from './FieldView'; import "./ImageBox.scss"; import React = require("react"); +import { SearchUtil } from '../../util/SearchUtil'; +import { ClientRecommender } from '../../ClientRecommender'; import { CollectionFreeFormView } from '../collections/collectionFreeForm/CollectionFreeFormView'; import { documentSchema } from '../../../new_fields/documentSchemas'; import { Id, Copy } from '../../../new_fields/FieldSymbols'; @@ -35,7 +37,7 @@ const path = require('path'); const { Howl } = require('howler'); -library.add(faImage, faEye as any, faPaintBrush); +library.add(faImage, faEye as any, faPaintBrush, faBrain); library.add(faFileAudio, faAsterisk); @@ -178,6 +180,7 @@ export class ImageBox extends DocAnnotatableComponent<FieldViewProps, ImageDocum const modes: ContextMenuProps[] = existingAnalyze && "subitems" in existingAnalyze ? existingAnalyze.subitems : []; modes.push({ description: "Generate Tags", event: this.generateMetadata, icon: "tag" }); modes.push({ description: "Find Faces", event: this.extractFaces, icon: "camera" }); + //modes.push({ description: "Recommend", event: this.extractText, icon: "brain" }); !existingAnalyze && ContextMenu.Instance.addItem({ description: "Analyzers...", subitems: modes, icon: "hand-point-right" }); ContextMenu.Instance.addItem({ description: "Image Funcs...", subitems: funcs, icon: "asterisk" }); diff --git a/src/client/views/nodes/PDFBox.tsx b/src/client/views/nodes/PDFBox.tsx index 593f40f10..f47620c24 100644 --- a/src/client/views/nodes/PDFBox.tsx +++ b/src/client/views/nodes/PDFBox.tsx @@ -56,7 +56,7 @@ export class PDFBox extends DocAnnotatableComponent<FieldViewProps, PdfDocument> const backup = "oldPath"; const { Document } = this.props; - const { url: { href } } = Cast(Document[this.props.fieldKey], PdfField)!; + const { url: { href } } = Cast(this.dataDoc[this.props.fieldKey], PdfField)!; const pathCorrectionTest = /upload\_[a-z0-9]{32}.(.*)/g; const matches = pathCorrectionTest.exec(href); console.log("\nHere's the { url } being fed into the outer regex:"); @@ -78,9 +78,7 @@ export class PDFBox extends DocAnnotatableComponent<FieldViewProps, PdfDocument> } } - componentWillUnmount() { - this._selectReactionDisposer && this._selectReactionDisposer(); - } + componentWillUnmount() { this._selectReactionDisposer?.(); } componentDidMount() { this._selectReactionDisposer = reaction(() => this.props.isSelected(), () => { @@ -96,11 +94,11 @@ export class PDFBox extends DocAnnotatableComponent<FieldViewProps, PdfDocument> !this.Document._fitWidth && (this.Document._height = this.Document[WidthSym]() * (nh / nw)); } - public search(string: string, fwd: boolean) { this._pdfViewer && this._pdfViewer.search(string, fwd); } - public prevAnnotation() { this._pdfViewer && this._pdfViewer.prevAnnotation(); } - public nextAnnotation() { this._pdfViewer && this._pdfViewer.nextAnnotation(); } - public backPage() { this._pdfViewer!.gotoPage((this.Document.curPage || 1) - 1); } - public forwardPage() { this._pdfViewer!.gotoPage((this.Document.curPage || 1) + 1); } + public search = (string: string, fwd: boolean) => { this._pdfViewer?.search(string, fwd); } + public prevAnnotation = () => { this._pdfViewer?.prevAnnotation(); } + public nextAnnotation = () => { this._pdfViewer?.nextAnnotation(); } + public backPage = () => { this._pdfViewer!.gotoPage((this.Document.curPage || 1) - 1); } + public forwardPage = () => { this._pdfViewer!.gotoPage((this.Document.curPage || 1) + 1); } public gotoPage = (p: number) => { this._pdfViewer!.gotoPage(p); }; @undoBatch @@ -233,7 +231,7 @@ export class PDFBox extends DocAnnotatableComponent<FieldViewProps, PdfDocument> isChildActive = (outsideReaction?: boolean) => this._isChildActive; @computed get renderPdfView() { const pdfUrl = Cast(this.dataDoc[this.props.fieldKey], PdfField); - return <div className={"pdfBox"} onContextMenu={this.specificContextMenu}> + return <div className={"pdfBox"} onContextMenu={this.specificContextMenu} style={{ height: this.props.Document._scrollTop ? NumCast(this.Document._height) * this.props.PanelWidth() / NumCast(this.Document._width) : undefined }}> <PDFViewer {...this.props} pdf={this._pdf!} url={pdfUrl!.url.pathname} active={this.props.active} loaded={this.loaded} setPdfViewer={this.setPdfViewer} ContainingCollectionView={this.props.ContainingCollectionView} renderDepth={this.props.renderDepth} PanelHeight={this.props.PanelHeight} PanelWidth={this.props.PanelWidth} @@ -243,7 +241,7 @@ export class PDFBox extends DocAnnotatableComponent<FieldViewProps, PdfDocument> ScreenToLocalTransform={this.props.ScreenToLocalTransform} select={this.props.select} isSelected={this.props.isSelected} whenActiveChanged={this.whenActiveChanged} isChildActive={this.isChildActive} - fieldKey={this.props.fieldKey} startupLive={this._initialScale < 2.5 ? true : false} /> + fieldKey={this.props.fieldKey} startupLive={this._initialScale < 2.5 || this.props.Document._scrollTop ? true : false} /> {this.settingsPanel()} </div>; } @@ -252,7 +250,7 @@ export class PDFBox extends DocAnnotatableComponent<FieldViewProps, PdfDocument> render() { const pdfUrl = Cast(this.dataDoc[this.props.fieldKey], PdfField, null); if (this.props.isSelected() || this.props.Document.scrollY !== undefined) this._everActive = true; - if (pdfUrl && (this._everActive || (this.dataDoc[this.props.fieldKey + "-nativeWidth"] && this.props.ScreenToLocalTransform().Scale < 2.5))) { + if (pdfUrl && (this._everActive || this.props.Document._scrollTop || (this.dataDoc[this.props.fieldKey + "-nativeWidth"] && this.props.ScreenToLocalTransform().Scale < 2.5))) { if (pdfUrl instanceof PdfField && this._pdf) { return this.renderPdfView; } diff --git a/src/client/views/nodes/PresBox.tsx b/src/client/views/nodes/PresBox.tsx index 4180ee255..d43df0bfb 100644 --- a/src/client/views/nodes/PresBox.tsx +++ b/src/client/views/nodes/PresBox.tsx @@ -1,6 +1,6 @@ import React = require("react"); import { library } from '@fortawesome/fontawesome-svg-core'; -import { faArrowLeft, faArrowRight, faEdit, faMinus, faPlay, faPlus, faStop, faTimes } from '@fortawesome/free-solid-svg-icons'; +import { faArrowLeft, faArrowRight, faEdit, faMinus, faPlay, faPlus, faStop, faHandPointLeft, faTimes } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { action, computed, IReactionDisposer, observable, reaction, runInAction } from "mobx"; import { observer } from "mobx-react"; @@ -24,6 +24,7 @@ library.add(faArrowLeft); library.add(faArrowRight); library.add(faPlay); library.add(faStop); +library.add(faHandPointLeft); library.add(faPlus); library.add(faTimes); library.add(faMinus); @@ -340,7 +341,7 @@ export class PresBox extends React.Component<FieldViewProps> { const funcs: ContextMenuProps[] = []; funcs.push({ description: "Show as Slideshow", event: action(() => this.props.Document._viewType = CollectionViewType.Carousel), icon: "asterisk" }); funcs.push({ description: "Show as Timeline", event: action(() => this.props.Document._viewType = CollectionViewType.Time), icon: "asterisk" }); - funcs.push({ description: "Show as List", event: action(() => this.props.Document._viewType = CollectionViewType.Invalid), icon: "asterisk" }); + funcs.push({ description: "Show as List", event: action(() => { this.props.Document._viewType = CollectionViewType.Stacking; this.props.Document._pivotField = undefined; }), icon: "asterisk" }); ContextMenu.Instance.addItem({ description: "Presentation Funcs...", subitems: funcs, icon: "asterisk" }); } @@ -377,6 +378,7 @@ export class PresBox extends React.Component<FieldViewProps> { viewChanged = action((e: React.ChangeEvent) => { //@ts-ignore this.props.Document._viewType = Number(e.target.selectedOptions[0].value); + this.props.Document._viewType === CollectionViewType.Stacking && (this.props.Document._pivotField = undefined); this.updateMinimize(e, Number(this.props.Document._viewType)); }); diff --git a/src/client/views/nodes/RadialMenu.tsx b/src/client/views/nodes/RadialMenu.tsx index 9314a3899..0ffed78de 100644 --- a/src/client/views/nodes/RadialMenu.tsx +++ b/src/client/views/nodes/RadialMenu.tsx @@ -5,6 +5,8 @@ import { RadialMenuItem, RadialMenuProps } from "./RadialMenuItem"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import Measure from "react-measure"; import "./RadialMenu.scss"; +import MobileInkOverlay from "../../../mobile/MobileInkOverlay"; +import MobileInterface from "../../../mobile/MobileInterface"; @observer export class RadialMenu extends React.Component { @@ -23,13 +25,23 @@ export class RadialMenu extends React.Component { @observable private _mouseDown: boolean = false; private _reactionDisposer?: IReactionDisposer; + public used: boolean = false; + + + catchTouch = (te: React.TouchEvent) => { + console.log("caught"); + te.stopPropagation(); + te.preventDefault(); + } @action onPointerDown = (e: PointerEvent) => { this._mouseDown = true; this._mouseX = e.clientX; this._mouseY = e.clientY; + this.used = false; document.addEventListener("pointermove", this.onPointerMove); + } @observable @@ -42,7 +54,6 @@ export class RadialMenu extends React.Component { const deltX = this._mouseX - curX; const deltY = this._mouseY - curY; const scale = Math.hypot(deltY, deltX); - if (scale < 150 && scale > 50) { const rad = Math.atan2(deltY, deltX) + Math.PI; let closest = 0; @@ -62,6 +73,7 @@ export class RadialMenu extends React.Component { } @action onPointerUp = (e: PointerEvent) => { + this.used = true; this._mouseDown = false; const curX = e.clientX; const curY = e.clientY; @@ -83,6 +95,7 @@ export class RadialMenu extends React.Component { @action componentDidMount = () => { + console.log(this._pageX); document.addEventListener("pointerdown", this.onPointerDown); document.addEventListener("pointerup", this.onPointerUp); this.previewcircle(); @@ -98,7 +111,7 @@ export class RadialMenu extends React.Component { @observable private _pageX: number = 0; @observable private _pageY: number = 0; - @observable private _display: boolean = false; + @observable _display: boolean = false; @observable private _yRelativeToTop: boolean = true; @@ -124,35 +137,34 @@ export class RadialMenu extends React.Component { displayMenu = (x: number, y: number) => { //maxX and maxY will change if the UI/font size changes, but will work for any amount //of items added to the menu - - this._pageX = x; - this._pageY = y; + this._mouseX = x; + this._mouseY = y; this._shouldDisplay = true; } - - get pageX() { - const x = this._pageX; - if (x < 0) { - return 0; - } - const width = this._width; - if (x + width > window.innerWidth - RadialMenu.buffer) { - return window.innerWidth - RadialMenu.buffer - width; - } - return x; - } - - get pageY() { - const y = this._pageY; - if (y < 0) { - return 0; - } - const height = this._height; - if (y + height > window.innerHeight - RadialMenu.buffer) { - return window.innerHeight - RadialMenu.buffer - height; - } - return y; - } + // @computed + // get pageX() { + // const x = this._pageX; + // if (x < 0) { + // return 0; + // } + // const width = this._width; + // if (x + width > window.innerWidth - RadialMenu.buffer) { + // return window.innerWidth - RadialMenu.buffer - width; + // } + // return x; + // } + // @computed + // get pageY() { + // const y = this._pageY; + // if (y < 0) { + // return 0; + // } + // const height = this._height; + // if (y + height > window.innerHeight - RadialMenu.buffer) { + // return window.innerHeight - RadialMenu.buffer - height; + // } + // return y; + // } @computed get menuItems() { return this._items.map((item, index) => <RadialMenuItem {...item} key={item.description} closeMenu={this.closeMenu} max={this._items.length} min={index} selected={this._closest} />); @@ -166,7 +178,10 @@ export class RadialMenu extends React.Component { } @action - openMenu = () => { + openMenu = (x: number, y: number) => { + + this._pageX = x; + this._pageY = y; this._shouldDisplay; this._display = true; } @@ -204,15 +219,15 @@ export class RadialMenu extends React.Component { render() { - if (!this._display) { + if (!this._display || MobileInterface.Instance) { return null; } - const style = this._yRelativeToTop ? { left: this._mouseX - 150, top: this._mouseY - 150 } : - { left: this._mouseX - 150, top: this._mouseY - 150 }; + const style = this._yRelativeToTop ? { left: this._pageX - 130, top: this._pageY - 130 } : + { left: this._pageX - 130, top: this._pageY - 130 }; return ( - <div className="radialMenu-cont" style={style}> + <div className="radialMenu-cont" onTouchStart={this.catchTouch} style={style}> <canvas id="newCanvas" style={{ position: "absolute" }} height="300" width="300"> Your browser does not support the HTML5 canvas tag.</canvas> {this.menuItems} </div> diff --git a/src/client/views/nodes/VideoBox.tsx b/src/client/views/nodes/VideoBox.tsx index 69c6f2617..439f2d85f 100644 --- a/src/client/views/nodes/VideoBox.tsx +++ b/src/client/views/nodes/VideoBox.tsx @@ -134,7 +134,7 @@ export class VideoBox extends DocAnnotatableComponent<FieldViewProps, VideoDocum }); imageSummary.isButton = true; this.props.addDocument && this.props.addDocument(imageSummary); - DocUtils.MakeLink({ doc: imageSummary }, { doc: this.props.Document }, "snapshot from " + this.Document.title, "video frame snapshot"); + DocUtils.MakeLink({ doc: imageSummary }, { doc: this.props.Document }, "video snapshot"); } }); } @@ -261,7 +261,6 @@ export class VideoBox extends DocAnnotatableComponent<FieldViewProps, VideoDocum } private get uIButtons() { - const scaling = Math.min(1.8, this.props.ScreenToLocalTransform().Scale); const curTime = (this.Document.currentTimecode || 0); return ([<div className="videoBox-time" key="time" onPointerDown={this.onResetDown} > <span>{"" + Math.round(curTime)}</span> diff --git a/src/client/views/nodes/WebBox.scss b/src/client/views/nodes/WebBox.scss index fbe9bf063..226103a53 100644 --- a/src/client/views/nodes/WebBox.scss +++ b/src/client/views/nodes/WebBox.scss @@ -90,4 +90,19 @@ width: 100%; margin-right: 10px; height: 100%; +} + +.touch-iframe-overlay { + width: 100%; + height: 100%; + position: absolute; + pointer-events: all; + + .indicator { + position: absolute; + + &.active { + background-color: rgba(0, 0, 0, 0.1); + } + } }
\ No newline at end of file diff --git a/src/client/views/nodes/WebBox.tsx b/src/client/views/nodes/WebBox.tsx index 4413ba020..2f8b6167f 100644 --- a/src/client/views/nodes/WebBox.tsx +++ b/src/client/views/nodes/WebBox.tsx @@ -21,6 +21,10 @@ import "./WebBox.scss"; import React = require("react"); import { DocAnnotatableComponent } from "../DocComponent"; import { documentSchema } from "../../../new_fields/documentSchemas"; +import { Id } from "../../../new_fields/FieldSymbols"; +import { DragManager } from "../../util/DragManager"; +import { ImageUtils } from "../../util/Import & Export/ImageUtils"; +import { select } from "async"; library.add(faStickyNote); @@ -34,6 +38,13 @@ export class WebBox extends DocAnnotatableComponent<FieldViewProps, WebDocument> @observable private collapsed: boolean = true; @observable private url: string = "hello"; + private _longPressSecondsHack?: NodeJS.Timeout; + private _iframeRef = React.createRef<HTMLIFrameElement>(); + private _iframeIndicatorRef = React.createRef<HTMLDivElement>(); + private _iframeDragRef = React.createRef<HTMLDivElement>(); + @observable private _pressX: number = 0; + @observable private _pressY: number = 0; + componentDidMount() { const field = Cast(this.props.Document[this.props.fieldKey], WebField); @@ -49,6 +60,14 @@ export class WebBox extends DocAnnotatableComponent<FieldViewProps, WebDocument> } this.setURL(); + + document.addEventListener("pointerup", this.onLongPressUp); + document.addEventListener("pointermove", this.onLongPressMove); + } + + componentWillUnmount() { + document.removeEventListener("pointerup", this.onLongPressUp); + document.removeEventListener("pointermove", this.onLongPressMove); } @action @@ -164,6 +183,107 @@ export class WebBox extends DocAnnotatableComponent<FieldViewProps, WebDocument> } } + onLongPressDown = (e: React.PointerEvent) => { + this._pressX = e.clientX; + this._pressY = e.clientY; + + // find the pressed element in the iframe (currently only works if its an img) + let pressedElement: HTMLElement | undefined; + let pressedBound: ClientRect | undefined; + let selectedText: string = ""; + let pressedImg: boolean = false; + if (this._iframeRef.current) { + const B = this._iframeRef.current.getBoundingClientRect(); + const iframeDoc = this._iframeRef.current.contentDocument; + if (B && iframeDoc) { + // TODO: this only works when scale = 1 as it is currently only inteded for mobile upload + const element = iframeDoc.elementFromPoint(this._pressX - B.left, this._pressY - B.top); + if (element && element.nodeName === "IMG") { + pressedBound = element.getBoundingClientRect(); + pressedElement = element.cloneNode(true) as HTMLElement; + pressedImg = true; + } else { + // check if there is selected text + const text = iframeDoc.getSelection(); + if (text && text.toString().length > 0) { + selectedText = text.toString(); + + // get html of the selected text + const range = text.getRangeAt(0); + const contents = range.cloneContents(); + const div = document.createElement("div"); + div.appendChild(contents); + pressedElement = div; + + pressedBound = range.getBoundingClientRect(); + } + } + } + } + + // mark the pressed element + if (pressedElement && pressedBound) { + if (this._iframeIndicatorRef.current) { + this._iframeIndicatorRef.current.style.top = pressedBound.top + "px"; + this._iframeIndicatorRef.current.style.left = pressedBound.left + "px"; + this._iframeIndicatorRef.current.style.width = pressedBound.width + "px"; + this._iframeIndicatorRef.current.style.height = pressedBound.height + "px"; + this._iframeIndicatorRef.current.classList.add("active"); + } + } + + // start dragging the pressed element if long pressed + this._longPressSecondsHack = setTimeout(() => { + if (pressedImg && pressedElement && pressedBound) { + e.stopPropagation(); + e.preventDefault(); + if (pressedElement.nodeName === "IMG") { + const src = pressedElement.getAttribute("src"); // TODO: may not always work + if (src) { + const doc = Docs.Create.ImageDocument(src); + ImageUtils.ExtractExif(doc); + + // add clone to div so that dragging ghost is placed properly + if (this._iframeDragRef.current) this._iframeDragRef.current.appendChild(pressedElement); + + const dragData = new DragManager.DocumentDragData([doc]); + DragManager.StartDocumentDrag([pressedElement], dragData, this._pressX, this._pressY, { hideSource: true }); + } + } + } else if (selectedText && pressedBound && pressedElement) { + e.stopPropagation(); + e.preventDefault(); + // create doc with the selected text's html + const doc = Docs.Create.HtmlDocument(pressedElement.innerHTML); + + // create dragging ghost with the selected text + if (this._iframeDragRef.current) this._iframeDragRef.current.appendChild(pressedElement); + + // start the drag + const dragData = new DragManager.DocumentDragData([doc]); + DragManager.StartDocumentDrag([pressedElement], dragData, this._pressX - pressedBound.top, this._pressY - pressedBound.top, { hideSource: true }); + } + }, 1500); + } + + onLongPressMove = (e: PointerEvent) => { + // this._pressX = e.clientX; + // this._pressY = e.clientY; + } + + onLongPressUp = (e: PointerEvent) => { + if (this._longPressSecondsHack) { + clearTimeout(this._longPressSecondsHack); + } + if (this._iframeIndicatorRef.current) { + this._iframeIndicatorRef.current.classList.remove("active"); + } + if (this._iframeDragRef.current) { + while (this._iframeDragRef.current.firstChild) this._iframeDragRef.current.removeChild(this._iframeDragRef.current.firstChild); + } + } + + @computed get content() { const field = this.dataDoc[this.props.fieldKey]; @@ -171,9 +291,9 @@ export class WebBox extends DocAnnotatableComponent<FieldViewProps, WebDocument> if (field instanceof HtmlField) { view = <span id="webBox-htmlSpan" dangerouslySetInnerHTML={{ __html: field.html }} />; } else if (field instanceof WebField) { - view = <iframe src={Utils.CorsProxy(field.url.href)} style={{ position: "absolute", width: "100%", height: "100%", top: 0 }} />; + view = <iframe ref={this._iframeRef} src={Utils.CorsProxy(field.url.href)} style={{ position: "absolute", width: "100%", height: "100%", top: 0 }} />; } else { - view = <iframe src={"https://crossorigin.me/https://cs.brown.edu"} style={{ position: "absolute", width: "100%", height: "100%", top: 0 }} />; + view = <iframe ref={this._iframeRef} src={"https://crossorigin.me/https://cs.brown.edu"} style={{ position: "absolute", width: "100%", height: "100%", top: 0 }} />; } const content = <div style={{ width: "100%", height: "100%", position: "absolute" }} onWheel={this.onPostWheel} onPointerDown={this.onPostPointer} onPointerMove={this.onPostPointer} onPointerUp={this.onPostPointer}> @@ -181,15 +301,23 @@ export class WebBox extends DocAnnotatableComponent<FieldViewProps, WebDocument> {view} </div>; - const frozen = !this.props.isSelected() || DocumentDecorations.Instance.Interacting; + const decInteracting = DocumentDecorations.Instance && DocumentDecorations.Instance.Interacting; + + const frozen = !this.props.isSelected() || decInteracting; - const classname = "webBox-cont" + (this.props.isSelected() && InkingControl.Instance.selectedTool === InkTool.None && !DocumentDecorations.Instance.Interacting ? "-interactive" : ""); + const classname = "webBox-cont" + (this.props.isSelected() && InkingControl.Instance.selectedTool === InkTool.None && !decInteracting ? "-interactive" : ""); return ( <> <div className={classname} > {content} </div> - {!frozen ? (null) : <div className="webBox-overlay" onWheel={this.onPreWheel} onPointerDown={this.onPrePointer} onPointerMove={this.onPrePointer} onPointerUp={this.onPrePointer} />} + {!frozen ? (null) : + <div className="webBox-overlay" onWheel={this.onPreWheel} onPointerDown={this.onPrePointer} onPointerMove={this.onPrePointer} onPointerUp={this.onPrePointer}> + <div className="touch-iframe-overlay" onPointerDown={this.onLongPressDown} > + <div className="indicator" ref={this._iframeIndicatorRef}></div> + <div className="dragger" ref={this._iframeDragRef}></div> + </div> + </div>} </>); } render() { |
