diff options
Diffstat (limited to 'src/client/views/pdf/PDFViewer.tsx')
| -rw-r--r-- | src/client/views/pdf/PDFViewer.tsx | 574 |
1 files changed, 289 insertions, 285 deletions
diff --git a/src/client/views/pdf/PDFViewer.tsx b/src/client/views/pdf/PDFViewer.tsx index 1856c5353..2c83082b7 100644 --- a/src/client/views/pdf/PDFViewer.tsx +++ b/src/client/views/pdf/PDFViewer.tsx @@ -1,39 +1,35 @@ -import { action, computed, IReactionDisposer, observable, ObservableMap, reaction, runInAction } from "mobx"; -import { observer } from "mobx-react"; -import * as Pdfjs from "pdfjs-dist"; -import "pdfjs-dist/web/pdf_viewer.css"; -import { Doc, DocListCast, Field, HeightSym, Opt, WidthSym } from "../../../fields/Doc"; -import { Id } from "../../../fields/FieldSymbols"; -import { InkTool } from "../../../fields/InkField"; -import { Cast, NumCast, ScriptCast, StrCast } from "../../../fields/Types"; -import { PdfField } from "../../../fields/URLField"; -import { TraceMobx } from "../../../fields/util"; -import { addStyleSheet, addStyleSheetRule, clearStyleSheetRules, emptyFunction, OmitKeys, smoothScroll, Utils } from "../../../Utils"; -import { DocUtils } from "../../documents/Documents"; -import { Networking } from "../../Network"; -import { CurrentUserUtils } from "../../util/CurrentUserUtils"; -import { CompiledScript, CompileScript } from "../../util/Scripting"; -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 { FieldViewProps } from "../nodes/FieldView"; -import { LinkDocPreview } from "../nodes/LinkDocPreview"; -import { StyleProp } from "../StyleProvider"; -import { AnchorMenu } from "./AnchorMenu"; -import { Annotation } from "./Annotation"; -import "./PDFViewer.scss"; -import React = require("react"); -const PDFJSViewer = require("pdfjs-dist/web/pdf_viewer"); -const pdfjsLib = require("pdfjs-dist"); -const _global = (window /* browser */ || global /* node */) as any; +import { action, computed, IReactionDisposer, observable, ObservableMap, reaction, runInAction } from 'mobx'; +import { observer } from 'mobx-react'; +import * as Pdfjs from 'pdfjs-dist'; +import 'pdfjs-dist/web/pdf_viewer.css'; +import { Doc, DocListCast, Field, HeightSym, Opt } from '../../../fields/Doc'; +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 { DocUtils } from '../../documents/Documents'; +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 { FieldViewProps } from '../nodes/FieldView'; +import { LinkDocPreview } from '../nodes/LinkDocPreview'; +import { StyleProp } from '../StyleProvider'; +import { AnchorMenu } from './AnchorMenu'; +import { Annotation } from './Annotation'; +import './PDFViewer.scss'; +import React = require('react'); +const PDFJSViewer = require('pdfjs-dist/web/pdf_viewer'); +const pdfjsLib = require('pdfjs-dist'); +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.13.216/build/pdf.worker.js"; +pdfjsLib.GlobalWorkerOptions.workerSrc = 'https://unpkg.com/pdfjs-dist@2.14.305/build/pdf.worker.js'; interface IViewerProps extends FieldViewProps { Document: Doc; @@ -43,11 +39,10 @@ interface IViewerProps extends FieldViewProps { fieldKey: string; pdf: Pdfjs.PDFDocumentProxy; url: string; - startupLive: boolean; loaded?: (nw: number, nh: number, np: number) => void; setPdfViewer: (view: PDFViewer) => void; - ContentScaling?: () => number; anchorMenuClick?: () => undefined | ((anchor: Doc) => void); + crop: (region: Doc | undefined, addCrop?: boolean) => Doc | undefined; } /** @@ -56,14 +51,11 @@ interface IViewerProps extends FieldViewProps { @observer export class PDFViewer extends React.Component<IViewerProps> { static _annotationStyle: any = addStyleSheet(); - @observable private _pageSizes: { width: number, height: number }[] = []; + @observable private _pageSizes: { width: number; height: number }[] = []; @observable private _savedAnnotations = new ObservableMap<number, HTMLDivElement[]>(); - @observable private _script: CompiledScript = CompileScript("return true") as CompiledScript; @observable private _marqueeing: number[] | undefined; @observable private _textSelecting = true; @observable private _showWaiting = true; - @observable private _showCover = false; - @observable private _zoomed = 1; @observable private _overlayAnnoInfo: Opt<Doc>; @observable private Index: number = -1; @@ -74,105 +66,94 @@ export class PDFViewer extends React.Component<IViewerProps> { private _annotationLayer: React.RefObject<HTMLDivElement> = React.createRef(); private _disposers: { [name: string]: IReactionDisposer } = {}; private _viewer: React.RefObject<HTMLDivElement> = React.createRef(); - private _mainCont: React.RefObject<HTMLDivElement> = React.createRef(); - private _selectionText: string = ""; + public _getAnchor: (savedAnnotations?: ObservableMap<number, HTMLDivElement[]>) => Opt<Doc> = () => undefined; + _mainCont: React.RefObject<HTMLDivElement> = React.createRef(); + private _selectionText: string = ''; private _downX: number = 0; private _downY: number = 0; - private _coverPath: any; private _lastSearch = false; private _viewerIsSetup = false; private _ignoreScroll = false; private _initialScroll: Opt<number>; private _forcedScroll = true; - + @observable isAnnotating = false; // key where data is stored @computed get allAnnotations() { - return DocUtils.FilterDocs(DocListCast(this.props.dataDoc[this.props.fieldKey + "-annotations"]), this.props.docFilters(), this.props.docRangeFilters(), undefined); + return DocUtils.FilterDocs(DocListCast(this.props.dataDoc[this.props.fieldKey + '-annotations']), this.props.docFilters(), this.props.docRangeFilters(), undefined); + } + @computed get inlineTextAnnotations() { + return this.allAnnotations.filter(a => a.textInlineAnnotations); } - @computed get inlineTextAnnotations() { return this.allAnnotations.filter(a => a.textInlineAnnotations); } componentDidMount = async () => { - // change the address to be the file address of the PNG version of each page - // file address of the pdf - const { url: { href } } = Cast(this.props.dataDoc[this.props.fieldKey], PdfField)!; - const { url: relative } = this.props; - if (relative.includes("/pdfs/")) { - const pathComponents = relative.split("/pdfs/")[1].split("/"); - const coreFilename = pathComponents.pop()!.split(".")[0]; - const params: any = { - coreFilename, - pageNum: Math.min(this.props.pdf.numPages, Math.max(1, NumCast(this.props.Document._curPage, 1))), - }; - if (pathComponents.length) { - params.subtree = `${pathComponents.join("/")}/`; - } - this._coverPath = href.startsWith(window.location.origin) ? await Networking.PostToServer("/thumbnail", params) : { width: 100, height: 100, path: "" }; - } else { - const params: any = { - coreFilename: relative.split("/")[relative.split("/").length - 1], - pageNum: Math.min(this.props.pdf.numPages, Math.max(1, NumCast(this.props.Document._curPage, 1))), - }; - this._coverPath = "http://cs.brown.edu/~bcz/face.gif";//href.startsWith(window.location.origin) ? await Networking.PostToServer("/thumbnail", params) : { width: 100, height: 100, path: "" }; - } - runInAction(() => this._showWaiting = true); - this.props.startupLive && this.setupPdfJsViewer(); - this._mainCont.current?.addEventListener("scroll", e => (e.target as any).scrollLeft = 0); + runInAction(() => (this._showWaiting = true)); + this.setupPdfJsViewer(); + this._mainCont.current?.addEventListener('scroll', e => ((e.target as any).scrollLeft = 0)); - this._disposers.autoHeight = reaction(() => this.props.layoutDoc._autoHeight, + this._disposers.autoHeight = reaction( + () => this.props.layoutDoc._autoHeight, autoHeight => { if (autoHeight) { - this.props.layoutDoc._nativeHeight = NumCast(this.props.Document[this.props.fieldKey + "-nativeHeight"]); - this.props.setHeight(NumCast(this.props.Document[this.props.fieldKey + "-nativeHeight"]) * (this.props.scaling?.() || 1)); + this.props.layoutDoc._nativeHeight = NumCast(this.props.Document[this.props.fieldKey + '-nativeHeight']); + this.props.setHeight?.(NumCast(this.props.Document[this.props.fieldKey + '-nativeHeight']) * (this.props.NativeDimScaling?.() || 1)); } - }); + } + ); - this._disposers.selected = reaction(() => this.props.isSelected(), + this._disposers.selected = reaction( + () => this.props.isSelected(), selected => { // if (!selected) { // Array.from(this._savedAnnotations.values()).forEach(v => v.forEach(a => a.remove())); // Array.from(this._savedAnnotations.keys()).forEach(k => this._savedAnnotations.set(k, [])); // } - (SelectionManager.Views().length === 1) && this.setupPdfJsViewer(); + SelectionManager.Views().length === 1 && this.setupPdfJsViewer(); }, - { fireImmediately: true }); - this._disposers.curPage = reaction(() => Cast(this.props.Document._curPage, "number", null), - (page) => page !== undefined && page !== this._pdfViewer?.currentPageNumber && this.gotoPage(page), { fireImmediately: true } ); - } + this._disposers.curPage = reaction( + () => Cast(this.props.Document._curPage, 'number', null), + page => page !== undefined && page !== this._pdfViewer?.currentPageNumber && this.gotoPage(page), + { fireImmediately: true } + ); + }; componentWillUnmount = () => { Object.values(this._disposers).forEach(disposer => disposer?.()); - document.removeEventListener("copy", this.copy); - } + document.removeEventListener('copy', this.copy); + }; copy = (e: ClipboardEvent) => { if (this.props.isContentActive() && e.clipboardData) { - e.clipboardData.setData("text/plain", this._selectionText); + e.clipboardData.setData('text/plain', this._selectionText); e.preventDefault(); } - } + }; @action initialLoad = async () => { if (this._pageSizes.length === 0) { - this._pageSizes = Array<{ width: number, height: number }>(this.props.pdf.numPages); - await Promise.all(this._pageSizes.map((val, i) => - this.props.pdf.getPage(i + 1).then(action((page: Pdfjs.PDFPageProxy) => { - const page0or180 = page.rotate === 0 || page.rotate === 180; - this._pageSizes.splice(i, 1, { - width: (page.view[page0or180 ? 2 : 3] - page.view[page0or180 ? 0 : 1]), - height: (page.view[page0or180 ? 3 : 2] - page.view[page0or180 ? 1 : 0]) - }); - if (i === this.props.pdf.numPages - 1) { - this.props.loaded?.(page.view[page0or180 ? 2 : 3] - page.view[page0or180 ? 0 : 1], - page.view[page0or180 ? 3 : 2] - page.view[page0or180 ? 1 : 0], i); - } - })))); - this.props.Document.scrollHeight = this._pageSizes.reduce((size, page) => size + page.height, 0) * 96 / 72; + this._pageSizes = Array<{ width: number; height: number }>(this.props.pdf.numPages); + await Promise.all( + this._pageSizes.map((val, i) => + this.props.pdf.getPage(i + 1).then( + action((page: Pdfjs.PDFPageProxy) => { + const page0or180 = page.rotate === 0 || page.rotate === 180; + this._pageSizes.splice(i, 1, { + width: page.view[page0or180 ? 2 : 3] - page.view[page0or180 ? 0 : 1], + height: page.view[page0or180 ? 3 : 2] - page.view[page0or180 ? 1 : 0], + }); + if (i === this.props.pdf.numPages - 1) { + this.props.loaded?.(page.view[page0or180 ? 2 : 3] - page.view[page0or180 ? 0 : 1], page.view[page0or180 ? 3 : 2] - page.view[page0or180 ? 1 : 0], this.props.pdf.numPages); + } + }) + ) + ) + ); + this.props.Document.scrollHeight = (this._pageSizes.reduce((size, page) => size + page.height, 0) * 96) / 72; } - } + }; // 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. @@ -180,9 +161,9 @@ export class PDFViewer extends React.Component<IViewerProps> { const mainCont = this._mainCont.current; let focusSpeed: Opt<number>; if (doc !== this.props.rootDoc && mainCont) { - const windowHeight = this.props.PanelHeight() / (this.props.scaling?.() || 1); - const scrollTo = doc.unrendered ? NumCast(doc.y) : Utils.scrollIntoView(NumCast(doc.y), doc[HeightSym](), NumCast(this.props.layoutDoc._scrollTop), windowHeight, .1 * windowHeight); - if (scrollTo !== undefined) { + 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)); + if (scrollTo !== undefined && scrollTo !== this.props.layoutDoc._scrollTop) { focusSpeed = 500; if (!this._pdfViewer) this._initialScroll = scrollTo; @@ -193,7 +174,10 @@ export class PDFViewer extends React.Component<IViewerProps> { this._initialScroll = NumCast(this.props.layoutDoc._scrollTop); } return focusSpeed; - } + }; + crop = (region: Doc | undefined, addCrop?: boolean) => { + return this.props.crop(region, addCrop); + }; @action setupPdfJsViewer = async () => { @@ -203,42 +187,34 @@ export class PDFViewer extends React.Component<IViewerProps> { this.props.setPdfViewer(this); await this.initialLoad(); - this._disposers.filterScript = reaction( - () => ScriptCast(this.props.Document.filterScript), - action(scriptField => { - const oldScript = this._script.originalScript; - this._script = scriptField?.script.compiled ? scriptField.script : CompileScript("return true") as CompiledScript; - if (this._script.originalScript !== oldScript) { - this.Index = -1; - } - }), - { fireImmediately: true }); - this.createPdfViewer(); - } + }; pagesinit = () => { if (this._pdfViewer._setDocumentViewerElement?.offsetParent) { - runInAction(() => this._pdfViewer.currentScaleValue = this._zoomed = 1); + runInAction(() => (this._pdfViewer.currentScaleValue = this.props.layoutDoc._viewScale = 1)); this.gotoPage(NumCast(this.props.Document._curPage, 1)); } - document.removeEventListener("pagesinit", this.pagesinit); - var quickScroll: string | undefined = this._initialScroll ? this._initialScroll.toString() : ""; + document.removeEventListener('pagesinit', this.pagesinit); + var quickScroll: string | undefined = this._initialScroll ? this._initialScroll.toString() : ''; this._disposers.scroll = reaction( () => Math.abs(NumCast(this.props.Document._scrollTop)), - (pos) => { + pos => { if (!this._ignoreScroll) { - (this._showCover || this._showWaiting) && this.setupPdfJsViewer(); + this._showWaiting && this.setupPdfJsViewer(); const viewTrans = quickScroll ?? 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; this._forcedScroll = true; if (duration) { - setTimeout(() => { - this._mainCont.current && smoothScroll(duration, this._mainCont.current, pos); - setTimeout(() => this._forcedScroll = false, duration); - }, this._mainCont.current ? 0 : 250); // wait for mainCont and try again to scroll + setTimeout( + () => { + this._mainCont.current && smoothScroll(duration, this._mainCont.current, pos); + setTimeout(() => (this._forcedScroll = false), duration); + }, + this._mainCont.current ? 0 : 250 + ); // wait for mainCont and try again to scroll } else { this._mainCont.current?.scrollTo({ top: pos }); this._forcedScroll = false; @@ -252,23 +228,27 @@ export class PDFViewer extends React.Component<IViewerProps> { this._mainCont.current?.scrollTo({ top: Math.abs(this._initialScroll || 0) }); this._initialScroll = undefined; } - } + }; createPdfViewer() { - if (!this._mainCont.current) { // bcz: I don't think this is ever triggered or needed - console.log("PDFViewer- I guess we got here"); + if (!this._mainCont.current) { + // bcz: I don't think this is ever triggered or needed + console.log('PDFViewer- I guess we got here'); if (this._retries < 5) { this._retries++; - console.log("PDFViewer- retry num:" + this._retries); + console.log('PDFViewer- retry num:' + this._retries); setTimeout(() => this.createPdfViewer(), 1000); } return; } - document.removeEventListener("copy", this.copy); - document.addEventListener("copy", this.copy); + document.removeEventListener('copy', this.copy); + document.addEventListener('copy', this.copy); const eventBus = new PDFJSViewer.EventBus(true); - eventBus._on("pagesinit", this.pagesinit); - eventBus._on("pagerendered", action(() => this._showWaiting = false)); + eventBus._on('pagesinit', this.pagesinit); + eventBus._on( + 'pagerendered', + action(() => (this._showWaiting = false)) + ); const pdfLinkService = new PDFJSViewer.PDFLinkService({ eventBus }); const pdfFindController = new PDFJSViewer.PDFFindController({ linkService: pdfLinkService, eventBus }); this._pdfViewer = new PDFJSViewer.PDFViewer({ @@ -276,33 +256,30 @@ export class PDFViewer extends React.Component<IViewerProps> { viewer: this._viewer.current, linkService: pdfLinkService, findController: pdfFindController, - renderer: "canvas", - eventBus + renderer: 'canvas', + eventBus, }); pdfLinkService.setViewer(this._pdfViewer); pdfLinkService.setDocument(this.props.pdf, null); this._pdfViewer.setDocument(this.props.pdf); } - @action prevAnnotation = () => { this.Index = Math.max(this.Index - 1, 0); this.scrollToAnnotation(this.allAnnotations.sort((a, b) => NumCast(a.y) - NumCast(b.y))[this.Index]); - } + }; @action nextAnnotation = () => { this.Index = Math.min(this.Index + 1, this.allAnnotations.length - 1); this.scrollToAnnotation(this.allAnnotations.sort((a, b) => NumCast(a.y) - NumCast(b.y))[this.Index]); - } + }; @action gotoPage = (p: number) => { - if (this._pdfViewer?._setDocumentViewerElement?.offsetParent) { - this._pdfViewer?.scrollPageIntoView({ pageNumber: Math.min(Math.max(1, p), this._pageSizes.length) }); - } - } + this._pdfViewer?.scrollPageIntoView({ pageNumber: Math.min(Math.max(1, p), this._pageSizes.length) }); + }; @action scrollToAnnotation = (scrollToAnnotation: Doc) => { @@ -310,7 +287,9 @@ export class PDFViewer extends React.Component<IViewerProps> { this.scrollFocus(scrollToAnnotation, true); Doc.linkFollowHighlight(scrollToAnnotation); } - } + }; + + @observable private _scrollTimer: any; onScroll = (e: React.UIEvent<HTMLElement>) => { if (this._mainCont.current && !this._forcedScroll) { @@ -319,8 +298,13 @@ export class PDFViewer extends React.Component<IViewerProps> { this.props.layoutDoc._scrollTop = this._mainCont.current.scrollTop; } 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); + this._scrollTimer = undefined; + }, 200); } - } + }; // get the page index that the vertical offset passed in is on getPageFromScroll = (vOffset: number) => { @@ -330,7 +314,7 @@ export class PDFViewer extends React.Component<IViewerProps> { currOffset -= this._pageSizes[index++].height; } return index; - } + }; @action search = (searchString: string, bwd?: boolean, clear: boolean = false) => { @@ -339,22 +323,21 @@ export class PDFViewer extends React.Component<IViewerProps> { findPrevious: bwd, highlightAll: true, phraseSearch: true, - query: searchString + query: searchString, }; if (clear) { - this._pdfViewer?.findController.executeCommand('reset', { query: "" }); + this._pdfViewer?.findController.executeCommand('reset', { query: '' }); } else if (!searchString) { bwd ? this.prevAnnotation() : this.nextAnnotation(); } else if (this._pdfViewer?.pageViewsReady) { this._pdfViewer.findController.executeCommand('findagain', findOpts); - } - else if (this._mainCont.current) { + } else if (this._mainCont.current) { const executeFind = () => this._pdfViewer.findController.executeCommand('find', findOpts); - this._mainCont.current.addEventListener("pagesloaded", executeFind); - this._mainCont.current.addEventListener("pagerendered", executeFind); + this._mainCont.current.addEventListener('pagesloaded', executeFind); + this._mainCont.current.addEventListener('pagerendered', executeFind); } return true; - } + }; @action onPointerDown = (e: React.PointerEvent): void => { @@ -371,46 +354,53 @@ export class PDFViewer extends React.Component<IViewerProps> { if ((e.button !== 0 || e.altKey) && this.props.isContentActive(true)) { this._setPreviewCursor?.(e.clientX, e.clientY, true, false); } - if (!e.altKey && e.button === 0 && this.props.isContentActive(true) && ![InkTool.Highlighter, InkTool.Pen].includes(CurrentUserUtils.SelectedTool)) { + if (!e.altKey && e.button === 0 && this.props.isContentActive(true) && ![InkTool.Highlighter, InkTool.Pen, InkTool.Write].includes(Doc.ActiveTool)) { this.props.select(false); MarqueeAnnotator.clearAnnotations(this._savedAnnotations); this._marqueeing = [e.clientX, e.clientY]; - if (e.target && ((e.target as any).className.includes("endOfContent") || ((e.target as any).parentElement.className !== "textLayer"))) { + this.isAnnotating = true; + if (e.target && ((e.target as any).className.includes('endOfContent') || (e.target as any).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 + 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(action(() => this._marqueeing = undefined), 100); // bcz: hack .. anchor menu is setup within MarqueeAnnotator so we need to at least create the marqueeAnnotator even though we aren't using it. - - this._styleRule = addStyleSheetRule(PDFViewer._annotationStyle, "htmlAnnotation", { "pointer-events": "none" }); - document.addEventListener("pointerup", this.onSelectEnd); - document.addEventListener("pointermove", this.onSelectMove); + setTimeout( + action(() => (this._marqueeing = undefined)), + 100 + ); // bcz: hack .. anchor menu is setup within MarqueeAnnotator so we need to at least create the marqueeAnnotator even though we aren't using it. + + this._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); - } + 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); + document.removeEventListener('pointermove', this.onSelectMove); + document.removeEventListener('pointerup', this.onSelectEnd); const sel = window.getSelection(); - if (sel?.type === "Range") { + if (sel?.type === 'Range') { this.createTextAnnotation(sel, sel.getRangeAt(0)); AnchorMenu.Instance.jumpTo(e.clientX, e.clientY); } - } + }; @action createTextAnnotation = (sel: Selection, selRange: Range) => { @@ -419,57 +409,44 @@ export class PDFViewer extends React.Component<IViewerProps> { const clientRects = selRange.getClientRects(); for (let i = 0; i < clientRects.length; i++) { const rect = clientRects.item(i); - if (rect && rect.width !== this._mainCont.current.clientWidth) { + if (rect && rect.width !== this._mainCont.current.clientWidth && rect.width) { const scaleX = this._mainCont.current.offsetWidth / boundingRect.width; - const annoBox = document.createElement("div"); - annoBox.className = "marqueeAnnotator-annotationBox"; + const pdfScale = NumCast(this.props.layoutDoc._viewScale, 1); + const annoBox = document.createElement('div'); + annoBox.className = 'marqueeAnnotator-annotationBox'; // transforms the positions from screen onto the pdf div - annoBox.style.top = ((rect.top - boundingRect.top) * scaleX / this._zoomed + this._mainCont.current.scrollTop).toString(); - annoBox.style.left = ((rect.left - boundingRect.left) * scaleX / this._zoomed).toString(); - annoBox.style.width = (rect.width * this._mainCont.current.offsetWidth / boundingRect.width / this._zoomed).toString(); - annoBox.style.height = (rect.height * this._mainCont.current.offsetHeight / boundingRect.height / this._zoomed).toString(); + annoBox.style.top = (((rect.top - boundingRect.top) * scaleX) / pdfScale + this._mainCont.current.scrollTop).toString(); + annoBox.style.left = (((rect.left - boundingRect.left) * scaleX) / pdfScale).toString(); + annoBox.style.width = ((rect.width * this._mainCont.current.offsetWidth) / boundingRect.width / pdfScale).toString(); + annoBox.style.height = ((rect.height * this._mainCont.current.offsetHeight) / boundingRect.height / pdfScale).toString(); this._annotationLayer.current && MarqueeAnnotator.previewNewAnnotation(this._savedAnnotations, this._annotationLayer.current, annoBox, this.getPageFromScroll(rect.top)); } } } - this._selectionText = selRange.cloneContents().textContent || ""; + this._selectionText = selRange.cloneContents().textContent || ''; // clear selection - if (sel.empty) { // Chrome + if (sel.empty) { + // Chrome sel.empty(); - } else if (sel.removeAllRanges) { // Firefox + } else if (sel.removeAllRanges) { + // Firefox sel.removeAllRanges(); } - } + }; scrollXf = () => { return this._mainCont.current ? this.props.ScreenToLocalTransform().translate(0, NumCast(this.props.layoutDoc._scrollTop)) : this.props.ScreenToLocalTransform(); - } + }; onClick = (e: React.MouseEvent) => { - if (this._setPreviewCursor && e.button === 0 && - Math.abs(e.clientX - this._downX) < Utils.DRAG_THRESHOLD && - Math.abs(e.clientY - this._downY) < Utils.DRAG_THRESHOLD) { + 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); } // e.stopPropagation(); // bcz: not sure why this was here. We need to allow the DocumentView to get clicks to process doubleClicks - } + }; - setPreviewCursor = (func?: (x: number, y: number, drag: boolean, hide: boolean) => void) => this._setPreviewCursor = func; - - getCoverImage = () => { - if (!this.props.Document[HeightSym]() || !Doc.NativeHeight(this.props.Document)) { - setTimeout((() => { - this.props.Document._height = this.props.Document[WidthSym]() * this._coverPath.height / this._coverPath.width; - Doc.SetNativeWidth(this.props.Document, (Doc.NativeWidth(this.props.Document) || 0) * this._coverPath.height / this._coverPath.width); - }).bind(this), 0); - } - const nativeWidth = Doc.NativeWidth(this.props.Document); - const nativeHeight = Doc.NativeHeight(this.props.Document); - const resolved = Utils.prepend(this._coverPath.path); - return <img key={resolved} src={resolved} onError={action(() => this._coverPath.path = "http://www.cs.brown.edu/~bcz/face.gif")} onLoad={action(() => this._showWaiting = false)} - style={{ position: "absolute", display: "inline-block", top: 0, left: 0, width: `${nativeWidth}px`, height: `${nativeHeight}px` }} />; - } + setPreviewCursor = (func?: (x: number, y: number, drag: boolean, hide: boolean) => void) => (this._setPreviewCursor = func); @action onZoomWheel = (e: React.WheelEvent) => { @@ -477,118 +454,145 @@ export class PDFViewer extends React.Component<IViewerProps> { e.stopPropagation(); if (e.ctrlKey) { const curScale = Number(this._pdfViewer.currentScaleValue); - this._pdfViewer.currentScaleValue = Math.max(1, Math.min(10, curScale - curScale * e.deltaY / 1000)); - this._zoomed = Number(this._pdfViewer.currentScaleValue); + this._pdfViewer.currentScaleValue = Math.max(1, Math.min(10, curScale - (curScale * e.deltaY) / 1000)); + this.props.layoutDoc._viewScale = Number(this._pdfViewer.currentScaleValue); } } - } + }; - pointerEvents = () => this.props.isContentActive() && this.props.pointerEvents !== "none" && !MarqueeOptionsMenu.Instance.isShown() ? "all" : SnappingManager.GetIsDragging() ? undefined : "none"; + pointerEvents = () => (this.props.isContentActive() && this.props.pointerEvents?.() !== 'none' && !MarqueeOptionsMenu.Instance.isShown() ? 'all' : SnappingManager.GetIsDragging() ? undefined : 'none'); @computed get annotationLayer() { - const pe = this.pointerEvents(); - return <div className="pdfViewerDash-annotationLayer" style={{ height: Doc.NativeHeight(this.props.Document), transform: `scale(${this._zoomed})` }} ref={this._annotationLayer}> - {this.inlineTextAnnotations.sort((a, b) => NumCast(a.y) - NumCast(b.y)).map(anno => - <Annotation {...this.props} fieldKey={this.props.fieldKey + "-annotations"} pointerEvents={pe} showInfo={this.showInfo} dataDoc={this.props.dataDoc} anno={anno} key={`${anno[Id]}-annotation`} />)} - </div>; + return ( + <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)) + .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`} /> + ))} + </div> + ); } @computed get overlayInfo() { - return !this._overlayAnnoInfo || this._overlayAnnoInfo.author === Doc.CurrentUserEmail ? (null) : + return !this._overlayAnnoInfo ? null : ( <div className="pdfViewerDash-overlayAnno" style={{ top: NumCast(this._overlayAnnoInfo.y), left: NumCast(this._overlayAnnoInfo.x) }}> <div className="pdfViewerDash-overlayAnno" style={{ right: -50, background: SharingManager.Instance.users.find(users => users.user.email === this._overlayAnnoInfo!.author)?.userColor }}> - {this._overlayAnnoInfo.author + " " + Field.toString(this._overlayAnnoInfo.creationDate as Field)} + {this._overlayAnnoInfo.author + ' ' + Field.toString(this._overlayAnnoInfo.creationDate as Field)} </div> - </div>; + </div> + ); } - showInfo = action((anno: Opt<Doc>) => this._overlayAnnoInfo = anno); - overlayTransform = () => this.scrollXf().scale(1 / this._zoomed); - panelWidth = () => this.props.PanelWidth() / (this.props.scaling?.() || 1); // (this.Document.scrollHeight || Doc.NativeHeight(this.Document) || 0); - panelHeight = () => this.props.PanelHeight() / (this.props.scaling?.() || 1); // () => this._pageSizes.length && this._pageSizes[0] ? this._pageSizes[0].width : Doc.NativeWidth(this.Document); - basicFilter = () => [...this.props.docFilters(), Utils.PropUnsetFilter("textInlineAnnotations")]; + showInfo = action((anno: Opt<Doc>) => (this._overlayAnnoInfo = anno)); + 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')]; transparentFilter = () => [...this.props.docFilters(), Utils.IsTransparentFilter()]; opaqueFilter = () => [...this.props.docFilters(), Utils.IsOpaqueFilter()]; - childStyleProvider = (doc: (Doc | undefined), props: Opt<DocumentViewProps>, property: string): any => { + childStyleProvider = (doc: Doc | undefined, props: Opt<DocumentViewProps>, property: string): any => { if (doc instanceof Doc && property === StyleProp.PointerEvents) { - if (doc.textInlineAnnotations) return "none"; - return "all"; + if (doc.textInlineAnnotations) return 'none'; + return 'all'; } 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} + /> + ); + @computed get overlayTransparentAnnotations() { + return this.renderAnnotations(this.transparentFilter, false); + } + @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>; } @computed get overlayLayer() { - const renderAnnotations = (docFilters?: () => string[]) => - <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} - ContentScaling={this.contentZoom} - bringToFront={emptyFunction} - docFilters={docFilters || this.basicFilter} - styleProvider={this.childStyleProvider} - dontRenderDocuments={docFilters ? false : true} - CollectionView={undefined} - ScreenToLocalTransform={this.overlayTransform} - renderDepth={this.props.renderDepth + 1} />; - return <div> - <div className={`pdfViewerDash-overlay${CurrentUserUtils.SelectedTool !== InkTool.None || SnappingManager.GetIsDragging() ? "-inking" : ""}`} - style={{ - pointerEvents: SnappingManager.GetIsDragging() ? "all" : undefined, - mixBlendMode: "multiply", - transform: `scale(${this._zoomed})` - }}> - {renderAnnotations(this.transparentFilter)} - </div> - <div className={`pdfViewerDash-overlay${CurrentUserUtils.SelectedTool !== InkTool.None || SnappingManager.GetIsDragging() ? "-inking" : ""}`} - style={{ - pointerEvents: SnappingManager.GetIsDragging() ? "all" : undefined, - mixBlendMode: this.allAnnotations.some(anno => anno.mixBlendMode) ? "hard-light" : undefined, - transform: `scale(${this._zoomed})` - }}> - {renderAnnotations(this.opaqueFilter)} - {SnappingManager.GetIsDragging() ? (null) : renderAnnotations()} + 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> </div> - </div>; + ); } @computed get pdfViewerDiv() { - return <div className={"pdfViewerDash-text" + (this.props.pointerEvents !== "none" && this._textSelecting && this.props.isContentActive() ? "-selected" : "")} ref={this._viewer} />; - } - @computed get contentScaling() { return this.props.ContentScaling?.() || 1; } - @computed get standinViews() { - return <> - {this._showCover ? this.getCoverImage() : (null)} - {this._showWaiting ? <img className="pdfViewerDash-waiting" key="waiting" src={"/assets/loading.gif"} /> : (null)} - </>; + return <div className={'pdfViewerDash-text' + (this.props.pointerEvents?.() !== 'none' && this._textSelecting && this.props.isContentActive() ? '-selected' : '')} ref={this._viewer} />; } - contentZoom = () => this._zoomed; + savedAnnotations = () => this._savedAnnotations; render() { TraceMobx(); - return <div className="pdfViewer-content"> - <div className={`pdfViewerDash${this.props.isContentActive() && this.props.pointerEvents !== "none" ? "-interactive" : ""}`} ref={this._mainCont} - onScroll={this.onScroll} onWheel={this.onZoomWheel} onPointerDown={this.onPointerDown} onClick={this.onClick} - style={{ - overflowX: this._zoomed !== 1 ? "scroll" : undefined, - height: !this.props.Document._fitWidth && (window.screen.width > 600) ? Doc.NativeHeight(this.props.Document) : `${100 / this.contentScaling}%`, - transform: `scale(${this.contentScaling})` - }} > - {this.pdfViewerDiv} - {this.annotationLayer} - {this.overlayLayer} - {this.overlayInfo} - {this.standinViews} - {!this._marqueeing || !this._mainCont.current || !this._annotationLayer.current ? (null) : - <MarqueeAnnotator rootDoc={this.props.rootDoc} scrollTop={0} down={this._marqueeing} - anchorMenuClick={this.props.anchorMenuClick} - addDocument={(doc: Doc | Doc[]) => this.props.addDocument!(doc)} - finishMarquee={this.finishMarquee} - docView={this.props.docViewPath().lastElement()} - getPageFromScroll={this.getPageFromScroll} - savedAnnotations={this._savedAnnotations} - annotationLayer={this._annotationLayer.current} mainCont={this._mainCont.current} />} + return ( + <div className="pdfViewer-content"> + <div + className={`pdfViewerDash${this.props.isContentActive() && this.props.pointerEvents?.() !== 'none' ? '-interactive' : ''}`} + ref={this._mainCont} + onScroll={this.onScroll} + onWheel={this.onZoomWheel} + onPointerDown={this.onPointerDown} + onClick={this.onClick} + style={{ + overflowX: NumCast(this.props.layoutDoc._viewScale, 1) !== 1 ? 'scroll' : undefined, + height: !this.props.Document._fitWidth && window.screen.width > 600 ? Doc.NativeHeight(this.props.Document) : `100%`, + }}> + {this.pdfViewerDiv} + {this.annotationLayer} + {this.overlayLayer} + {this.overlayInfo} + {this._showWaiting ? <img className="pdfViewerDash-waiting" src={'/assets/loading.gif'} /> : null} + {!this._marqueeing || !this._mainCont.current || !this._annotationLayer.current ? null : ( + <MarqueeAnnotator + rootDoc={this.props.rootDoc} + getPageFromScroll={this.getPageFromScroll} + anchorMenuClick={this.props.anchorMenuClick} + scrollTop={0} + down={this._marqueeing} + addDocument={(doc: Doc | Doc[]) => this.props.addDocument!(doc)} + docView={this.props.docViewPath().lastElement()} + finishMarquee={this.finishMarquee} + savedAnnotations={this.savedAnnotations} + annotationLayer={this._annotationLayer.current} + mainCont={this._mainCont.current} + anchorMenuCrop={this._textSelecting ? undefined : this.crop} + /> + )} + </div> </div> - </div>; + ); } -}
\ No newline at end of file +} |
