import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { action, computed, IReactionDisposer, observable, 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, HeightSym, Opt, WidthSym } from "../../../fields/Doc"; import { Id } from '../../../fields/FieldSymbols'; import { Cast, ImageCast, NumCast, StrCast } from '../../../fields/Types'; import { ImageField, PdfField } from "../../../fields/URLField"; import { TraceMobx } from '../../../fields/util'; import { emptyFunction, returnOne, setupMoveUpEvents, Utils } from '../../../Utils'; import { Docs, DocUtils } from '../../documents/Documents'; import { DocumentType } from '../../documents/DocumentTypes'; import { KeyCodes } from '../../util/KeyCodes'; import { undoBatch, UndoManager } from '../../util/UndoManager'; import { ContextMenu } from '../ContextMenu'; import { ContextMenuProps } from '../ContextMenuItem'; import { ViewBoxAnnotatableComponent, ViewBoxAnnotatableProps } from "../DocComponent"; import { Colors } from '../global/globalEnums'; import { CreateImage } from "../nodes/WebBoxRenderer"; import { AnchorMenu } from '../pdf/AnchorMenu'; import { PDFViewer } from "../pdf/PDFViewer"; import { SidebarAnnos } from '../SidebarAnnos'; import { FieldView, FieldViewProps } from './FieldView'; import { ImageBox } from './ImageBox'; import "./PDFBox.scss"; import { VideoBox } from './VideoBox'; import React = require("react"); import { CollectionFreeFormView } from '../collections/collectionFreeForm'; @observer export class PDFBox extends ViewBoxAnnotatableComponent() { public static LayoutString(fieldKey: string) { return FieldView.LayoutString(PDFBox, fieldKey); } public static openSidebarWidth = 250; public static sidebarResizerWidth = 5; private _searchString: string = ""; private _initialScrollTarget: Opt; private _pdfViewer: PDFViewer | undefined; private _searchRef = React.createRef(); private _selectReactionDisposer: IReactionDisposer | undefined; private _sidebarRef = React.createRef(); @observable private _searching: boolean = false; @observable private _pdf: Opt; @observable private _pageControls = false; @computed get pdfUrl() { return Cast(this.dataDoc[this.props.fieldKey], PdfField); } @computed get pdfThumb() { return ImageCast(this.layoutDoc["thumb-frozen"], ImageCast(this.layoutDoc.thumb))?.url; } constructor(props: any) { super(props); const nw = Doc.NativeWidth(this.Document, this.dataDoc) || 927; const nh = Doc.NativeHeight(this.Document, this.dataDoc) || 1200; !this.Document._fitWidth && (this.Document._height = this.Document[WidthSym]() * (nh / nw)); if (this.pdfUrl) { if (PDFBox.pdfcache.get(this.pdfUrl.url.href)) runInAction(() => this._pdf = PDFBox.pdfcache.get(this.pdfUrl!.url.href)); else if (PDFBox.pdfpromise.get(this.pdfUrl.url.href)) PDFBox.pdfpromise.get(this.pdfUrl.url.href)?.then(action((pdf: any) => this._pdf = pdf)); } } replaceCanvases = (oldDiv: HTMLElement, newDiv: HTMLElement) => { if (oldDiv.childNodes) { for (let i = 0; i < oldDiv.childNodes.length; i++) { this.replaceCanvases(oldDiv.childNodes[i] as HTMLElement, newDiv.childNodes[i] as HTMLElement); } } if (oldDiv.className === "pdfBox-ui" || oldDiv.className === "pdfViewerDash-overlay-inking") { newDiv.style.display = "none"; } if (newDiv && newDiv.style) newDiv.style.overflow = "hidden"; if (oldDiv instanceof HTMLCanvasElement) { const canvas = oldDiv; const img = document.createElement('img'); // create a Image Element img.src = canvas.toDataURL(); //image sourcez img.style.width = canvas.style.width; img.style.height = canvas.style.height; const newCan = newDiv as HTMLCanvasElement; const parEle = newCan.parentElement as HTMLElement; parEle.removeChild(newCan); parEle.appendChild(img); } } crop = (region: Doc | undefined, addCrop?: boolean) => { if (!region) return; const cropping = Doc.MakeCopy(region, true); Doc.GetProto(region).lockedPosition = true; Doc.GetProto(region).title = "region:" + this.rootDoc.title; Doc.GetProto(region).isPushpin = true; this.addDocument(region); const docViewContent = this.props.docViewPath().lastElement().ContentDiv!; const newDiv = docViewContent.cloneNode(true) as HTMLDivElement; newDiv.style.width = (this.layoutDoc[WidthSym]()).toString(); newDiv.style.height = (this.layoutDoc[HeightSym]()).toString(); this.replaceCanvases(docViewContent, newDiv); const htmlString = this._pdfViewer?._mainCont.current && new XMLSerializer().serializeToString(newDiv); const anchx = NumCast(cropping.x); const anchy = NumCast(cropping.y); const anchw = cropping[WidthSym]() * (this.props.scaling?.() || 1); const anchh = cropping[HeightSym]() * (this.props.scaling?.() || 1); const viewScale = 1; cropping.title = "crop: " + this.rootDoc.title; cropping.x = NumCast(this.rootDoc.x) + NumCast(this.rootDoc._width); cropping.y = NumCast(this.rootDoc.y); cropping._width = anchw; cropping._height = anchh; cropping.isLinkButton = undefined; const croppingProto = Doc.GetProto(cropping); croppingProto.annotationOn = undefined; croppingProto.isPrototype = true; croppingProto.proto = Cast(this.rootDoc.proto, Doc, null)?.proto; // set proto of cropping's data doc to be IMAGE_PROTO croppingProto.type = DocumentType.IMG; croppingProto.layout = ImageBox.LayoutString("data"); croppingProto.data = new ImageField(Utils.CorsProxy("http://www.cs.brown.edu/~bcz/noImage.png")); croppingProto["data-nativeWidth"] = anchw; croppingProto["data-nativeHeight"] = anchh; if (addCrop) { DocUtils.MakeLink({ doc: region }, { doc: cropping }, "cropped image", ""); } this.props.bringToFront(cropping); CreateImage( "", document.styleSheets, htmlString, anchw, anchh, NumCast(region.y) * this.props.PanelWidth() / NumCast(this.rootDoc[this.fieldKey + "-nativeWidth"]), NumCast(region.x) * this.props.PanelWidth() / NumCast(this.rootDoc[this.fieldKey + "-nativeWidth"]), 4 ).then ((data_url: any) => { VideoBox.convertDataUri(data_url, region[Id]).then( returnedfilename => setTimeout(action(() => { croppingProto.data = new ImageField(returnedfilename); }), 500)); }) .catch(function (error: any) { console.error('oops, something went wrong!', error); }); return cropping; } updateIcon = () => { // currently we render pdf icons as text labels const docViewContent = this.props.docViewPath().lastElement().ContentDiv!; const filename = this.layoutDoc[Id] + "-icon" + (new Date()).getTime(); this._pdfViewer?._mainCont.current && CollectionFreeFormView.UpdateIcon( filename, docViewContent, this.layoutDoc[WidthSym](), this.layoutDoc[HeightSym](), this.props.PanelWidth(), this.props.PanelHeight(), NumCast(this.layoutDoc._scrollTop), NumCast(this.rootDoc[this.fieldKey + "-nativeHeight"], 1), true, this.layoutDoc[Id] + "-icon", (iconFile:string, nativeWidth:number, nativeHeight:number) => { setTimeout(() => { this.dataDoc.icon = new ImageField(iconFile); this.dataDoc["icon-nativeWidth"] = nativeWidth; this.dataDoc["icon-nativeHeight"] = nativeHeight; }, 500); }); } componentWillUnmount() { this._selectReactionDisposer?.(); } componentDidMount() { this.props.setContentView?.(this); this._selectReactionDisposer = reaction(() => this.props.isSelected(), () => { document.removeEventListener("keydown", this.onKeyDown); this.props.isSelected(true) && document.addEventListener("keydown", this.onKeyDown); }, { fireImmediately: true }); } scrollFocus = (doc: Doc, smooth: boolean) => { if (DocListCast(this.props.Document[this.fieldKey + "-sidebar"]).includes(doc) && !this.SidebarShown) { this.toggleSidebar(!smooth); } if (this._sidebarRef?.current?.makeDocUnfiltered(doc)) return 1; this._initialScrollTarget = doc; return this._pdfViewer?.scrollFocus(doc, smooth); } getAnchor = () => { const anchor = this._pdfViewer?._getAnchor(this._pdfViewer.savedAnnotations()) ?? Docs.Create.TextanchorDocument({ title: StrCast(this.rootDoc.title + "@" + NumCast(this.layoutDoc._scrollTop)?.toFixed(0)), y: NumCast(this.layoutDoc._scrollTop), unrendered: true }); this.addDocument(anchor); return anchor; } @action loaded = (nw: number, nh: number, np: number) => { this.dataDoc[this.props.fieldKey + "-numPages"] = np; Doc.SetNativeWidth(this.dataDoc, Math.max(Doc.NativeWidth(this.dataDoc), nw * 96 / 72)); Doc.SetNativeHeight(this.dataDoc, nh * 96 / 72); this.layoutDoc._height = this.layoutDoc[WidthSym]() / (Doc.NativeAspect(this.dataDoc) || 1); !this.Document._fitWidth && (this.Document._height = this.Document[WidthSym]() * (nh / nw)); } public search = action((searchString: string, bwd?: boolean, clear: boolean = false) => { if (!this._searching && !clear) { this._searching = true; setTimeout(() => { this._searchRef.current?.focus(); this._searchRef.current?.select(); this._searchRef.current?.setRangeText(searchString); }); } return this._pdfViewer?.search(searchString, bwd, clear) || false; }); public prevAnnotation = () => this._pdfViewer?.prevAnnotation(); public nextAnnotation = () => this._pdfViewer?.nextAnnotation(); public backPage = () => { this.Document._curPage = Math.max(1, (NumCast(this.Document._curPage) || 1) - 1); return true; } public forwardPage = () => { this.Document._curPage = Math.min(NumCast(this.dataDoc[this.props.fieldKey + "-numPages"]), (NumCast(this.Document._curPage) || 1) + 1); return true; } public gotoPage = (p: number) => this.Document._curPage = p; @undoBatch onKeyDown = action((e: KeyboardEvent) => { let processed = false; switch (e.key) { case "PageDown": processed = this.forwardPage(); break; case "PageUp": processed = this.backPage(); break; } if (processed) { e.stopImmediatePropagation(); e.preventDefault(); } }); setPdfViewer = (pdfViewer: PDFViewer) => { this._pdfViewer = pdfViewer; if (this._initialScrollTarget) { this.scrollFocus(this._initialScrollTarget, false); this._initialScrollTarget = undefined; } } searchStringChanged = (e: React.ChangeEvent) => this._searchString = e.currentTarget.value; // adding external documents; to sidebar key // if (doc.Geolocation) this.addDocument(doc, this.fieldkey+"-annotation") sidebarAddDocument = (doc: Doc | Doc[], sidebarKey?: string) => { if (!this.layoutDoc._showSidebar) this.toggleSidebar(); return this.addDocument(doc, sidebarKey); } sidebarBtnDown = (e: React.PointerEvent, onButton: boolean) => { // onButton determines whether the width of the pdf box changes, or just the ratio of the sidebar to the pdf const batch = UndoManager.StartBatch("sidebar"); setupMoveUpEvents(this, e, (e, down, delta) => { const localDelta = this.props.ScreenToLocalTransform().scale(this.props.scaling?.() || 1).transformDirection(delta[0], delta[1]); const nativeWidth = NumCast(this.layoutDoc[this.fieldKey + "-nativeWidth"]); const curNativeWidth = NumCast(this.layoutDoc.nativeWidth, nativeWidth); const ratio = (curNativeWidth + (onButton ? 1 : -1) * localDelta[0] / (this.props.scaling?.() || 1)) / nativeWidth; if (ratio >= 1) { this.layoutDoc.nativeWidth = nativeWidth * ratio; onButton && (this.layoutDoc._width = this.layoutDoc[WidthSym]() + localDelta[0]); this.layoutDoc._showSidebar = nativeWidth !== this.layoutDoc._nativeWidth; } return false; }, () => batch.end(), () => this.toggleSidebar()); } @observable _previewNativeWidth: Opt = undefined; @observable _previewWidth: Opt = undefined; toggleSidebar = action((preview: boolean = false) => { const nativeWidth = NumCast(this.layoutDoc[this.fieldKey + "-nativeWidth"]); const sideratio = ((!this.layoutDoc.nativeWidth || this.layoutDoc.nativeWidth === nativeWidth ? PDFBox.openSidebarWidth : 0) + nativeWidth) / nativeWidth; const pdfratio = ((!this.layoutDoc.nativeWidth || this.layoutDoc.nativeWidth === nativeWidth ? PDFBox.openSidebarWidth + PDFBox.sidebarResizerWidth : 0) + nativeWidth) / nativeWidth; const curNativeWidth = NumCast(this.layoutDoc.nativeWidth, nativeWidth); if (preview) { this._previewNativeWidth = nativeWidth * sideratio; this._previewWidth = this.layoutDoc[WidthSym]() * nativeWidth * sideratio / curNativeWidth; this._showSidebar = true; } else { this.layoutDoc.nativeWidth = nativeWidth * pdfratio; this.layoutDoc._width = this.layoutDoc[WidthSym]() * nativeWidth * pdfratio / curNativeWidth; this.layoutDoc._showSidebar = nativeWidth !== this.layoutDoc._nativeWidth; } }); settingsPanel() { const pageBtns = <> ; const searchTitle = `${!this._searching ? "Open" : "Close"} Search Bar`; const curPage = NumCast(this.Document._curPage) || 1; return !this.props.isContentActive() || this._pdfViewer?.isAnnotating ? (null) :
[KeyCodes.BACKSPACE, KeyCodes.DELETE].includes(e.keyCode) ? e.stopPropagation() : true} onPointerDown={e => e.stopPropagation()} style={{ display: this.props.isContentActive() ? "flex" : "none" }}>
e.stopPropagation()} style={{ left: `${this._searching ? 0 : 100}%` }}>
99 ? 4 : 3}ch`, pointerEvents: "all" }} onChange={e => this.Document._curPage = Number(e.currentTarget.value)} onKeyDown={e => e.stopPropagation()} onClick={action(() => this._pageControls = !this._pageControls)} /> {this._pageControls ? pageBtns : (null)}
this.sidebarBtnDown(e, true)} >
; } sidebarWidth = () => !this.SidebarShown ? 0 : PDFBox.sidebarResizerWidth + (this._previewWidth ? PDFBox.openSidebarWidth : (NumCast(this.layoutDoc.nativeWidth) - Doc.NativeWidth(this.dataDoc)) * this.props.PanelWidth() / NumCast(this.layoutDoc.nativeWidth)) specificContextMenu = (e: React.MouseEvent): void => { const funcs: ContextMenuProps[] = []; funcs.push({ description: "Copy path", event: () => this.pdfUrl && Utils.CopyText(Utils.prepend("") + this.pdfUrl.url.pathname), icon: "expand-arrows-alt" }); funcs.push({ description: "update icon", event: () => this.pdfUrl && this.updateIcon(), icon: "expand-arrows-alt" }); //funcs.push({ description: "Toggle Sidebar ", event: () => this.toggleSidebar(), icon: "expand-arrows-alt" }); ContextMenu.Instance.addItem({ description: "Options...", subitems: funcs, icon: "asterisk" }); } @computed get renderTitleBox() { const classname = "pdfBox" + (this.props.isContentActive() ? "-interactive" : ""); return
{this.props.Document.title}
; } anchorMenuClick = () => this._sidebarRef.current?.anchorMenuClick; @observable _showSidebar = false; @computed get SidebarShown() { return this._showSidebar || this.layoutDoc._showSidebar ? true : false; } contentScaling = () => 1; isPdfContentActive = () => this.isAnyChildContentActive() || this.props.isSelected(); @computed get renderPdfView() { TraceMobx(); const previewScale = this._previewNativeWidth ? 1 - this.sidebarWidth() / this._previewNativeWidth : 1; const scale = previewScale * (this.props.scaling?.() || 1); return
600) ? NumCast(this.Document._height) * this.props.PanelWidth() / NumCast(this.Document._width) : undefined }}>
this.sidebarBtnDown(e, false)} />
{this.settingsPanel()}
; } static pdfcache = new Map(); static pdfpromise = new Map>(); render() { TraceMobx(); if (this._pdf) { if (!this.props.thumbShown?.()) { return this.renderPdfView; } return null; } const href = this.pdfUrl?.url.href; if (href) { if (PDFBox.pdfcache.get(href)) setTimeout(action(() => this._pdf = PDFBox.pdfcache.get(href))); else { if (!PDFBox.pdfpromise.get(href)) PDFBox.pdfpromise.set(href, Pdfjs.getDocument(href).promise); PDFBox.pdfpromise.get(href)?.then(action((pdf: any) => PDFBox.pdfcache.set(href, this._pdf = pdf))); } } return this.renderTitleBox; } }