diff options
| author | bobzel <zzzman@gmail.com> | 2023-04-05 22:44:03 -0400 |
|---|---|---|
| committer | bobzel <zzzman@gmail.com> | 2023-04-05 22:44:03 -0400 |
| commit | 9b41da1af16b982ee8ac2fc09f2f8b5d67eac9fb (patch) | |
| tree | bc3f57cd5b31fd453d272c925f6d5b728ab63bae /src/client/views/pdf | |
| parent | 9dae453967183b294bf4f7444b948023a1d52d39 (diff) | |
| parent | 8f7e99641f84ad15f34ba9e4a60b664ac93d2e5d (diff) | |
Merge branch 'master' into data-visualization-view-naafi
Diffstat (limited to 'src/client/views/pdf')
| -rw-r--r-- | src/client/views/pdf/AnchorMenu.tsx | 163 | ||||
| -rw-r--r-- | src/client/views/pdf/Annotation.tsx | 38 | ||||
| -rw-r--r-- | src/client/views/pdf/PDFViewer.scss | 12 | ||||
| -rw-r--r-- | src/client/views/pdf/PDFViewer.tsx | 179 |
4 files changed, 196 insertions, 196 deletions
diff --git a/src/client/views/pdf/AnchorMenu.tsx b/src/client/views/pdf/AnchorMenu.tsx index ee2ae10a7..7392d2706 100644 --- a/src/client/views/pdf/AnchorMenu.tsx +++ b/src/client/views/pdf/AnchorMenu.tsx @@ -11,12 +11,14 @@ import { AntimodeMenu, AntimodeMenuProps } from '../AntimodeMenu'; import { LinkPopup } from '../linking/LinkPopup'; import { ButtonDropdown } from '../nodes/formattedText/RichTextMenu'; import './AnchorMenu.scss'; +import { LightboxView } from '../LightboxView'; @observer export class AnchorMenu extends AntimodeMenu<AntimodeMenuProps> { static Instance: AnchorMenu; private _disposer: IReactionDisposer | undefined; + private _disposer2: IReactionDisposer | undefined; private _commentCont = React.createRef<HTMLButtonElement>(); private _palette = [ 'rgba(208, 2, 27, 0.8)', @@ -36,13 +38,9 @@ export class AnchorMenu extends AntimodeMenu<AntimodeMenuProps> { 'rgba(0, 0, 0, 0.8)', ]; - @observable private _keyValue: string = ''; - @observable private _valueValue: string = ''; - @observable private _added: boolean = false; @observable private highlightColor: string = 'rgba(245, 230, 95, 0.616)'; @observable private _showLinkPopup: boolean = false; - @observable public Highlighting: boolean = false; @observable public Status: 'marquee' | 'annotation' | '' = ''; public onMakeAnchor: () => Opt<Doc> = () => undefined; // Method to get anchor from text search @@ -52,13 +50,13 @@ export class AnchorMenu extends AntimodeMenu<AntimodeMenuProps> { public OnAudio: (e: PointerEvent) => void = unimplementedFunction; public StartDrag: (e: PointerEvent, ele: HTMLElement) => void = unimplementedFunction; public StartCropDrag: (e: PointerEvent, ele: HTMLElement) => void = unimplementedFunction; - public Highlight: (color: string, isPushpin: boolean) => Opt<Doc> = (color: string, isPushpin: boolean) => undefined; - public GetAnchor: (savedAnnotations?: ObservableMap<number, HTMLDivElement[]>) => Opt<Doc> = () => undefined; + public Highlight: (color: string, isTargetToggler: boolean, savedAnnotations?: ObservableMap<number, HTMLDivElement[]>, addAsAnnotation?: boolean) => Opt<Doc> = (color: string, isTargetToggler: boolean) => undefined; + public GetAnchor: (savedAnnotations: Opt<ObservableMap<number, HTMLDivElement[]>>, addAsAnnotation: boolean) => Opt<Doc> = (savedAnnotations: Opt<ObservableMap<number, HTMLDivElement[]>>, addAsAnnotation: boolean) => undefined; public Delete: () => void = unimplementedFunction; - public AddTag: (key: string, value: string) => boolean = returnFalse; public PinToPres: () => void = unimplementedFunction; - public MakePushpin: () => void = unimplementedFunction; - public IsPushpin: () => boolean = returnFalse; + public MakeTargetToggle: () => void = unimplementedFunction; + public ShowTargetTrail: () => void = unimplementedFunction; + public IsTargetToggler: () => boolean = returnFalse; public get Active() { return this._left > 0; } @@ -70,9 +68,19 @@ export class AnchorMenu extends AntimodeMenu<AntimodeMenuProps> { AnchorMenu.Instance._canFade = false; } + componentWillUnmount() { + this._disposer?.(); + this._disposer2?.(); + } + componentDidMount() { + this._disposer2 = reaction( + () => this._opacity, + opacity => !opacity && (this._showLinkPopup = false), + { fireImmediately: true } + ); this._disposer = reaction( - () => SelectionManager.Views(), + () => SelectionManager.Views().slice(), selected => { this._showLinkPopup = false; AnchorMenu.Instance.fadeOut(true); @@ -112,9 +120,7 @@ export class AnchorMenu extends AntimodeMenu<AntimodeMenuProps> { @action highlightClicked = (e: React.MouseEvent) => { - if (!this.Highlight(this.highlightColor, false) && this.Pinned) { - this.Highlighting = !this.Highlighting; - } + this.Highlight(this.highlightColor, false, undefined, true); AnchorMenu.Instance.fadeOut(true); }; @@ -129,7 +135,7 @@ export class AnchorMenu extends AntimodeMenu<AntimodeMenuProps> { const button = ( <button className="antimodeMenu-button anchor-color-preview-button" title="" key="highlighter-button" onClick={this.highlightClicked}> <div className="anchor-color-preview"> - <FontAwesomeIcon icon="highlighter" size="lg" style={{ transition: 'transform 0.1s', transform: this.Highlighting ? '' : 'rotate(-45deg)' }} /> + <FontAwesomeIcon icon="highlighter" size="lg" style={{ transition: 'transform 0.1s', transform: 'rotate(-45deg)' }} /> <div className="color-preview" style={{ backgroundColor: this.highlightColor }}></div> </div> </button> @@ -174,83 +180,62 @@ export class AnchorMenu extends AntimodeMenu<AntimodeMenuProps> { this.highlightColor = Utils.colorString(col); }; - @action keyChanged = (e: React.ChangeEvent<HTMLInputElement>) => { - this._keyValue = e.currentTarget.value; - }; - @action valueChanged = (e: React.ChangeEvent<HTMLInputElement>) => { - this._valueValue = e.currentTarget.value; - }; - @action addTag = (e: React.PointerEvent) => { - if (this._keyValue.length > 0 && this._valueValue.length > 0) { - this._added = this.AddTag(this._keyValue, this._valueValue); - setTimeout( - action(() => (this._added = false)), - 1000 - ); - } - }; - render() { const buttons = - this.Status === 'marquee' - ? [ - this.highlighter, - - <Tooltip key="annotate" title={<div className="dash-tooltip">{'Drag to Place Annotation'}</div>}> - <button className="antimodeMenu-button annotate" ref={this._commentCont} onPointerDown={this.pointerDown} style={{ cursor: 'grab' }}> - <FontAwesomeIcon icon="comment-alt" size="lg" /> - </button> - </Tooltip>, - AnchorMenu.Instance.OnAudio === unimplementedFunction ? ( - <></> - ) : ( - <Tooltip key="annoaudiotate" title={<div className="dash-tooltip">{'Click to Record Annotation'}</div>}> - <button className="antimodeMenu-button annotate" onPointerDown={this.audioDown} style={{ cursor: 'grab' }}> - <FontAwesomeIcon icon="microphone" size="lg" /> - </button> - </Tooltip> - ), - //NOTE: link popup is currently in progress - <Tooltip key="link" title={<div className="dash-tooltip">{'Find document to link to selected text'}</div>}> - <button className="antimodeMenu-button link" onPointerDown={this.toggleLinkPopup} style={{}}> - <FontAwesomeIcon style={{ position: 'absolute', transform: 'scale(1.5)' }} icon={'search'} size="lg" /> - <FontAwesomeIcon style={{ position: 'absolute', transform: 'scale(0.5)', transformOrigin: 'top left', top: 12, left: 12 }} icon={'link'} size="lg" /> - </button> - </Tooltip>, - <LinkPopup key="popup" showPopup={this._showLinkPopup} linkFrom={this.onMakeAnchor} />, - AnchorMenu.Instance.StartCropDrag === unimplementedFunction ? ( - <></> - ) : ( - <Tooltip key="crop" title={<div className="dash-tooltip">{'Click/Drag to create cropped image'}</div>}> - <button className="antimodeMenu-button annotate" onPointerDown={this.cropDown} style={{ cursor: 'grab' }}> - <FontAwesomeIcon icon="image" size="lg" /> - </button> - </Tooltip> - ), - ] - : [ - <Tooltip key="trash" title={<div className="dash-tooltip">{'Remove Link Anchor'}</div>}> - <button className="antimodeMenu-button" onPointerDown={this.Delete}> - <FontAwesomeIcon icon="trash-alt" size="lg" /> - </button> - </Tooltip>, - <Tooltip key="Pin" title={<div className="dash-tooltip">{'Pin to Presentation'}</div>}> - <button className="antimodeMenu-button" onPointerDown={this.PinToPres}> - <FontAwesomeIcon icon="map-pin" size="lg" /> - </button> - </Tooltip>, - <Tooltip key="pushpin" title={<div className="dash-tooltip">{'toggle pushpin behavior'}</div>}> - <button className="antimodeMenu-button" style={{ color: this.IsPushpin() ? 'black' : 'white', backgroundColor: this.IsPushpin() ? 'white' : 'black' }} onPointerDown={this.MakePushpin}> - <FontAwesomeIcon icon="thumbtack" size="lg" /> - </button> - </Tooltip>, - // <div key="7" className="anchorMenu-addTag" > - // <input onChange={this.keyChanged} placeholder="Key" style={{ gridColumn: 1 }} /> - // <input onChange={this.valueChanged} placeholder="Value" style={{ gridColumn: 3 }} /> - // </div>, - // <button key="8" className="antimodeMenu-button" title={`Add tag: ${this._keyValue} with value: ${this._valueValue}`} onPointerDown={this.addTag}> - // <FontAwesomeIcon style={{ transition: "all .2s" }} color={this._added ? "#42f560" : "white"} icon="check" size="lg" /></button>, - ]; + this.Status === 'marquee' ? ( + <> + {this.highlighter} + <Tooltip key="annotate" title={<div className="dash-tooltip">Drag to Place Annotation</div>}> + <button className="antimodeMenu-button annotate" ref={this._commentCont} onPointerDown={this.pointerDown} style={{ cursor: 'grab' }}> + <FontAwesomeIcon icon="comment-alt" size="lg" /> + </button> + </Tooltip> + {AnchorMenu.Instance.OnAudio === unimplementedFunction ? null : ( + <Tooltip key="annoaudiotate" title={<div className="dash-tooltip">Click to Record Annotation</div>}> + <button className="antimodeMenu-button annotate" onPointerDown={this.audioDown} style={{ cursor: 'grab' }}> + <FontAwesomeIcon icon="microphone" size="lg" /> + </button> + </Tooltip> + )} + <Tooltip key="link" title={<div className="dash-tooltip">Find document to link to selected text</div>}> + <button className="antimodeMenu-button link" onPointerDown={this.toggleLinkPopup}> + <FontAwesomeIcon style={{ position: 'absolute', transform: 'scale(1.5)' }} icon={'search'} size="lg" /> + <FontAwesomeIcon style={{ position: 'absolute', transform: 'scale(0.5)', transformOrigin: 'top left', top: 12, left: 12 }} icon={'link'} size="lg" /> + </button> + </Tooltip> + <LinkPopup key="popup" showPopup={this._showLinkPopup} linkCreateAnchor={this.onMakeAnchor} />, + {AnchorMenu.Instance.StartCropDrag === unimplementedFunction ? null : ( + <Tooltip key="crop" title={<div className="dash-tooltip">Click/Drag to create cropped image</div>}> + <button className="antimodeMenu-button annotate" onPointerDown={this.cropDown} style={{ cursor: 'grab' }}> + <FontAwesomeIcon icon="image" size="lg" /> + </button> + </Tooltip> + )} + </> + ) : ( + <> + <Tooltip key="trash" title={<div className="dash-tooltip">Remove Link Anchor</div>}> + <button className="antimodeMenu-button" onPointerDown={this.Delete}> + <FontAwesomeIcon icon="trash-alt" size="lg" /> + </button> + </Tooltip> + <Tooltip key="Pin" title={<div className="dash-tooltip">Pin to Presentation</div>}> + <button className="antimodeMenu-button" onPointerDown={this.PinToPres}> + <FontAwesomeIcon icon="map-pin" size="lg" /> + </button> + </Tooltip> + <Tooltip key="trail" title={<div className="dash-tooltip">Show Linked Trail</div>}> + <button className="antimodeMenu-button" onPointerDown={this.ShowTargetTrail}> + <FontAwesomeIcon icon="taxi" size="lg" /> + </button> + </Tooltip> + <Tooltip key="toggle" title={<div className="dash-tooltip">make target visibility toggle on click</div>}> + <button className="antimodeMenu-button" style={{ color: this.IsTargetToggler() ? 'black' : 'white', backgroundColor: this.IsTargetToggler() ? 'white' : 'black' }} onPointerDown={this.MakeTargetToggle}> + <FontAwesomeIcon icon="thumbtack" size="lg" /> + </button> + </Tooltip> + </> + ); return this.getElement(buttons); } diff --git a/src/client/views/pdf/Annotation.tsx b/src/client/views/pdf/Annotation.tsx index 44f815336..db6b1f011 100644 --- a/src/client/views/pdf/Annotation.tsx +++ b/src/client/views/pdf/Annotation.tsx @@ -1,12 +1,13 @@ import React = require('react'); -import { action, computed, IReactionDisposer, observable, reaction, runInAction } from 'mobx'; +import { action, computed } from 'mobx'; import { observer } from 'mobx-react'; import { Doc, DocListCast, Opt } from '../../../fields/Doc'; import { Id } from '../../../fields/FieldSymbols'; import { List } from '../../../fields/List'; -import { BoolCast, Cast, NumCast, StrCast } from '../../../fields/Types'; +import { BoolCast, Cast, DocCast, NumCast, StrCast } from '../../../fields/Types'; import { LinkFollower } from '../../util/LinkFollower'; import { undoBatch } from '../../util/UndoManager'; +import { OpenWhere } from '../nodes/DocumentView'; import { FieldViewProps } from '../nodes/FieldView'; import { AnchorMenu } from './AnchorMenu'; import './Annotation.scss'; @@ -22,7 +23,7 @@ interface IAnnotationProps extends FieldViewProps { export class Annotation extends React.Component<IAnnotationProps> { render() { return ( - <div> + <div style={{ display: this.props.anno.textCopied && !Doc.isBrushedHighlightedDegree(this.props.anno) ? 'none' : undefined }}> {DocListCast(this.props.anno.textInlineAnnotations).map(a => ( <RegionAnnotation pointerEvents={this.props.pointerEvents} {...this.props} document={a} key={a[Id]} /> ))} @@ -52,12 +53,20 @@ class RegionAnnotation extends React.Component<IRegionAnnotationProps> { }; @undoBatch - pinToPres = () => this.props.pinToPres(this.annoTextRegion); + pinToPres = () => this.props.pinToPres(this.annoTextRegion, {}); @undoBatch - makePushpin = () => (this.annoTextRegion.isPushpin = !this.annoTextRegion.isPushpin); + makeTargretToggle = () => (this.annoTextRegion.followLinkToggle = !this.annoTextRegion.followLinkToggle); - isPushpin = () => BoolCast(this.annoTextRegion.isPushpin); + isTargetToggler = () => BoolCast(this.annoTextRegion.followLinkToggle); + @undoBatch + showTargetTrail = (anchor: Doc) => { + const trail = DocCast(anchor.presTrail); + if (trail) { + Doc.ActivePresentation = trail; + this.props.addDocTab(trail, OpenWhere.replaceRight); + } + }; @action onPointerDown = (e: React.PointerEvent) => { @@ -65,24 +74,18 @@ class RegionAnnotation extends React.Component<IRegionAnnotationProps> { AnchorMenu.Instance.Status = 'annotation'; AnchorMenu.Instance.Delete = this.deleteAnnotation.bind(this); AnchorMenu.Instance.Pinned = false; - AnchorMenu.Instance.AddTag = this.addTag.bind(this); AnchorMenu.Instance.PinToPres = this.pinToPres; - AnchorMenu.Instance.MakePushpin = this.makePushpin; - AnchorMenu.Instance.IsPushpin = this.isPushpin; + AnchorMenu.Instance.MakeTargetToggle = this.makeTargretToggle; + AnchorMenu.Instance.IsTargetToggler = this.isTargetToggler; + AnchorMenu.Instance.ShowTargetTrail = () => this.showTargetTrail(this.annoTextRegion); AnchorMenu.Instance.jumpTo(e.clientX, e.clientY, true); e.stopPropagation(); } else if (e.button === 0) { e.stopPropagation(); - LinkFollower.FollowLink(undefined, this.annoTextRegion, this.props, false); + LinkFollower.FollowLink(undefined, this.annoTextRegion, false); } }; - addTag = (key: string, value: string): boolean => { - const valNum = parseInt(value); - this.annoTextRegion[key] = isNaN(valNum) ? value : valNum; - return true; - }; - render() { const brushed = this.annoTextRegion && Doc.isBrushedHighlightedDegree(this.annoTextRegion); return ( @@ -107,7 +110,8 @@ class RegionAnnotation extends React.Component<IRegionAnnotationProps> { pointerEvents: this.props.pointerEvents?.() as any, outline: brushed === Doc.DocBrushStatus.linkHighlighted ? 'solid 1px lightBlue' : undefined, backgroundColor: brushed === Doc.DocBrushStatus.highlighted ? 'orange' : StrCast(this.props.document.backgroundColor), - }}></div> + }} + /> ); } } diff --git a/src/client/views/pdf/PDFViewer.scss b/src/client/views/pdf/PDFViewer.scss index 822af6a68..470aa3eb1 100644 --- a/src/client/views/pdf/PDFViewer.scss +++ b/src/client/views/pdf/PDFViewer.scss @@ -27,14 +27,14 @@ opacity: unset; mix-blend-mode: multiply; // bcz: makes text fuzzy! - span { - padding-right: 5px; - padding-bottom: 4px; - } + // span { + // padding-right: 5px; + // padding-bottom: 4px; + // } } .textLayer ::selection { - background: #ACCEF7; + background: #accef7; } // should match the backgroundColor in createAnnotation() @@ -111,4 +111,4 @@ .pdfViewerDash-interactive { pointer-events: all; -}
\ No newline at end of file +} diff --git a/src/client/views/pdf/PDFViewer.tsx b/src/client/views/pdf/PDFViewer.tsx index 2c83082b7..6ab541207 100644 --- a/src/client/views/pdf/PDFViewer.tsx +++ b/src/client/views/pdf/PDFViewer.tsx @@ -7,15 +7,16 @@ import { Id } from '../../../fields/FieldSymbols'; import { InkTool } from '../../../fields/InkField'; import { Cast, NumCast, StrCast } from '../../../fields/Types'; import { TraceMobx } from '../../../fields/util'; -import { addStyleSheet, addStyleSheetRule, clearStyleSheetRules, emptyFunction, OmitKeys, smoothScroll, Utils } from '../../../Utils'; +import { addStyleSheet, addStyleSheetRule, clearStyleSheetRules, emptyFunction, returnAll, returnFalse, returnNone, returnTrue, returnZero, smoothScroll, Utils } from '../../../Utils'; import { DocUtils } from '../../documents/Documents'; +import { DragManager } from '../../util/DragManager'; import { SelectionManager } from '../../util/SelectionManager'; import { SharingManager } from '../../util/SharingManager'; import { SnappingManager } from '../../util/SnappingManager'; import { MarqueeOptionsMenu } from '../collections/collectionFreeForm'; import { CollectionFreeFormView } from '../collections/collectionFreeForm/CollectionFreeFormView'; import { MarqueeAnnotator } from '../MarqueeAnnotator'; -import { DocumentViewProps } from '../nodes/DocumentView'; +import { DocFocusOptions, DocumentViewProps } from '../nodes/DocumentView'; import { FieldViewProps } from '../nodes/FieldView'; import { LinkDocPreview } from '../nodes/LinkDocPreview'; import { StyleProp } from '../StyleProvider'; @@ -29,7 +30,7 @@ const _global = (window /* browser */ || global) /* node */ as any; //pdfjsLib.GlobalWorkerOptions.workerSrc = `/assets/pdf.worker.js`; // The workerSrc property shall be specified. -pdfjsLib.GlobalWorkerOptions.workerSrc = 'https://unpkg.com/pdfjs-dist@2.14.305/build/pdf.worker.js'; +pdfjsLib.GlobalWorkerOptions.workerSrc = 'https://unpkg.com/pdfjs-dist@2.16.105/build/pdf.worker.js'; interface IViewerProps extends FieldViewProps { Document: Doc; @@ -63,19 +64,26 @@ export class PDFViewer extends React.Component<IViewerProps> { private _styleRule: any; // stylesheet rule for making hyperlinks clickable private _retries = 0; // number of times tried to create the PDF viewer private _setPreviewCursor: undefined | ((x: number, y: number, drag: boolean, hide: boolean) => void); + private _setBrushViewer: undefined | ((view: { width: number; height: number; panX: number; panY: number }) => void); private _annotationLayer: React.RefObject<HTMLDivElement> = React.createRef(); private _disposers: { [name: string]: IReactionDisposer } = {}; private _viewer: React.RefObject<HTMLDivElement> = React.createRef(); - public _getAnchor: (savedAnnotations?: ObservableMap<number, HTMLDivElement[]>) => Opt<Doc> = () => undefined; _mainCont: React.RefObject<HTMLDivElement> = React.createRef(); private _selectionText: string = ''; + private _selectionContent: DocumentFragment | undefined; private _downX: number = 0; private _downY: number = 0; private _lastSearch = false; private _viewerIsSetup = false; private _ignoreScroll = false; - private _initialScroll: Opt<number>; + private _initialScroll: { loc: Opt<number>; easeFunc: 'linear' | 'ease' | undefined } | undefined; private _forcedScroll = true; + get _getAnchor() { + return AnchorMenu.Instance?.GetAnchor; + } + + selectionText = () => this._selectionText; + selectionContent = () => this._selectionContent; @observable isAnnotating = false; // key where data is stored @@ -127,10 +135,17 @@ export class PDFViewer extends React.Component<IViewerProps> { copy = (e: ClipboardEvent) => { if (this.props.isContentActive() && e.clipboardData) { e.clipboardData.setData('text/plain', this._selectionText); + const anchor = this._getAnchor(undefined, false); + if (anchor) { + anchor.textCopied = true; + e.clipboardData.setData('dash/pdfAnchor', anchor[Id]); + } e.preventDefault(); } }; + @observable _scrollHeight = 0; + @action initialLoad = async () => { if (this._pageSizes.length === 0) { @@ -151,33 +166,32 @@ export class PDFViewer extends React.Component<IViewerProps> { ) ) ); - this.props.Document.scrollHeight = (this._pageSizes.reduce((size, page) => size + page.height, 0) * 96) / 72; } + runInAction(() => (this._scrollHeight = (this._pageSizes.reduce((size, page) => size + page.height, 0) * 96) / 72)); }; + _scrollStopper: undefined | (() => void); + // scrolls to focus on a nested annotation document. if this is part a link preview then it will jump to the scroll location, // otherwise it will scroll smoothly. - scrollFocus = (doc: Doc, smooth: boolean) => { + scrollFocus = (doc: Doc, scrollTop: number, options: DocFocusOptions) => { const mainCont = this._mainCont.current; let focusSpeed: Opt<number>; if (doc !== this.props.rootDoc && mainCont) { const windowHeight = this.props.PanelHeight() / (this.props.NativeDimScaling?.() || 1); - const scrollTo = doc.unrendered ? NumCast(doc.y) : Utils.scrollIntoView(NumCast(doc.y), doc[HeightSym](), NumCast(this.props.layoutDoc._scrollTop), windowHeight, 0.1 * windowHeight, NumCast(this.props.Document.scrollHeight)); + const scrollTo = Utils.scrollIntoView(scrollTop, doc[HeightSym](), NumCast(this.props.layoutDoc._scrollTop), windowHeight, windowHeight * 0.1, this._scrollHeight); if (scrollTo !== undefined && scrollTo !== this.props.layoutDoc._scrollTop) { - focusSpeed = 500; - - if (!this._pdfViewer) this._initialScroll = scrollTo; - else if (smooth) smoothScroll(focusSpeed, mainCont, scrollTo); + if (!this._pdfViewer) this._initialScroll = { loc: scrollTo, easeFunc: options.easeFunc }; + else if (!options.instant) this._scrollStopper = smoothScroll((focusSpeed = options.zoomTime ?? 500), mainCont, scrollTo, options.easeFunc, this._scrollStopper); else this._mainCont.current?.scrollTo({ top: Math.abs(scrollTo || 0) }); } } else { - this._initialScroll = NumCast(this.props.layoutDoc._scrollTop); + this._initialScroll = { loc: NumCast(this.props.layoutDoc._scrollTop), easeFunc: options.easeFunc }; } return focusSpeed; }; - crop = (region: Doc | undefined, addCrop?: boolean) => { - return this.props.crop(region, addCrop); - }; + crop = (region: Doc | undefined, addCrop?: boolean) => this.props.crop(region, addCrop); + brushView = (view: { width: number; height: number; panX: number; panY: number }) => this._setBrushViewer?.(view); @action setupPdfJsViewer = async () => { @@ -196,13 +210,18 @@ export class PDFViewer extends React.Component<IViewerProps> { this.gotoPage(NumCast(this.props.Document._curPage, 1)); } document.removeEventListener('pagesinit', this.pagesinit); - var quickScroll: string | undefined = this._initialScroll ? this._initialScroll.toString() : ''; + var quickScroll: { loc?: string; easeFunc?: 'ease' | 'linear' } | undefined = { loc: this._initialScroll ? this._initialScroll.loc?.toString() : '', easeFunc: this._initialScroll ? this._initialScroll.easeFunc : undefined }; + this._disposers.scale = reaction( + () => NumCast(this.props.layoutDoc._viewScale, 1), + scale => (this._pdfViewer.currentScaleValue = scale), + { fireImmediately: true } + ); this._disposers.scroll = reaction( () => Math.abs(NumCast(this.props.Document._scrollTop)), pos => { if (!this._ignoreScroll) { this._showWaiting && this.setupPdfJsViewer(); - const viewTrans = quickScroll ?? StrCast(this.props.Document._viewTransition); + const viewTrans = quickScroll?.loc ?? StrCast(this.props.Document._viewTransition); const durationMiliStr = viewTrans.match(/([0-9]*)ms/); const durationSecStr = viewTrans.match(/([0-9.]*)s/); const duration = durationMiliStr ? Number(durationMiliStr[1]) : durationSecStr ? Number(durationSecStr[1]) * 1000 : 0; @@ -210,7 +229,7 @@ export class PDFViewer extends React.Component<IViewerProps> { if (duration) { setTimeout( () => { - this._mainCont.current && smoothScroll(duration, this._mainCont.current, pos); + this._mainCont.current && (this._scrollStopper = smoothScroll(duration, this._mainCont.current, pos, this._initialScroll?.easeFunc ?? 'ease', this._scrollStopper)); setTimeout(() => (this._forcedScroll = false), duration); }, this._mainCont.current ? 0 : 250 @@ -225,7 +244,7 @@ export class PDFViewer extends React.Component<IViewerProps> { ); quickScroll = undefined; if (this._initialScroll !== undefined && this._mainCont.current) { - this._mainCont.current?.scrollTo({ top: Math.abs(this._initialScroll || 0) }); + this._mainCont.current?.scrollTo({ top: Math.abs(this._initialScroll?.loc || 0) }); this._initialScroll = undefined; } }; @@ -284,7 +303,7 @@ export class PDFViewer extends React.Component<IViewerProps> { @action scrollToAnnotation = (scrollToAnnotation: Doc) => { if (scrollToAnnotation) { - this.scrollFocus(scrollToAnnotation, true); + this.scrollFocus(scrollToAnnotation, NumCast(scrollToAnnotation.y), { zoomTime: 500 }); Doc.linkFollowHighlight(scrollToAnnotation); } }; @@ -300,7 +319,7 @@ export class PDFViewer extends React.Component<IViewerProps> { this._ignoreScroll = false; if (this._scrollTimer) clearTimeout(this._scrollTimer); // wait until a scrolling pause, then create an anchor to audio this._scrollTimer = setTimeout(() => { - DocUtils.MakeLinkToActiveAudio(() => this.props.DocumentView?.().ComponentView?.getAnchor!()!, false); + DocUtils.MakeLinkToActiveAudio(() => this.props.DocumentView?.().ComponentView?.getAnchor!(true)!, false); this._scrollTimer = undefined; }, 200); } @@ -326,13 +345,13 @@ export class PDFViewer extends React.Component<IViewerProps> { query: searchString, }; if (clear) { - this._pdfViewer?.findController.executeCommand('reset', { query: '' }); + this._pdfViewer?.eventBus.dispatch('reset', {}); } else if (!searchString) { bwd ? this.prevAnnotation() : this.nextAnnotation(); } else if (this._pdfViewer?.pageViewsReady) { - this._pdfViewer.findController.executeCommand('findagain', findOpts); + this._pdfViewer?.eventBus.dispatch('find', { ...findOpts, type: 'again' }); } else if (this._mainCont.current) { - const executeFind = () => this._pdfViewer.findController.executeCommand('find', findOpts); + const executeFind = () => this._pdfViewer?.eventBus.dispatch('find', findOpts); this._mainCont.current.addEventListener('pagesloaded', executeFind); this._mainCont.current.addEventListener('pagerendered', executeFind); } @@ -359,9 +378,9 @@ export class PDFViewer extends React.Component<IViewerProps> { MarqueeAnnotator.clearAnnotations(this._savedAnnotations); this._marqueeing = [e.clientX, e.clientY]; this.isAnnotating = true; - if (e.target && ((e.target as any).className.includes('endOfContent') || (e.target as any).parentElement.className !== 'textLayer')) { + const target = e.target as any; + if (e.target && (target.className.includes('endOfContent') || (target.parentElement.className !== 'textLayer' && target.parentElement.parentElement?.className !== 'textLayer'))) { this._textSelecting = false; - document.addEventListener('pointermove', this.onSelectMove); // need this to prevent document from being dragged if stopPropagation doesn't get called } else { // if textLayer is hit, then we select text instead of using a marquee so clear out the marquee. setTimeout( @@ -371,28 +390,22 @@ export class PDFViewer extends React.Component<IViewerProps> { this._styleRule = addStyleSheetRule(PDFViewer._annotationStyle, 'htmlAnnotation', { 'pointer-events': 'none' }); document.addEventListener('pointerup', this.onSelectEnd); - document.addEventListener('pointermove', this.onSelectMove); } } }; @action finishMarquee = (x?: number, y?: number) => { - this._getAnchor = AnchorMenu.Instance?.GetAnchor; this.isAnnotating = false; this._marqueeing = undefined; this._textSelecting = true; - document.removeEventListener('pointermove', this.onSelectMove); }; - onSelectMove = (e: PointerEvent) => e.stopPropagation(); - @action onSelectEnd = (e: PointerEvent): void => { this.isAnnotating = false; clearStyleSheetRules(PDFViewer._annotationStyle); this.props.select(false); - document.removeEventListener('pointermove', this.onSelectMove); document.removeEventListener('pointerup', this.onSelectEnd); const sel = window.getSelection(); @@ -423,7 +436,8 @@ export class PDFViewer extends React.Component<IViewerProps> { } } } - this._selectionText = selRange.cloneContents().textContent || ''; + this._selectionContent = selRange.cloneContents(); + this._selectionText = this._selectionContent?.textContent || ''; // clear selection if (sel.empty) { @@ -435,11 +449,8 @@ export class PDFViewer extends React.Component<IViewerProps> { } }; - scrollXf = () => { - return this._mainCont.current ? this.props.ScreenToLocalTransform().translate(0, NumCast(this.props.layoutDoc._scrollTop)) : this.props.ScreenToLocalTransform(); - }; - onClick = (e: React.MouseEvent) => { + this._scrollStopper?.(); if (this._setPreviewCursor && e.button === 0 && Math.abs(e.clientX - this._downX) < Utils.DRAG_THRESHOLD && Math.abs(e.clientY - this._downY) < Utils.DRAG_THRESHOLD) { this._setPreviewCursor(e.clientX, e.clientY, false, false); } @@ -447,6 +458,7 @@ export class PDFViewer extends React.Component<IViewerProps> { }; setPreviewCursor = (func?: (x: number, y: number, drag: boolean, hide: boolean) => void) => (this._setPreviewCursor = func); + setBrushViewer = (func?: (view: { width: number; height: number; panX: number; panY: number }) => void) => (this._setBrushViewer = func); @action onZoomWheel = (e: React.WheelEvent) => { @@ -466,6 +478,7 @@ export class PDFViewer extends React.Component<IViewerProps> { <div className="pdfViewerDash-annotationLayer" style={{ height: Doc.NativeHeight(this.props.Document), transform: `scale(${NumCast(this.props.layoutDoc._viewScale, 1)})` }} ref={this._annotationLayer}> {this.inlineTextAnnotations .sort((a, b) => NumCast(a.y) - NumCast(b.y)) + .filter(anno => !anno.hidden) .map(anno => ( <Annotation {...this.props} fieldKey={this.props.fieldKey + '-annotations'} pointerEvents={this.pointerEvents} showInfo={this.showInfo} dataDoc={this.props.dataDoc} anno={anno} key={`${anno[Id]}-annotation`} /> ))} @@ -483,13 +496,14 @@ export class PDFViewer extends React.Component<IViewerProps> { ); } + getScrollHeight = () => this._scrollHeight; showInfo = action((anno: Opt<Doc>) => (this._overlayAnnoInfo = anno)); + scrollXf = () => (this._mainCont.current ? this.props.ScreenToLocalTransform().translate(0, NumCast(this.props.layoutDoc._scrollTop)) : this.props.ScreenToLocalTransform()); overlayTransform = () => this.scrollXf().scale(1 / NumCast(this.props.layoutDoc._viewScale, 1)); - panelWidth = () => this.props.PanelWidth() / (this.props.NativeDimScaling?.() || 1); // (this.Document.scrollHeight || Doc.NativeHeight(this.Document) || 0); - panelHeight = () => this.props.PanelHeight() / (this.props.NativeDimScaling?.() || 1); // () => this._pageSizes.length && this._pageSizes[0] ? this._pageSizes[0].width : Doc.NativeWidth(this.Document); - basicFilter = () => [...this.props.docFilters(), Utils.PropUnsetFilter('textInlineAnnotations')]; + panelWidth = () => this.props.PanelWidth() / (this.props.NativeDimScaling?.() || 1); + panelHeight = () => this.props.PanelHeight() / (this.props.NativeDimScaling?.() || 1); transparentFilter = () => [...this.props.docFilters(), Utils.IsTransparentFilter()]; - opaqueFilter = () => [...this.props.docFilters(), Utils.IsOpaqueFilter()]; + opaqueFilter = () => [...this.props.docFilters(), Utils.noDragsDocFilter, ...(DragManager.docsBeingDragged.length ? [] : [Utils.IsOpaqueFilter()])]; childStyleProvider = (doc: Doc | undefined, props: Opt<DocumentViewProps>, property: string): any => { if (doc instanceof Doc && property === StyleProp.PointerEvents) { if (doc.textInlineAnnotations) return 'none'; @@ -498,56 +512,52 @@ export class PDFViewer extends React.Component<IViewerProps> { return this.props.styleProvider?.(doc, props, property); }; - renderAnnotations = (docFilters?: () => string[], dontRender?: boolean) => ( - <CollectionFreeFormView - {...OmitKeys(this.props, ['NativeWidth', 'NativeHeight', 'setContentView']).omit} - isAnnotationOverlay={true} - fieldKey={this.props.fieldKey + '-annotations'} - setPreviewCursor={this.setPreviewCursor} - PanelHeight={this.panelHeight} - PanelWidth={this.panelWidth} - dropAction={'alias'} - select={emptyFunction} - bringToFront={emptyFunction} - docFilters={docFilters || this.basicFilter} - styleProvider={this.childStyleProvider} - dontRenderDocuments={dontRender} - CollectionView={undefined} - ScreenToLocalTransform={this.overlayTransform} - renderDepth={this.props.renderDepth + 1} - /> + renderAnnotations = (docFilters: () => string[], mixBlendMode?: any, display?: string) => ( + <div + className="pdfViewerDash-overlay" + style={{ + mixBlendMode: mixBlendMode, + display: display, + transform: `scale(${NumCast(this.props.layoutDoc._viewScale, 1)})`, + pointerEvents: Doc.ActiveTool !== InkTool.None ? 'all' : undefined, + }}> + <CollectionFreeFormView + {...this.props} + NativeWidth={returnZero} + NativeHeight={returnZero} + setContentView={emptyFunction} // override setContentView to do nothing + pointerEvents={SnappingManager.GetIsDragging() ? returnAll : returnNone} + renderDepth={this.props.renderDepth + 1} + isAnnotationOverlay={true} + fieldKey={this.props.fieldKey + '-annotations'} + CollectionView={undefined} + getScrollHeight={this.getScrollHeight} + setPreviewCursor={this.setPreviewCursor} + setBrushViewer={this.setBrushViewer} + PanelHeight={this.panelHeight} + PanelWidth={this.panelWidth} + ScreenToLocalTransform={this.overlayTransform} + isAnyChildContentActive={returnFalse} + isAnnotationOverlayScrollable={true} + dropAction={'alias'} + docFilters={docFilters} + select={emptyFunction} + bringToFront={emptyFunction} + styleProvider={this.childStyleProvider} + /> + </div> ); @computed get overlayTransparentAnnotations() { - return this.renderAnnotations(this.transparentFilter, false); + return this.renderAnnotations(this.transparentFilter, 'multiply', DragManager.docsBeingDragged.length ? 'none' : undefined); } @computed get overlayOpaqueAnnotations() { - return this.renderAnnotations(this.opaqueFilter, false); - } - @computed get overlayClickableAnnotations() { - return <div style={{ height: NumCast(this.props.rootDoc.scrollHeight) }}>{this.renderAnnotations(undefined, true)}</div>; + return this.renderAnnotations(this.opaqueFilter, this.allAnnotations.some(anno => anno.mixBlendMode) ? 'hard-light' : undefined); } @computed get overlayLayer() { return ( <div style={{ pointerEvents: SnappingManager.GetIsDragging() ? 'all' : 'none' }}> - <div - className="pdfViewerDash-overlay" - style={{ - pointerEvents: SnappingManager.GetIsDragging() ? 'all' : 'none', - mixBlendMode: 'multiply', - transform: `scale(${NumCast(this.props.layoutDoc._viewScale, 1)})`, - }}> - {this.overlayTransparentAnnotations} - </div> - <div - className="pdfViewerDash-overlay" - style={{ - pointerEvents: SnappingManager.GetIsDragging() ? 'all' : 'none', - mixBlendMode: this.allAnnotations.some(anno => anno.mixBlendMode) ? 'hard-light' : undefined, - transform: `scale(${NumCast(this.props.layoutDoc._viewScale, 1)})`, - }}> - {this.overlayOpaqueAnnotations} - {this.overlayClickableAnnotations} - </div> + {this.overlayTransparentAnnotations} + {this.overlayOpaqueAnnotations} </div> ); } @@ -586,6 +596,7 @@ export class PDFViewer extends React.Component<IViewerProps> { docView={this.props.docViewPath().lastElement()} finishMarquee={this.finishMarquee} savedAnnotations={this.savedAnnotations} + selectionText={this.selectionText} annotationLayer={this._annotationLayer.current} mainCont={this._mainCont.current} anchorMenuCrop={this._textSelecting ? undefined : this.crop} |
