diff options
| author | usodhi <61431818+usodhi@users.noreply.github.com> | 2021-01-20 20:07:48 -0500 |
|---|---|---|
| committer | usodhi <61431818+usodhi@users.noreply.github.com> | 2021-01-20 20:07:48 -0500 |
| commit | 7b49bca26f95e7bf1e878d4987f061511a5def39 (patch) | |
| tree | b160034ebfd770c73071203410b468778fa93697 /src/client/views/nodes | |
| parent | e4c9218e217b617e513259cac90b077431890eaa (diff) | |
| parent | 68785a97178d229935c0429791081d7c09312dc3 (diff) | |
Merge branch 'master' of https://github.com/browngraphicslab/Dash-Web into filters
Diffstat (limited to 'src/client/views/nodes')
| -rw-r--r-- | src/client/views/nodes/DocumentView.tsx | 14 | ||||
| -rw-r--r-- | src/client/views/nodes/ImageBox.scss | 10 | ||||
| -rw-r--r-- | src/client/views/nodes/ImageBox.tsx | 43 | ||||
| -rw-r--r-- | src/client/views/nodes/VideoBox.tsx | 45 | ||||
| -rw-r--r-- | src/client/views/nodes/WebBox.tsx | 248 | ||||
| -rw-r--r-- | src/client/views/nodes/formattedText/FormattedTextBox.tsx | 40 | ||||
| -rw-r--r-- | src/client/views/nodes/formattedText/FormattedTextBoxComment.tsx | 2 |
7 files changed, 173 insertions, 229 deletions
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx index 721418ca8..546eca427 100644 --- a/src/client/views/nodes/DocumentView.tsx +++ b/src/client/views/nodes/DocumentView.tsx @@ -553,10 +553,13 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps "Linking to document tabs not yet supported. Drop link on document content."); return; } + if (de.complete.annoDragData) de.complete.annoDragData.annotationDocument = de.complete.annoDragData.annotationDocCreator(); const linkSource = de.complete.annoDragData ? de.complete.annoDragData.annotationDocument : de.complete.linkDragData ? de.complete.linkDragData.linkSourceDocument : undefined; if (linkSource && linkSource !== this.props.Document) { e.stopPropagation(); - de.complete.linkDocument = DocUtils.MakeLink({ doc: linkSource }, { doc: this._componentView?.getAnchor() || this.rootDoc }, "link", undefined, undefined, undefined, [de.x, de.y]); + const dropDoc = this._componentView?.getAnchor() || this.rootDoc; + if (de.complete.annoDragData) de.complete.annoDragData.dropDocument = dropDoc; + de.complete.linkDocument = DocUtils.MakeLink({ doc: linkSource }, { doc: dropDoc }, "link", undefined, undefined, undefined, [de.x, de.y]); } } @@ -892,10 +895,11 @@ export class DocumentView extends React.Component<DocumentViewProps> { @computed get panelWidth() { return this.nativeWidth ? this.nativeWidth * this.nativeScaling : this.props.PanelWidth(); } @computed get panelHeight() { if (this.nativeHeight) { - if (this.props.Document._fitWidth) { - return Math.min(this.props.PanelHeight(), NumCast(this.props.Document.scrollHeight, this.props.PanelHeight())); - } - return Math.min(this.props.PanelHeight(), this.nativeHeight * this.nativeScaling); + return Math.min(this.props.PanelHeight(), + this.props.Document._fitWidth ? + Math.max(NumCast(this.props.Document._height), NumCast((this.props.Document.scrollHeight as number) * this.props.PanelWidth() / this.nativeWidth, this.props.PanelHeight())) : + this.nativeHeight * this.nativeScaling + ); } return this.props.PanelHeight(); } diff --git a/src/client/views/nodes/ImageBox.scss b/src/client/views/nodes/ImageBox.scss index c1b95b308..41055e2db 100644 --- a/src/client/views/nodes/ImageBox.scss +++ b/src/client/views/nodes/ImageBox.scss @@ -5,6 +5,16 @@ position: relative; transform-origin: top left; + + .imageBox-annotationLayer { + position: absolute; + transform-origin: left top; + top: 0; + width: 100%; + pointer-events: none; + mix-blend-mode: multiply; // bcz: makes text fuzzy! + } + .imageBox-fader { pointer-events: inherit; } diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx index b0e7f4ce5..47fa25951 100644 --- a/src/client/views/nodes/ImageBox.tsx +++ b/src/client/views/nodes/ImageBox.tsx @@ -27,6 +27,10 @@ import { FieldView, FieldViewProps } from './FieldView'; import "./ImageBox.scss"; import React = require("react"); import { StyleProp } from '../StyleProvider'; +import { AnchorMenu } from '../pdf/AnchorMenu'; +import { Dictionary } from 'typescript-collections'; +import { MarqueeAnnotator } from '../MarqueeAnnotator'; +import { Annotation } from '../pdf/Annotation'; const path = require('path'); const { Howl } = require('howler'); @@ -63,7 +67,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps, ImageD public static LayoutString(fieldKey: string) { return FieldView.LayoutString(ImageBox, fieldKey); } private _imgRef: React.RefObject<HTMLImageElement> = React.createRef(); private _dropDisposer?: DragManager.DragDropDisposer; - private _pathDisposer?: IReactionDisposer; + private _disposers: { [name: string]: IReactionDisposer } = {}; @observable private _audioState = 0; @observable static _showControls: boolean; @observable uploadIcon = uploadIcons.idle; @@ -74,7 +78,16 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps, ImageD } componentDidMount() { - this._pathDisposer = reaction(() => ({ nativeSize: this.nativeSize, width: this.layoutDoc[WidthSym]() }), + this._disposers.selection = reaction(() => this.props.isSelected(), + selected => { + if (!selected) { + this._savedAnnotations.values().forEach(v => v.forEach(a => a.remove())); + this._savedAnnotations.clear(); + AnchorMenu.Instance.fadeOut(true); + } + }, + { fireImmediately: true }); + this._disposers.path = reaction(() => ({ nativeSize: this.nativeSize, width: this.layoutDoc[WidthSym]() }), action(({ nativeSize, width }) => { if (!this.layoutDoc._height) { this.layoutDoc._height = width * nativeSize.nativeHeight / nativeSize.nativeWidth; @@ -84,7 +97,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps, ImageD } componentWillUnmount() { - this._pathDisposer?.(); + Object.values(this._disposers).forEach(disposer => disposer?.()); } @undoBatch @@ -356,7 +369,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps, ImageD transform = `translate(-100%, 0%) rotate(${rotation}deg) scale(${aspect})`; } - return <div className="imageBox-cont" key={this.layoutDoc[Id]} ref={this.createDropTarget}> + return <div className="imageBox-cont" key={this.layoutDoc[Id]} ref={this.createDropTarget} onPointerDown={this.marqueeDown}> <div className="imageBox-fader" > <img key={this._smallRetryCount + (this._mediumRetryCount << 4) + (this._largeRetryCount << 8)} // force cache to update on retrys src={srcpath} @@ -402,11 +415,28 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps, ImageD screenToLocalTransform = () => this.props.ScreenToLocalTransform().translate(0, -this.ycenter); contentFunc = () => [this.content]; + private _mainCont: React.RefObject<HTMLDivElement> = React.createRef(); + private _annotationLayer: React.RefObject<HTMLDivElement> = React.createRef(); + @observable _marqueeing: number[] | undefined; + @observable _savedAnnotations: Dictionary<number, HTMLDivElement[]> = new Dictionary<number, HTMLDivElement[]>(); + @computed get annotationLayer() { + return <div className="imageBox-annotationLayer" style={{ height: Doc.NativeHeight(this.Document) || undefined }} ref={this._annotationLayer} />; + } + @action + marqueeDown = (e: React.PointerEvent) => { + if (!e.altKey && e.button === 0 && this.active(true)) this._marqueeing = [e.clientX, e.clientY]; + } + @action + finishMarquee = () => { + this._marqueeing = undefined; + this.props.select(true); + } + render() { TraceMobx(); const borderRad = this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.BorderRounding); const borderRadius = borderRad?.includes("px") ? `${Number(borderRad.split("px")[0]) / (this.props.scaling?.() || 1)}px` : borderRad; - return (<div className={`imageBox`} onContextMenu={this.specificContextMenu} + return (<div className={`imageBox`} onContextMenu={this.specificContextMenu} ref={this._mainCont} style={{ width: this.props.PanelWidth() ? undefined : `100%`, height: this.props.PanelWidth() ? undefined : `100%`, @@ -437,6 +467,9 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps, ImageD whenActiveChanged={this.whenActiveChanged}> {this.contentFunc} </CollectionFreeFormView> + {this.annotationLayer} + {!this._marqueeing || !this._mainCont.current || !this._annotationLayer.current ? (null) : + <MarqueeAnnotator rootDoc={this.rootDoc} down={this._marqueeing} scaling={this.props.scaling} addDocument={this.addDocument} finishMarquee={this.finishMarquee} savedAnnotations={this._savedAnnotations} annotationLayer={this._annotationLayer.current} mainCont={this._mainCont.current} />} </div >); } } diff --git a/src/client/views/nodes/VideoBox.tsx b/src/client/views/nodes/VideoBox.tsx index c2c159e4b..55a9818ad 100644 --- a/src/client/views/nodes/VideoBox.tsx +++ b/src/client/views/nodes/VideoBox.tsx @@ -25,6 +25,9 @@ import { LinkDocPreview } from "./LinkDocPreview"; import { FormattedTextBoxComment } from "./formattedText/FormattedTextBoxComment"; import { Transform } from "../../util/Transform"; import { StyleProp } from "../StyleProvider"; +import { Dictionary } from "typescript-collections"; +import { MarqueeAnnotator } from "../MarqueeAnnotator"; +import { AnchorMenu } from "../pdf/AnchorMenu"; const path = require('path'); export const timeSchema = createSchema({ @@ -36,14 +39,16 @@ const VideoDocument = makeInterface(documentSchema, timeSchema); @observer export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps, VideoDocument>(VideoDocument) { static _youtubeIframeCounter: number = 0; - // private _reactionDisposer?: IReactionDisposer; - // private _youtubeReactionDisposer?: IReactionDisposer; private _disposers: { [name: string]: IReactionDisposer } = {}; private _youtubePlayer: YT.Player | undefined = undefined; private _videoRef: HTMLVideoElement | null = null; private _youtubeIframeId: number = -1; private _youtubeContentCreated = false; private _isResetClick = 0; + private _mainCont: React.RefObject<HTMLDivElement> = React.createRef(); + private _annotationLayer: React.RefObject<HTMLDivElement> = React.createRef(); + @observable _savedAnnotations: Dictionary<number, HTMLDivElement[]> = new Dictionary<number, HTMLDivElement[]>(); + @observable _marqueeing: number[] | undefined; @observable _forceCreateYouTubeIFrame = false; @observable _playTimer?: NodeJS.Timeout = undefined; @observable _fullScreen = false; @@ -185,6 +190,15 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps, VideoD } componentDidMount() { + this._disposers.selection = reaction(() => this.props.isSelected(), + selected => { + if (!selected) { + this._savedAnnotations.values().forEach(v => v.forEach(a => a.remove())); + this._savedAnnotations.clear(); + AnchorMenu.Instance.fadeOut(true); + } + }, + { fireImmediately: true }); this._disposers.videoStart = reaction( () => this.Document._videoStart, (videoStart) => { @@ -225,9 +239,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps, VideoD componentWillUnmount() { this.Pause(); - this._disposers.reactionDisposer?.(); - this._disposers.youtubeReactionDisposer?.(); - this._disposers.videoStart?.(); + Object.values(this._disposers).forEach(disposer => disposer?.()); } @action @@ -417,17 +429,29 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps, VideoD screenToLocalTransform = () => this.props.ScreenToLocalTransform(); contentFunc = () => [this.youtubeVideoId ? this.youtubeContent : this.content]; + + @computed get annotationLayer() { + return <div className="imageBox-annotationLayer" style={{ height: Doc.NativeHeight(this.Document) || undefined }} ref={this._annotationLayer} />; + } + + marqueeDown = action((e: React.PointerEvent) => { + if (!e.altKey && e.button === 0 && this.active(true)) this._marqueeing = [e.clientX, e.clientY]; + }) + + finishMarquee = action(() => { + this._marqueeing = undefined; + this.props.select(true); + }) + render() { const borderRad = this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.BorderRounding); const borderRadius = borderRad?.includes("px") ? `${Number(borderRad.split("px")[0]) / (this.props.scaling?.() || 1)}px` : borderRad; - return (<div className="videoBox" onContextMenu={this.specificContextMenu} + return (<div className="videoBox" onContextMenu={this.specificContextMenu} ref={this._mainCont} style={{ - width: "100%", - height: "100%", pointerEvents: this.props.layerProvider?.(this.layoutDoc) === false ? "none" : undefined, borderRadius }} > - <div className="videoBox-viewer" > + <div className="videoBox-viewer" onPointerDown={this.marqueeDown}> <CollectionFreeFormView {...OmitKeys(this.props, ["NativeWidth", "NativeHeight", "setContentView"]).omit} forceScaling={true} fieldKey={this.annotationKey} @@ -446,6 +470,9 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps, VideoD </CollectionFreeFormView> </div> {this.uIButtons} + {this.annotationLayer} + {!this._marqueeing || !this._mainCont.current || !this._annotationLayer.current ? (null) : + <MarqueeAnnotator rootDoc={this.rootDoc} down={this._marqueeing} scaling={this.props.scaling} addDocument={this.addDocumentWithTimestamp} finishMarquee={this.finishMarquee} savedAnnotations={this._savedAnnotations} annotationLayer={this._annotationLayer.current} mainCont={this._mainCont.current} />} </div >); } } diff --git a/src/client/views/nodes/WebBox.tsx b/src/client/views/nodes/WebBox.tsx index 4c4f3f15a..c9c4ed159 100644 --- a/src/client/views/nodes/WebBox.tsx +++ b/src/client/views/nodes/WebBox.tsx @@ -1,9 +1,10 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { Tooltip } from '@material-ui/core'; import { action, computed, IReactionDisposer, observable, reaction, runInAction } from "mobx"; import { observer } from "mobx-react"; import { Dictionary } from "typescript-collections"; import * as WebRequest from 'web-request'; -import { Doc, DocListCast, Opt, AclAddonly, AclEdit, AclAdmin, DataSym, HeightSym, WidthSym } from "../../../fields/Doc"; +import { Doc, DocListCast, HeightSym, Opt, WidthSym } from "../../../fields/Doc"; import { documentSchema } from "../../../fields/documentSchemas"; import { Id } from "../../../fields/FieldSymbols"; import { HtmlField } from "../../../fields/HtmlField"; @@ -12,9 +13,9 @@ import { List } from "../../../fields/List"; import { listSpec, makeInterface } from "../../../fields/Schema"; import { Cast, NumCast, StrCast } from "../../../fields/Types"; import { WebField } from "../../../fields/URLField"; -import { TraceMobx, GetEffectiveAcl } from "../../../fields/util"; -import { addStyleSheet, clearStyleSheetRules, emptyFunction, returnOne, returnZero, Utils, returnTrue, OmitKeys, smoothScroll } from "../../../Utils"; -import { Docs, DocUtils } from "../../documents/Documents"; +import { TraceMobx } from "../../../fields/util"; +import { emptyFunction, OmitKeys, returnOne, smoothScroll, Utils } from "../../../Utils"; +import { Docs } from "../../documents/Documents"; import { DragManager } from "../../util/DragManager"; import { ImageUtils } from "../../util/Import & Export/ImageUtils"; import { undoBatch } from "../../util/UndoManager"; @@ -24,15 +25,11 @@ import { ContextMenuProps } from "../ContextMenuItem"; import { ViewBoxAnnotatableComponent } from "../DocComponent"; import { DocumentDecorations } from "../DocumentDecorations"; import { Annotation } from "../pdf/Annotation"; -import { PDFMenu } from "../pdf/PDFMenu"; -import { PdfViewerMarquee } from "../pdf/PDFViewer"; +import { AnchorMenu } from "../pdf/AnchorMenu"; import { FieldView, FieldViewProps } from './FieldView'; import "./WebBox.scss"; -import "../pdf/PDFViewer.scss"; import React = require("react"); -import { Tooltip } from '@material-ui/core'; -import { CurrentUserUtils } from '../../util/CurrentUserUtils'; -import { FormattedTextBox } from './formattedText/FormattedTextBox'; +import { MarqueeAnnotator } from "../MarqueeAnnotator"; const htmlToText = require("html-to-text"); type WebDocument = makeInterface<[typeof documentSchema]>; @@ -40,32 +37,22 @@ const WebDocument = makeInterface(documentSchema); @observer export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps, WebDocument>(WebDocument) { - private _annotationLayer: React.RefObject<HTMLDivElement> = React.createRef(); - static _annotationStyle: any = addStyleSheet(); public static LayoutString(fieldKey: string) { return FieldView.LayoutString(WebBox, fieldKey); } private _mainCont: React.RefObject<HTMLDivElement> = React.createRef(); - private _startX: number = 0; - private _startY: number = 0; - @observable private _marqueeX: number = 0; - @observable private _marqueeY: number = 0; - @observable private _marqueeWidth: number = 0; - @observable private _marqueeHeight: number = 0; - @observable private _marqueeing: boolean = false; + private _setPreviewCursor: undefined | ((x: number, y: number, drag: boolean) => void); + private _disposers: { [name: string]: IReactionDisposer } = {}; + private _longPressSecondsHack?: NodeJS.Timeout; + private _outerRef = React.createRef<HTMLDivElement>(); + private _iframeIndicatorRef = React.createRef<HTMLDivElement>(); + private _iframeDragRef = React.createRef<HTMLDivElement>(); + @observable private _marqueeing: number[] | undefined; @observable private _url: string = "hello"; @observable private _pressX: number = 0; @observable private _pressY: number = 0; @observable private _iframe: HTMLIFrameElement | null = null; @observable private _savedAnnotations: Dictionary<number, HTMLDivElement[]> = new Dictionary<number, HTMLDivElement[]>(); - private _selectionReactionDisposer?: IReactionDisposer; - private _scrollReactionDisposer?: IReactionDisposer; - private _scrollTopReactionDisposer?: IReactionDisposer; - private _moveReactionDisposer?: IReactionDisposer; - private _longPressSecondsHack?: NodeJS.Timeout; - private _outerRef = React.createRef<HTMLDivElement>(); - private _iframeIndicatorRef = React.createRef<HTMLDivElement>(); - private _iframeDragRef = React.createRef<HTMLDivElement>(); - private _setPreviewCursor: undefined | ((x: number, y: number, drag: boolean) => void); + get scrollHeight() { return this.webpage?.scrollHeight || 1000; } get _collapsed() { return StrCast(this.layoutDoc._chromeStatus) !== "enabled"; } set _collapsed(value) { this.layoutDoc._chromeStatus = !value ? "enabled" : "disabled"; } @@ -99,8 +86,8 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps, WebDocum this.webpage.scrollLeft = NumCast(this.layoutDoc._scrollLeft); } } - this._scrollReactionDisposer?.(); - this._scrollReactionDisposer = reaction(() => ({ scrollY: this.layoutDoc._scrollY, scrollX: this.layoutDoc._scrollX }), + this._disposers.scrollReaction?.(); + this._disposers.scrollReaction = reaction(() => ({ scrollY: this.layoutDoc._scrollY, scrollX: this.layoutDoc._scrollX }), ({ scrollY, scrollX }) => { const delay = this._outerRef.current ? 0 : 250; // wait for mainCont and try again to scroll const durationStr = StrCast(this.Document._viewTransition).match(/([0-9]*)ms/); @@ -118,7 +105,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps, WebDocum }, { fireImmediately: true } ); - this._scrollTopReactionDisposer = reaction(() => this.layoutDoc._scrollTop, + this._disposers.scrollTop = reaction(() => this.layoutDoc._scrollTop, scrollTop => { const durationStr = StrCast(this.Document._viewTransition).match(/([0-9]*)ms/); const duration = durationStr ? Number(durationStr[1]) : 1000; @@ -166,15 +153,15 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps, WebDocum const urlField = Cast(this.dataDoc[this.props.fieldKey], WebField); runInAction(() => this._url = urlField?.url.toString() || ""); - this._moveReactionDisposer = reaction(() => this.layoutDoc.x || this.layoutDoc.y, + this._disposers.scrollMove = reaction(() => this.layoutDoc.x || this.layoutDoc.y, () => this.updateScroll(this.layoutDoc._scrollLeft, this.layoutDoc._scrollTop)); - this._selectionReactionDisposer = reaction(() => this.props.isSelected(), + this._disposers.selection = reaction(() => this.props.isSelected(), selected => { if (!selected) { this._savedAnnotations.values().forEach(v => v.forEach(a => a.remove())); - this._savedAnnotations.keys().forEach(k => this._savedAnnotations.setValue(k, [])); - PDFMenu.Instance.fadeOut(true); + this._savedAnnotations.clear(); + AnchorMenu.Instance.fadeOut(true); } }, { fireImmediately: true }); @@ -202,18 +189,13 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps, WebDocum } componentWillUnmount() { - this._moveReactionDisposer?.(); - this._selectionReactionDisposer?.(); - this._scrollTopReactionDisposer?.(); - this._scrollReactionDisposer?.(); + Object.values(this._disposers).forEach(disposer => disposer?.()); document.removeEventListener("pointerup", this.onLongPressUp); document.removeEventListener("pointermove", this.onLongPressMove); this._iframe?.removeEventListener('wheel', this.iframeWheel); } - onUrlDragover = (e: React.DragEvent) => { - e.preventDefault(); - } + onUrlDragover = (e: React.DragEvent) => { e.preventDefault(); } @undoBatch @action @@ -256,6 +238,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps, WebDocum urlHash(s: string) { return s.split('').reduce((a: any, b: any) => { a = ((a << 5) - a) + b.charCodeAt(0); return a & a; }, 0); } + @action submitURL = () => { if (!this._url.startsWith("http")) this._url = "http://" + this._url; @@ -283,9 +266,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps, WebDocum } onValueKeyDown = async (e: React.KeyboardEvent) => { - if (e.key === "Enter") { - this.submitURL(); - } + if (e.key === "Enter") this.submitURL(); e.stopPropagation(); } @@ -300,22 +281,14 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps, WebDocum } _ignore = 0; - onPreWheel = (e: React.WheelEvent) => { - this._ignore = e.timeStamp; - } - onPrePointer = (e: React.PointerEvent) => { - this._ignore = e.timeStamp; - } + onPreWheel = (e: React.WheelEvent) => { this._ignore = e.timeStamp; } + onPrePointer = (e: React.PointerEvent) => { this._ignore = e.timeStamp; } onPostPointer = (e: React.PointerEvent) => { - if (this._ignore !== e.timeStamp) { - e.stopPropagation(); - } + if (this._ignore !== e.timeStamp) e.stopPropagation(); } onPostWheel = (e: React.WheelEvent) => { - if (this._ignore !== e.timeStamp) { - e.stopPropagation(); - } + if (this._ignore !== e.timeStamp) e.stopPropagation(); } onLongPressDown = (e: React.PointerEvent) => { @@ -431,7 +404,6 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps, WebDocum @computed get urlContent() { - const field = this.dataDoc[this.props.fieldKey]; let view; if (field instanceof HtmlField) { @@ -449,6 +421,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps, WebDocum } return view; } + @computed get content() { const view = this.urlContent; @@ -476,26 +449,9 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps, WebDocum </>); } - - @computed get allAnnotations() { return DocListCast(this.dataDoc[this.props.fieldKey + "-annotations"]); } @computed get nonDocAnnotations() { return this.allAnnotations.filter(a => a.annotations); } - @undoBatch - @action - makeAnnotationDocument = (color: string): Opt<Doc> => { - const scale = this.props.scaling?.() || 1; - if (this._savedAnnotations.size() === 0) return undefined; - const anno = this._savedAnnotations.values()[0][0]; - const annoDoc = Docs.Create.FreeformDocument([], { backgroundColor: color, annotationOn: this.props.Document, title: "Annotation on " + this.Document.title }); - if (anno.style.left) annoDoc.x = parseInt(anno.style.left) / scale; - if (anno.style.top) annoDoc.y = (NumCast(this.layoutDoc._scrollTop) + parseInt(anno.style.top)) / scale; - if (anno.style.height) annoDoc._height = parseInt(anno.style.height) / scale; - if (anno.style.width) annoDoc._width = parseInt(anno.style.width) / scale; - anno.remove(); - this._savedAnnotations.clear(); - return annoDoc; - } @computed get annotationLayer() { TraceMobx(); return <div className="webBox-annotationLayer" style={{ height: Doc.NativeHeight(this.Document) || undefined }} ref={this._annotationLayer}> @@ -504,145 +460,20 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps, WebDocum } </div>; } - @action - createAnnotation = (div: HTMLDivElement, page: number) => { - if (this._annotationLayer.current) { - if (div.style.top) { - div.style.top = (parseInt(div.style.top)).toString(); - } - this._annotationLayer.current.append(div); - div.style.backgroundColor = "#ACCEF7"; - div.style.opacity = "0.5"; - const savedPage = this._savedAnnotations.getValue(page); - if (savedPage) { - savedPage.push(div); - this._savedAnnotations.setValue(page, savedPage); - } - else { - this._savedAnnotations.setValue(page, [div]); - } - } - } @action - highlight = (color: string) => { - // creates annotation documents for current highlights - const effectiveAcl = GetEffectiveAcl(this.props.Document[DataSym]); - const annotationDoc = [AclAddonly, AclEdit, AclAdmin].includes(effectiveAcl) ? this.makeAnnotationDocument(color.replace(/[0-9.]*\)/, "0.3)")) : undefined; - annotationDoc && this.addDocument?.(annotationDoc); - return annotationDoc ?? undefined; - } - /** - * This is temporary for creating annotations from highlights. It will - * start a drag event and create or put the necessary info into the drag event. - */ - @action - startDrag = async (e: PointerEvent, ele: HTMLElement) => { - e.preventDefault(); - e.stopPropagation(); - - const targetDoc = CurrentUserUtils.GetNewTextDoc("Note linked to " + this.props.Document.title, 0, 0, 125, 125); - FormattedTextBox.SelectOnLoad = targetDoc[Id]; - const annotationDoc = this.highlight("rgba(173, 216, 230, 0.35)"); // hyperlink color - if (annotationDoc) { - DragManager.StartPdfAnnoDrag([ele], new DragManager.PdfAnnoDragData(this.props.Document, annotationDoc, targetDoc), e.pageX, e.pageY, { - dragComplete: e => { - if (!e.aborted && e.annoDragData && !e.linkDocument) { - e.linkDocument = DocUtils.MakeLink({ doc: annotationDoc }, { doc: e.annoDragData.dropDocument }, "Annotation"); - annotationDoc.isLinkButton = true; - annotationDoc.isPushpin = e.annoDragData?.dropDocument.annotationOn === this.props.Document; - } - } - }); - } - } - @action onMarqueeDown = (e: React.PointerEvent) => { - this._marqueeing = false; if (!e.altKey && e.button === 0 && this.active(true)) { - // clear out old marquees and initialize menu for new selection - PDFMenu.Instance.StartDrag = this.startDrag; - PDFMenu.Instance.Highlight = this.highlight; - PDFMenu.Instance.Status = "pdf"; - PDFMenu.Instance.fadeOut(true); - this._savedAnnotations.values().forEach(v => v.forEach(a => a.remove())); - this._savedAnnotations.keys().forEach(k => this._savedAnnotations.setValue(k, [])); - if ((e.target as any)?.parentElement.className === "textLayer") { - // start selecting text if mouse down on textLayer spans - } - else if (this._mainCont.current) { - // set marquee x and y positions to the spatially transformed position - const boundingRect = this._mainCont.current.getBoundingClientRect(); - this._startX = this._marqueeX = (e.clientX - boundingRect.left) * (this._mainCont.current.offsetWidth / boundingRect.width); - this._startY = this._marqueeY = (e.clientY - boundingRect.top) * (this._mainCont.current.offsetHeight / boundingRect.height) + this._mainCont.current.scrollTop; - this._marqueeHeight = this._marqueeWidth = 0; - this._marqueeing = true; - } - document.addEventListener("pointermove", this.onSelectMove); - document.addEventListener("pointerup", this.onSelectEnd); - } - } - @action - onSelectMove = (e: PointerEvent): void => { - if (this._marqueeing && this._mainCont.current) { - // transform positions and find the width and height to set the marquee to - const boundingRect = this._mainCont.current.getBoundingClientRect(); - this._marqueeWidth = ((e.clientX - boundingRect.left) * (this._mainCont.current.offsetWidth / boundingRect.width)) - this._startX; - this._marqueeHeight = ((e.clientY - boundingRect.top) * (this._mainCont.current.offsetHeight / boundingRect.height)) - this._startY + this._mainCont.current.scrollTop; - this._marqueeX = Math.min(this._startX, this._startX + this._marqueeWidth); - this._marqueeY = Math.min(this._startY, this._startY + this._marqueeHeight); - this._marqueeWidth = Math.abs(this._marqueeWidth); - this._marqueeHeight = Math.abs(this._marqueeHeight); - e.stopPropagation(); - e.preventDefault(); - } - else if (e.target && (e.target as any).parentElement === this._mainCont.current) { - e.stopPropagation(); + this._marqueeing = [e.clientX, e.clientY]; } } @action - onSelectEnd = (e: PointerEvent): void => { - clearStyleSheetRules(WebBox._annotationStyle); - this._savedAnnotations.clear(); - if (this._marqueeWidth > 10 || this._marqueeHeight > 10) { - const marquees = this._mainCont.current!.getElementsByClassName("pdfViewerDash-dragAnnotationBox"); - if (marquees?.length) { // copy the marquee and convert it to a permanent annotation. - const style = (marquees[0] as HTMLDivElement).style; - const copy = document.createElement("div"); - copy.style.left = style.left; - copy.style.top = style.top; - copy.style.width = style.width; - copy.style.height = style.height; - copy.style.border = style.border; - copy.style.opacity = style.opacity; - (copy as any).marqueeing = true; - copy.className = "webBox-annotationBox"; - this.createAnnotation(copy, 0); - } - - if (!e.ctrlKey) { - PDFMenu.Instance.Marquee = { left: this._marqueeX, top: this._marqueeY, width: this._marqueeWidth, height: this._marqueeHeight }; - } - PDFMenu.Instance.jumpTo(e.clientX, e.clientY); - } - //this._marqueeing = false; - - if (PDFMenu.Instance.Highlighting) {// when highlighter has been toggled when menu is pinned, we auto-highlight immediately on mouse up - this.highlight("rgba(245, 230, 95, 0.616)"); // yellowish highlight color for highlighted text (should match PDFMenu's highlight color) - } - else { - PDFMenu.Instance.StartDrag = this.startDrag; - PDFMenu.Instance.Highlight = this.highlight; - } - document.removeEventListener("pointermove", this.onSelectMove); - document.removeEventListener("pointerup", this.onSelectEnd); + finishMarquee = () => { + this._marqueeing = undefined; + this.props.select(true); } - marqueeWidth = () => this._marqueeWidth; - marqueeHeight = () => this._marqueeHeight; - marqueeX = () => this._marqueeX; - marqueeY = () => this._marqueeY; - marqueeing = () => this._marqueeing; + scrollXf = () => this.props.ScreenToLocalTransform().translate(NumCast(this.layoutDoc._scrollLeft), NumCast(this.layoutDoc._scrollTop)); render() { const inactiveLayer = this.props.layerProvider?.(this.layoutDoc) === false; @@ -655,9 +486,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps, WebDocum {this.content} <div className={"webBox-outerContent"} ref={this._outerRef} style={{ - width: `${100 / scale}%`, - height: `${100 / scale}%`, - transform: `scale(${scale})`, + width: `${100 / scale}%`, height: `${100 / scale}%`, transform: `scale(${scale})`, pointerEvents: this.layoutDoc.isAnnotating && !inactiveLayer ? "all" : "none" }} onWheel={e => { @@ -700,7 +529,8 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps, WebDocum </div> </div> {this.annotationLayer} - <PdfViewerMarquee isMarqueeing={this.marqueeing} width={this.marqueeWidth} height={this.marqueeHeight} x={this.marqueeX} y={this.marqueeY} /> + {!this._marqueeing || !this._mainCont.current || !this._annotationLayer.current ? (null) : + <MarqueeAnnotator rootDoc={this.rootDoc} down={this._marqueeing} scaling={this.props.scaling} addDocument={this.addDocument} finishMarquee={this.finishMarquee} savedAnnotations={this._savedAnnotations} annotationLayer={this._annotationLayer.current} mainCont={this._mainCont.current} />} </div > {this.props.isSelected() ? this.editToggleBtn() : null} </div>); diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx index c129d0204..f9982f747 100644 --- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx +++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx @@ -64,6 +64,8 @@ import { SnappingManager } from '../../../util/SnappingManager'; import { LinkDocPreview } from '../LinkDocPreview'; import { SubCollectionViewProps } from '../../collections/CollectionSubView'; import { StyleProp } from '../../StyleProvider'; +import { AnchorMenu } from '../../pdf/AnchorMenu'; +import { CurrentUserUtils } from '../../../util/CurrentUserUtils'; export interface FormattedTextBoxProps { makeLink?: () => Opt<Doc>; // bcz: hack: notifies the text document when the container has made a link. allows the text doc to react and setup a hyeprlink for any selected text @@ -252,6 +254,43 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp let unchanged = true; const effectiveAcl = GetEffectiveAcl(this.dataDoc); + if (!this._editorView.state.selection.empty) { + runInAction(() => { + AnchorMenu.Instance.Status = "marquee"; + AnchorMenu.Instance.Highlight = action((color: string) => { + this._editorView?.state && RichTextMenu.Instance.insertHighlight(color, this._editorView?.state, this._editorView?.dispatch); + return undefined; + }); + /** + * This function is used by the PDFmenu to create an anchor highlight and a new linked text annotation. + * It also initiates a Drag/Drop interaction to place the text annotation. + */ + AnchorMenu.Instance.StartDrag = action(async (e: PointerEvent, ele: HTMLElement) => { + e.preventDefault(); + e.stopPropagation(); + const targetCreator = () => { + const target = CurrentUserUtils.GetNewTextDoc("Note linked to " + this.rootDoc.title, 0, 0, 100, 100); + FormattedTextBox.SelectOnLoad = target[Id]; + return target; + } + + DragManager.StartAnchorAnnoDrag([ele], new DragManager.AnchorAnnoDragData(this.rootDoc, () => this.rootDoc, targetCreator), e.pageX, e.pageY, { + dragComplete: e => { + if (!e.aborted && e.annoDragData && e.annoDragData.annotationDocument && e.annoDragData.dropDocument && !e.linkDocument) { + e.linkDocument = DocUtils.MakeLink({ doc: e.annoDragData.annotationDocument }, { doc: e.annoDragData.dropDocument }, "hyperlink", "link to note"); + e.annoDragData.annotationDocument.isPushpin = e.annoDragData?.dropDocument.annotationOn === this.rootDoc; + } + e.linkDocument && e.annoDragData?.dropDocument && this.makeLinkToSelection(e.linkDocument[Id], "a link", "add:right", e.annoDragData.dropDocument[Id]); + e.linkDocument && e.annoDragData?.linkDropCallback?.(e as { linkDocument: Doc });// bcz: typescript can't figure out that this is valid even though we tested e.linkDocument + } + }); + }); + }); + const coordsT = this._editorView!.coordsAtPos(this._editorView!.state.selection.to); + const coordsB = this._editorView!.coordsAtPos(this._editorView!.state.selection.to); + AnchorMenu.Instance.jumpTo(Math.min(coordsT.left, coordsB.left), Math.max(coordsT.bottom, coordsB.bottom)); + } + const removeSelection = (json: string | undefined) => { return json?.indexOf("\"storedMarks\"") === -1 ? json?.replace(/"selection":.*/, "") : json?.replace(/"selection":"\"storedMarks\""/, "\"storedMarks\""); }; @@ -960,6 +999,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp this._disposers.selected = reaction(() => this.props.isSelected(), action((selected) => { this._recording = false; + AnchorMenu.Instance.fadeOut(true); if (RichTextMenu.Instance?.view === this._editorView && !selected) { RichTextMenu.Instance?.updateMenu(undefined, undefined, undefined); } diff --git a/src/client/views/nodes/formattedText/FormattedTextBoxComment.tsx b/src/client/views/nodes/formattedText/FormattedTextBoxComment.tsx index 038a91aa3..8867595ff 100644 --- a/src/client/views/nodes/formattedText/FormattedTextBoxComment.tsx +++ b/src/client/views/nodes/formattedText/FormattedTextBoxComment.tsx @@ -255,7 +255,7 @@ export class FormattedTextBoxComment { docTarget && DocServer.GetRefField(docTarget).then(async linkDoc => { if (linkDoc instanceof Doc) { (FormattedTextBoxComment.tooltipText as any).href = href; - FormattedTextBoxComment.linkDoc = DocListCast(textBox.props.Document.links).find(link => link.anchor1 === textBox.props.Document || link.anchor2 === textBox.props.Document ? link : undefined) || linkDoc; + FormattedTextBoxComment.linkDoc = linkDoc; const anchor = FieldValue(Doc.AreProtosEqual(FieldValue(Cast(linkDoc.anchor1, Doc)), textBox.dataDoc) ? Cast(linkDoc.anchor2, Doc) : (Cast(linkDoc.anchor1, Doc)) || linkDoc); const target = anchor?.annotationOn ? await DocCastAsync(anchor.annotationOn) : anchor; if (anchor !== target && anchor && target) { |
