diff options
| author | andrewdkim <adkim414@gmail.com> | 2019-07-19 11:21:47 -0400 |
|---|---|---|
| committer | andrewdkim <adkim414@gmail.com> | 2019-07-19 11:21:47 -0400 |
| commit | e453e7010def252b6cc10ad49d64708767c5589b (patch) | |
| tree | 4353a4be5495acd61d813066d001e2b12da7d207 /src/client/views/pdf | |
| parent | 9cad9abcf164c7d81b8debf4aa2639d83edd227b (diff) | |
| parent | 8854d3277541a67aef4187b5d3592bea5a7fcfa2 (diff) | |
merge from master
Diffstat (limited to 'src/client/views/pdf')
| -rw-r--r-- | src/client/views/pdf/Annotation.tsx | 153 | ||||
| -rw-r--r-- | src/client/views/pdf/PDFMenu.scss | 11 | ||||
| -rw-r--r-- | src/client/views/pdf/PDFMenu.tsx | 109 | ||||
| -rw-r--r-- | src/client/views/pdf/PDFViewer.scss | 89 | ||||
| -rw-r--r-- | src/client/views/pdf/PDFViewer.tsx | 637 | ||||
| -rw-r--r-- | src/client/views/pdf/Page.tsx | 174 |
6 files changed, 825 insertions, 348 deletions
diff --git a/src/client/views/pdf/Annotation.tsx b/src/client/views/pdf/Annotation.tsx new file mode 100644 index 000000000..ed7081b1d --- /dev/null +++ b/src/client/views/pdf/Annotation.tsx @@ -0,0 +1,153 @@ +import React = require("react"); +import { Doc, DocListCast, WidthSym, HeightSym } from "../../../new_fields/Doc"; +import { AnnotationTypes, Viewer, scale } from "./PDFViewer"; +import { observer } from "mobx-react"; +import { observable, IReactionDisposer, reaction, action } from "mobx"; +import { BoolCast, NumCast, FieldValue, Cast, StrCast } from "../../../new_fields/Types"; +import { Id } from "../../../new_fields/FieldSymbols"; +import { List } from "../../../new_fields/List"; +import PDFMenu from "./PDFMenu"; +import { DocumentManager } from "../../util/DocumentManager"; +import { PresentationView } from "../presentationview/PresentationView"; + +interface IAnnotationProps { + anno: Doc; + index: number; + parent: Viewer; +} + +export default class Annotation extends React.Component<IAnnotationProps> { + render() { + let annotationDocs = DocListCast(this.props.anno.annotations); + let res = annotationDocs.map(a => { + let type = NumCast(a.type); + switch (type) { + // case AnnotationTypes.Pin: + // return <PinAnnotation parent={this} document={a} x={NumCast(a.x)} y={NumCast(a.y)} width={a[WidthSym]()} height={a[HeightSym]()} key={a[Id]} />; + case AnnotationTypes.Region: + return <RegionAnnotation parent={this.props.parent} document={a} index={this.props.index} x={NumCast(a.x)} y={NumCast(a.y)} width={a[WidthSym]()} height={a[HeightSym]()} key={a[Id]} />; + default: + return <div></div>; + } + }); + return res; + } +} + +interface IRegionAnnotationProps { + x: number; + y: number; + width: number; + height: number; + index: number; + parent: Viewer; + document: Doc; +} + +@observer +class RegionAnnotation extends React.Component<IRegionAnnotationProps> { + @observable private _backgroundColor: string = "red"; + + private _reactionDisposer?: IReactionDisposer; + private _scrollDisposer?: IReactionDisposer; + private _mainCont: React.RefObject<HTMLDivElement>; + + constructor(props: IRegionAnnotationProps) { + super(props); + + this._mainCont = React.createRef(); + } + + componentDidMount() { + this._reactionDisposer = reaction( + () => BoolCast(this.props.document.delete), + () => { + if (BoolCast(this.props.document.delete)) { + if (this._mainCont.current) { + this._mainCont.current.style.display = "none"; + } + } + }, + { fireImmediately: true } + ); + + this._scrollDisposer = reaction( + () => this.props.parent.Index, + () => { + if (this.props.parent.Index === this.props.index) { + this.props.parent.scrollTo(this.props.y * scale); + } + } + ); + } + + componentWillUnmount() { + this._reactionDisposer && this._reactionDisposer(); + this._scrollDisposer && this._scrollDisposer(); + } + + deleteAnnotation = () => { + let annotation = DocListCast(this.props.parent.props.parent.fieldExtensionDoc.annotations); + let group = FieldValue(Cast(this.props.document.group, Doc)); + if (group && annotation.indexOf(group) !== -1) { + let newAnnotations = annotation.filter(a => a !== FieldValue(Cast(this.props.document.group, Doc))); + this.props.parent.props.parent.fieldExtensionDoc.annotations = new List<Doc>(newAnnotations); + } + + if (group) { + let groupAnnotations = DocListCast(group.annotations); + groupAnnotations.forEach(anno => anno.delete = true); + } + + PDFMenu.Instance.fadeOut(true); + } + + pinToPres = () => { + let group = FieldValue(Cast(this.props.document.group, Doc)); + if (group) { + PresentationView.Instance.PinDoc(group); + } + } + + @action + onPointerDown = (e: React.PointerEvent) => { + if (e.button === 0) { + let targetDoc = Cast(this.props.document.target, Doc, null); + if (targetDoc) { + DocumentManager.Instance.jumpToDocument(targetDoc, false); + } + } + if (e.button === 2) { + PDFMenu.Instance.Status = "annotation"; + PDFMenu.Instance.Delete = this.deleteAnnotation.bind(this); + PDFMenu.Instance.Pinned = false; + PDFMenu.Instance.AddTag = this.addTag.bind(this); + PDFMenu.Instance.PinToPres = this.pinToPres; + PDFMenu.Instance.jumpTo(e.clientX, e.clientY, true); + } + } + + addTag = (key: string, value: string): boolean => { + let group = FieldValue(Cast(this.props.document.group, Doc)); + if (group) { + let valNum = parseInt(value); + group[key] = isNaN(valNum) ? value : valNum; + return true; + } + return false; + } + + render() { + return ( + <div className="pdfViewer-annotationBox" onPointerDown={this.onPointerDown} ref={this._mainCont} + style={{ + top: this.props.y * scale, + left: this.props.x * scale, + width: this.props.width * scale, + height: this.props.height * scale, + pointerEvents: "all", + backgroundColor: this.props.parent.Index === this.props.index ? "green" : StrCast(this.props.document.color) + }}></div> + ); + } +}
\ No newline at end of file diff --git a/src/client/views/pdf/PDFMenu.scss b/src/client/views/pdf/PDFMenu.scss index 22868082a..b06d19c53 100644 --- a/src/client/views/pdf/PDFMenu.scss +++ b/src/client/views/pdf/PDFMenu.scss @@ -21,5 +21,16 @@ .pdfMenu-dragger { height: 100%; transition: width .2s; + background-image: url("https://logodix.com/logo/1020374.png"); + background-size: 90% 100%; + background-repeat: no-repeat; + background-position: left center; + } + + .pdfMenu-addTag { + display: grid; + width: 200px; + padding: 5px; + grid-template-columns: 90px 20px 90px; } }
\ No newline at end of file diff --git a/src/client/views/pdf/PDFMenu.tsx b/src/client/views/pdf/PDFMenu.tsx index 39b15fb11..27c2a8f1a 100644 --- a/src/client/views/pdf/PDFMenu.tsx +++ b/src/client/views/pdf/PDFMenu.tsx @@ -1,10 +1,11 @@ import React = require("react"); import "./PDFMenu.scss"; -import { observable, action } from "mobx"; +import { observable, action, runInAction } from "mobx"; import { observer } from "mobx-react"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { emptyFunction } from "../../../Utils"; +import { emptyFunction, returnFalse } from "../../../Utils"; import { Doc } from "../../../new_fields/Doc"; +import { handleBackspace } from "../nodes/PDFBox"; @observer export default class PDFMenu extends React.Component { @@ -16,26 +17,34 @@ export default class PDFMenu extends React.Component { @observable private _transition: string = "opacity 0.5s"; @observable private _transitionDelay: string = ""; - @observable public Pinned: boolean = false; - StartDrag: (e: PointerEvent) => void = emptyFunction; + StartDrag: (e: PointerEvent, ele: HTMLElement) => void = emptyFunction; Highlight: (d: Doc | undefined, color: string | undefined) => void = emptyFunction; Delete: () => void = emptyFunction; + Snippet: (marquee: { left: number, top: number, width: number, height: number }) => void = emptyFunction; + AddTag: (key: string, value: string) => boolean = returnFalse; + PinToPres: () => void = emptyFunction; @observable public Highlighting: boolean = false; - @observable public Status: "pdf" | "annotation" | "" = ""; + @observable public Status: "pdf" | "annotation" | "snippet" | "" = ""; + @observable public Pinned: boolean = false; + + public Marquee: { left: number; top: number; width: number; height: number; } | undefined; private _offsetY: number = 0; private _offsetX: number = 0; - private _mainCont: React.RefObject<HTMLDivElement>; + private _mainCont: React.RefObject<HTMLDivElement> = React.createRef(); + private _commentCont = React.createRef<HTMLButtonElement>(); + private _snippetButton: React.RefObject<HTMLButtonElement> = React.createRef(); private _dragging: boolean = false; + @observable private _keyValue: string = ""; + @observable private _valueValue: string = ""; + @observable private _added: boolean = false; constructor(props: Readonly<{}>) { super(props); PDFMenu.Instance = this; - - this._mainCont = React.createRef(); } pointerDown = (e: React.PointerEvent) => { @@ -56,7 +65,7 @@ export default class PDFMenu extends React.Component { return; } - this.StartDrag(e); + this.StartDrag(e, this._commentCont.current!); this._dragging = true; } @@ -171,34 +180,88 @@ export default class PDFMenu extends React.Component { e.preventDefault(); } + snippetStart = (e: React.PointerEvent) => { + document.removeEventListener("pointermove", this.snippetDrag); + document.addEventListener("pointermove", this.snippetDrag); + document.removeEventListener("pointerup", this.snippetEnd); + document.addEventListener("pointerup", this.snippetEnd); + + e.stopPropagation(); + e.preventDefault(); + } + + snippetDrag = (e: PointerEvent) => { + e.stopPropagation(); + e.preventDefault(); + if (this._dragging) { + return; + } + this._dragging = true; + + if (this.Marquee) { + this.Snippet(this.Marquee); + } + } + + snippetEnd = (e: PointerEvent) => { + this._dragging = false; + document.removeEventListener("pointermove", this.snippetDrag); + document.removeEventListener("pointerup", this.snippetEnd); + e.stopPropagation(); + e.preventDefault(); + } + + @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( + () => { + runInAction(() => { + this._added = false; + }); + }, 1000 + ); + } + } + render() { - let buttons = this.Status === "pdf" ? [ - <button className="pdfMenu-button" title="Click to Highlight" onClick={this.highlightClicked} + let buttons = this.Status === "pdf" || this.Status === "snippet" ? [ + <button key="1" className="pdfMenu-button" title="Click to Highlight" onClick={this.highlightClicked} style={this.Highlighting ? { backgroundColor: "#121212" } : {}}> <FontAwesomeIcon icon="highlighter" size="lg" style={{ transition: "transform 0.1s", transform: this.Highlighting ? "" : "rotate(-45deg)" }} /> </button>, - <button className="pdfMenu-button" title="Drag to Annotate" onPointerDown={this.pointerDown}><FontAwesomeIcon icon="comment-alt" size="lg" /></button>, - <button className="pdfMenu-button" title="Pin Menu" onClick={this.togglePin} + <button className="pdfMenu-button" title="Drag to Annotate" ref={this._commentCont} onPointerDown={this.pointerDown}><FontAwesomeIcon icon="comment-alt" size="lg" key="2" /></button>, + this.Status === "snippet" ? <button className="pdfMenu-button" title="Drag to Snippetize Selection" onPointerDown={this.snippetStart} ref={this._snippetButton}><FontAwesomeIcon icon="cut" size="lg" /></button> : undefined, + <button key="3" className="pdfMenu-button" title="Pin Menu" onClick={this.togglePin} style={this.Pinned ? { backgroundColor: "#121212" } : {}}> <FontAwesomeIcon icon="thumbtack" size="lg" style={{ transition: "transform 0.1s", transform: this.Pinned ? "rotate(45deg)" : "" }} /> </button> ] : [ - <button className="pdfMenu-button" title="Delete Anchor" onPointerDown={this.deleteClicked}><FontAwesomeIcon icon="trash-alt" size="lg" /></button> + <button key="4" className="pdfMenu-button" title="Delete Anchor" onPointerDown={this.deleteClicked}><FontAwesomeIcon icon="trash-alt" size="lg" key="1" /></button>, + <button key="5" className="pdfMenu-button" title="Pin to Presentation" onPointerDown={this.PinToPres}><FontAwesomeIcon icon="map-pin" size="lg" key="2" /></button>, + <div className="pdfMenu-addTag" key="3"> + <input onKeyDown={handleBackspace} onChange={this.keyChanged} placeholder="Key" style={{ gridColumn: 1 }} /> + <input onKeyDown={handleBackspace} onChange={this.valueChanged} placeholder="Value" style={{ gridColumn: 3 }} /> + </div>, + <button key="6" className="pdfMenu-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" key="4" /></button>, ]; return ( <div className="pdfMenu-cont" onPointerLeave={this.pointerLeave} onPointerEnter={this.pointerEntered} ref={this._mainCont} onContextMenu={this.handleContextMenu} style={{ left: this._left, top: this._top, opacity: this._opacity, transition: this._transition, transitionDelay: this._transitionDelay }}> {buttons} - {/* <button className="pdfMenu-button" title="Highlight" onClick={this.highlightClicked} - style={this.Highlighting ? { backgroundColor: "#121212" } : {}}> - <FontAwesomeIcon icon="highlighter" size="lg" style={{ transition: "transform 0.1s", transform: this.Highlighting ? "" : "rotate(-45deg)" }} /> - </button> - <button className="pdfMenu-button" title="Annotate" onPointerDown={this.pointerDown}><FontAwesomeIcon icon="comment-alt" size="lg" /></button> - <button className="pdfMenu-button" title="Pin Menu" onClick={this.togglePin} - style={this._pinned ? { backgroundColor: "#121212" } : {}}> - <FontAwesomeIcon icon="thumbtack" size="lg" style={{ transition: "transform 0.1s", transform: this._pinned ? "rotate(45deg)" : "" }} /> - </button> */} <div className="pdfMenu-dragger" onPointerDown={this.dragStart} style={{ width: this.Pinned ? "20px" : "0px" }} /> </div > ); diff --git a/src/client/views/pdf/PDFViewer.scss b/src/client/views/pdf/PDFViewer.scss index 53c33ce0b..0fde764d0 100644 --- a/src/client/views/pdf/PDFViewer.scss +++ b/src/client/views/pdf/PDFViewer.scss @@ -1,9 +1,9 @@ + .textLayer { div { user-select: text; } } - .viewer-button-cont { position: absolute; display: flex; @@ -23,6 +23,93 @@ .textLayer { user-select: auto; } +.viewer { + // position: absolute; + // top: 0; +} + +.pdfViewer-text { + + .page { + .canvasWrapper { + display: none; + } + + .textLayer { + position: relative; + user-select: none; + } + } +} +.pdfViewer-viewerCont { + width:100%; +} + +.page-cont { + .textLayer { + user-select: auto; + + div { + user-select: text; + } + } +} + +.pdfViewer-overlayCont { + position: absolute; + width: 100%; + height: 100px; + background: #121721; + bottom: 0; + display: flex; + justify-content: center; + align-items: center; + padding: 20px; + overflow: hidden; + transition: left .5s; +} + +.pdfViewer-overlaySearchBar { + width: 20%; + height: 100%; + font-size: 30px; + padding: 5px; +} + +.pdfViewer-overlayButton { + border-bottom-left-radius: 50%; + display: flex; + justify-content: space-evenly; + align-items: center; + height: 70px; + background: none; + padding: 0; + position: absolute; + + .pdfViewer-overlayButton-arrow { + width: 0; + height: 0; + border-top: 25px solid transparent; + border-bottom: 25px solid transparent; + border-right: 25px solid #121721; + transition: all 0.5s; + } + + .pdfViewer-overlayButton-iconCont { + background: #121721; + height: 50px; + width: 70px; + display: flex; + justify-content: center; + align-items: center; + margin-left: -2px; + border-radius: 3px; + } +} + +.pdfViewer-overlayButton:hover { + background: none; +} .pdfViewer-annotationBox { position: absolute; diff --git a/src/client/views/pdf/PDFViewer.tsx b/src/client/views/pdf/PDFViewer.tsx index 7000352e7..b7ded7e06 100644 --- a/src/client/views/pdf/PDFViewer.tsx +++ b/src/client/views/pdf/PDFViewer.tsx @@ -4,22 +4,23 @@ import * as Pdfjs from "pdfjs-dist"; import "pdfjs-dist/web/pdf_viewer.css"; import * as rp from "request-promise"; import { Dictionary } from "typescript-collections"; -import { Doc, DocListCast, HeightSym, Opt, WidthSym } from "../../../new_fields/Doc"; +import { Doc, DocListCast, Opt } from "../../../new_fields/Doc"; import { Id } from "../../../new_fields/FieldSymbols"; import { List } from "../../../new_fields/List"; -import { BoolCast, Cast, NumCast, StrCast, FieldValue } from "../../../new_fields/Types"; -import { emptyFunction } from "../../../Utils"; -import { DocServer } from "../../DocServer"; +import { Cast, NumCast, StrCast } from "../../../new_fields/Types"; +import { emptyFunction, Utils } from "../../../Utils"; import { Docs, DocUtils } from "../../documents/Documents"; -import { DocumentManager } from "../../util/DocumentManager"; import { DragManager } from "../../util/DragManager"; -import { DocumentView } from "../nodes/DocumentView"; import { PDFBox } from "../nodes/PDFBox"; import Page from "./Page"; import "./PDFViewer.scss"; import React = require("react"); -import PDFMenu from "./PDFMenu"; -import { UndoManager } from "../../util/UndoManager"; +import { CompileScript, CompileResult } from "../../util/Scripting"; +import { ScriptField } from "../../../new_fields/ScriptField"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import Annotation from "./Annotation"; +import { KeyCodes } from "../../northstar/utils/KeyCodes"; +const PDFJSViewer = require("pdfjs-dist/web/pdf_viewer"); export const scale = 2; interface IPDFViewerProps { @@ -44,7 +45,7 @@ export class PDFViewer extends React.Component<IPDFViewerProps> { render() { return ( - <div ref={this._mainDiv}> + <div className="pdfViewer-viewerCont" ref={this._mainDiv}> {!this._pdf ? (null) : <Viewer pdf={this._pdf} loaded={this.props.loaded} scrollY={this.props.scrollY} parent={this.props.parent} mainCont={this._mainDiv} url={this.props.url} />} </div> @@ -61,13 +62,11 @@ interface IViewerProps { url: string; } -const PinRadius = 25; - /** * Handles rendering and virtualization of the pdf */ @observer -class Viewer extends React.Component<IViewerProps> { +export class Viewer extends React.Component<IViewerProps> { // _visibleElements is the array of JSX elements that gets rendered @observable.shallow private _visibleElements: JSX.Element[] = []; // _isPage is an array that tells us whether or not an index is rendered as a page or as a placeholder @@ -75,12 +74,32 @@ class Viewer extends React.Component<IViewerProps> { @observable private _pageSizes: { width: number, height: number }[] = []; @observable private _annotations: Doc[] = []; @observable private _savedAnnotations: Dictionary<number, HTMLDivElement[]> = new Dictionary<number, HTMLDivElement[]>(); + @observable private _script: CompileResult | undefined; + @observable private _searching: boolean = false; + + @observable public Index: number = -1; private _pageBuffer: number = 1; private _annotationLayer: React.RefObject<HTMLDivElement> = React.createRef(); private _reactionDisposer?: IReactionDisposer; private _annotationReactionDisposer?: IReactionDisposer; private _dropDisposer?: DragManager.DragDropDisposer; + private _filterReactionDisposer?: IReactionDisposer; + private _viewer: React.RefObject<HTMLDivElement>; + private _mainCont: React.RefObject<HTMLDivElement>; + private _pdfViewer: any; + // private _textContent: Pdfjs.TextContent[] = []; + private _pdfFindController: any; + private _searchString: string = ""; + + constructor(props: IViewerProps) { + super(props); + + let scriptfield = Cast(this.props.parent.Document.filterScript, ScriptField); + this._script = scriptfield ? scriptfield.script : CompileScript("return true"); + this._viewer = React.createRef(); + this._mainCont = React.createRef(); + } componentDidUpdate = (prevProps: IViewerProps) => { if (this.scrollY !== prevProps.scrollY) { @@ -99,15 +118,57 @@ class Viewer extends React.Component<IViewerProps> { }, { fireImmediately: true }); this._annotationReactionDisposer = reaction( - () => this.props.parent.Document && DocListCast(this.props.parent.Document.annotations), - (annotations: Doc[]) => - annotations && annotations.length && this.renderAnnotations(annotations, true), + () => { + return this.props.parent && this.props.parent.fieldExtensionDoc && DocListCast(this.props.parent.fieldExtensionDoc.annotations); + }, + (annotations: Doc[]) => { + annotations && annotations.length && this.renderAnnotations(annotations, true); + }, { fireImmediately: true }); + + + if (this.props.parent.props.ContainingCollectionView) { + this._filterReactionDisposer = reaction( + () => this.props.parent.Document.filterScript, + () => { + runInAction(() => { + let scriptfield = Cast(this.props.parent.Document.filterScript, ScriptField); + this._script = scriptfield ? scriptfield.script : CompileScript("return true"); + if (this.props.parent.props.ContainingCollectionView) { + let fieldDoc = Doc.resolvedFieldDataDoc(this.props.parent.props.ContainingCollectionView.props.DataDoc ? + this.props.parent.props.ContainingCollectionView.props.DataDoc : this.props.parent.props.ContainingCollectionView.props.Document, this.props.parent.props.ContainingCollectionView.props.fieldKey, "true"); + let ccvAnnos = DocListCast(fieldDoc.annotations); + ccvAnnos.forEach(d => { + if (this._script && this._script.compiled) { + let run = this._script.run(d); + if (run.success) { + d.opacity = run.result ? 1 : 0; + } + } + }); + } + this.Index = -1; + }); + } + ); + } + + if (this._mainCont.current) { + this._dropDisposer = this._mainCont.current && DragManager.MakeDropTarget(this._mainCont.current, { handlers: { drop: this.drop.bind(this) } }); + } } componentWillUnmount = () => { this._reactionDisposer && this._reactionDisposer(); this._annotationReactionDisposer && this._annotationReactionDisposer(); + this._filterReactionDisposer && this._filterReactionDisposer(); + this._dropDisposer && this._dropDisposer(); + } + + scrollTo(y: number) { + if (this.props.mainCont.current) { + this.props.parent.scrollTo(y - this.props.mainCont.current.clientHeight); + } } @action @@ -115,35 +176,58 @@ class Viewer extends React.Component<IViewerProps> { if (this._pageSizes.length === 0) { let pageSizes = Array<{ width: number, height: number }>(this.props.pdf.numPages); this._isPage = Array<string>(this.props.pdf.numPages); + // this._textContent = Array<Pdfjs.TextContent>(this.props.pdf.numPages); + const proms: Pdfjs.PDFPromise<any>[] = []; for (let i = 0; i < this.props.pdf.numPages; i++) { - await this.props.pdf.getPage(i + 1).then(page => runInAction(() => { - // pageSizes[i] = { width: page.view[2] * scale, height: page.view[3] * scale }; - let x = page.getViewport(scale); - pageSizes[i] = { width: x.width, height: x.height }; - })); + proms.push(this.props.pdf.getPage(i + 1).then(page => runInAction(() => { + pageSizes[i] = { + width: (page.view[page.rotate === 0 || page.rotate === 180 ? 2 : 3] - page.view[page.rotate === 0 || page.rotate === 180 ? 0 : 1]) * scale, + height: (page.view[page.rotate === 0 || page.rotate === 180 ? 3 : 2] - page.view[page.rotate === 0 || page.rotate === 180 ? 1 : 0]) * scale + }; + // let x = page.getViewport(scale); + // page.getTextContent().then((text: Pdfjs.TextContent) => { + // // let tc = new Pdfjs.TextContentItem() + // // let tc = {str: } + // this._textContent[i] = text; + // // text.items.forEach(t => { + // // tcStr += t.str; + // // }) + // }); + // pageSizes[i] = { width: x.width, height: x.height }; + }))); } + await Promise.all(proms); runInAction(() => Array.from(Array((this._pageSizes = pageSizes).length).keys()).map(this.getPlaceholderPage)); this.props.loaded(Math.max(...pageSizes.map(i => i.width)), pageSizes[0].height, this.props.pdf.numPages); // this.props.loaded(Math.max(...pageSizes.map(i => i.width)), pageSizes[0].height, this.props.pdf.numPages); - } - } - private mainCont = (div: HTMLDivElement | null) => { - this._dropDisposer && this._dropDisposer(); - if (div) { - this._dropDisposer = div && DragManager.MakeDropTarget(div, { handlers: { drop: this.drop.bind(this) } }); + let startY = NumCast(this.props.parent.Document.startY); + let ccv = this.props.parent.props.ContainingCollectionView; + if (ccv) { + ccv.props.Document.panY = startY; + } + this.props.parent.Document.scrollY = 0; + this.props.parent.Document.scrollY = startY + 1; } } + @action makeAnnotationDocument = (sourceDoc: Doc | undefined, s: number, color: string): Doc => { let annoDocs: Doc[] = []; - let mainAnnoDoc = new Doc(); + let mainAnnoDoc = Docs.Create.InstanceFromProto(new Doc(), "", {}); + + mainAnnoDoc.title = "Annotation on " + StrCast(this.props.parent.Document.title); + mainAnnoDoc.pdfDoc = this.props.parent.props.Document; + let minY = Number.MAX_VALUE; this._savedAnnotations.forEach((key: number, value: HTMLDivElement[]) => { for (let anno of value) { let annoDoc = new Doc(); if (anno.style.left) annoDoc.x = parseInt(anno.style.left) / scale; - if (anno.style.top) annoDoc.y = parseInt(anno.style.top) / scale; + if (anno.style.top) { + annoDoc.y = parseInt(anno.style.top) / scale; + minY = Math.min(parseInt(anno.style.top), minY); + } if (anno.style.height) annoDoc.height = parseInt(anno.style.height) / scale; if (anno.style.width) annoDoc.width = parseInt(anno.style.width) / scale; annoDoc.page = key; @@ -156,11 +240,13 @@ class Viewer extends React.Component<IViewerProps> { } }); + mainAnnoDoc.y = Math.max(minY, 0); mainAnnoDoc.annotations = new List<Doc>(annoDocs); if (sourceDoc) { DocUtils.MakeLink(sourceDoc, mainAnnoDoc, undefined, `Annotation from ${StrCast(this.props.parent.Document.title)}`, "", StrCast(this.props.parent.Document.title)); } this._savedAnnotations.clear(); + this.Index = -1; return mainAnnoDoc; } @@ -168,13 +254,14 @@ class Viewer extends React.Component<IViewerProps> { if (de.data instanceof DragManager.LinkDragData) { let sourceDoc = de.data.linkSourceDocument; let destDoc = this.makeAnnotationDocument(sourceDoc, 1, "red"); - let targetAnnotations = DocListCast(this.props.parent.Document.annotations); + de.data.droppedDocuments.push(destDoc); + let targetAnnotations = DocListCast(this.props.parent.fieldExtensionDoc.annotations); if (targetAnnotations) { targetAnnotations.push(destDoc); - this.props.parent.Document.annotations = new List<Doc>(targetAnnotations); + this.props.parent.fieldExtensionDoc.annotations = new List<Doc>(targetAnnotations); } else { - this.props.parent.Document.annotations = new List<Doc>([destDoc]); + this.props.parent.fieldExtensionDoc.annotations = new List<Doc>([destDoc]); } e.stopPropagation(); } @@ -187,6 +274,7 @@ class Viewer extends React.Component<IViewerProps> { pageLoaded = (index: number, page: Pdfjs.PDFPageViewport): void => { this.props.loaded(page.width, page.height, this.props.pdf.numPages); } + @action getPlaceholderPage = (page: number) => { if (this._isPage[page] !== "none") { @@ -197,12 +285,14 @@ class Viewer extends React.Component<IViewerProps> { ); } } + @action getRenderedPage = (page: number) => { if (this._isPage[page] !== "page") { this._isPage[page] = "page"; this._visibleElements[page] = ( <Page + size={this._pageSizes[page]} pdf={this.props.pdf} page={page} numPages={this.props.pdf.numPages} @@ -215,7 +305,7 @@ class Viewer extends React.Component<IViewerProps> { createAnnotation={this.createAnnotation} sendAnnotations={this.receiveAnnotations} makeAnnotationDocuments={this.makeAnnotationDocument} - receiveAnnotations={this.sendAnnotations} + getScrollFromPage={this.getScrollFromPage} {...this.props} /> ); } @@ -229,10 +319,14 @@ class Viewer extends React.Component<IViewerProps> { if (this._isPage[page] !== "image") { this._isPage[page] = "image"; const address = this.props.url; - let res = JSON.parse(await rp.get(DocServer.prepend(`/thumbnail${address.substring("files/".length, address.length - ".pdf".length)}-${page + 1}.PNG`))); - runInAction(() => this._visibleElements[page] = - <img key={res.path} src={res.path} onError={handleError} - style={{ width: `${parseInt(res.width) * scale}px`, height: `${parseInt(res.height) * scale}px` }} />); + try { + let res = JSON.parse(await rp.get(Utils.prepend(`/thumbnail${address.substring("files/".length, address.length - ".pdf".length)}-${page + 1}.PNG`))); + runInAction(() => this._visibleElements[page] = + <img key={res.path} src={res.path} onError={handleError} + style={{ width: `${parseInt(res.width) * scale}px`, height: `${parseInt(res.height) * scale}px` }} />); + } catch (e) { + console.log(e); + } } } @@ -287,28 +381,6 @@ class Viewer extends React.Component<IViewerProps> { return this._savedAnnotations.getValue(page); } - // createPinAnnotation = (x: number, y: number, page: number): void => { - // let targetDoc = Docs.TextDocument({ width: 100, height: 50, title: "New Pin Annotation" }); - // let pinAnno = new Doc(); - // pinAnno.x = x; - // pinAnno.y = y + this.getScrollFromPage(page); - // pinAnno.width = pinAnno.height = PinRadius; - // pinAnno.page = page; - // pinAnno.target = targetDoc; - // pinAnno.type = AnnotationTypes.Pin; - // // this._annotations.push(pinAnno); - // let annoDoc = new Doc(); - // annoDoc.annotations = new List<Doc>([pinAnno]); - // let annotations = DocListCast(this.props.parent.Document.annotations); - // if (annotations && annotations.length) { - // annotations.push(annoDoc); - // this.props.parent.Document.annotations = new List<Doc>(annotations); - // } - // else { - // this.props.parent.Document.annotations = new List<Doc>([annoDoc]); - // } - // } - // get the page index that the vertical offset passed in is on getPageFromScroll = (vOffset: number) => { let index = 0; @@ -344,37 +416,250 @@ class Viewer extends React.Component<IViewerProps> { } } - renderAnnotation = (anno: Doc): JSX.Element[] => { - let annotationDocs = DocListCast(anno.annotations); - let res = annotationDocs.map(a => { - let type = NumCast(a.type); - switch (type) { - // case AnnotationTypes.Pin: - // return <PinAnnotation parent={this} document={a} x={NumCast(a.x)} y={NumCast(a.y)} width={a[WidthSym]()} height={a[HeightSym]()} key={a[Id]} />; - case AnnotationTypes.Region: - return <RegionAnnotation parent={this} document={a} x={NumCast(a.x)} y={NumCast(a.y)} width={a[WidthSym]()} height={a[HeightSym]()} key={a[Id]} />; - default: - return <div></div>; + renderAnnotation = (anno: Doc, index: number): JSX.Element => { + return <Annotation anno={anno} index={index} parent={this} key={`${anno[Id]}-annotation`} />; + } + + @action + pointerDown = () => { + // this._searching = false; + } + + @action + search = (searchString: string) => { + if (this._pdfViewer._pageViewsReady) { + this._pdfFindController.executeCommand('find', + { + caseSensitive: false, + findPrevious: undefined, + highlightAll: true, + phraseSearch: true, + query: searchString + }); + } + else { + let container = this._mainCont.current; + if (container) { + container.addEventListener("pagesloaded", () => { + console.log("rendered"); + this._pdfFindController.executeCommand('find', + { + caseSensitive: false, + findPrevious: undefined, + highlightAll: true, + phraseSearch: true, + query: searchString + }); + }); + container.addEventListener("pagerendered", () => { + console.log("rendered"); + this._pdfFindController.executeCommand('find', + { + caseSensitive: false, + findPrevious: undefined, + highlightAll: true, + phraseSearch: true, + query: searchString + }); + }); + } + } + + // let viewer = this._viewer.current; + + // if (!this._pdfFindController) { + // if (container && viewer) { + // let simpleLinkService = new SimpleLinkService(); + // let pdfViewer = new PDFJSViewer.PDFViewer({ + // container: container, + // viewer: viewer, + // linkService: simpleLinkService + // }); + // simpleLinkService.setPdf(this.props.pdf); + // container.addEventListener("pagesinit", () => { + // pdfViewer.currentScaleValue = 1; + // }); + // container.addEventListener("pagerendered", () => { + // console.log("rendered"); + // this._pdfFindController.executeCommand('find', + // { + // caseSensitive: false, + // findPrevious: undefined, + // highlightAll: true, + // phraseSearch: true, + // query: searchString + // }); + // }); + // pdfViewer.setDocument(this.props.pdf); + // this._pdfFindController = new PDFJSViewer.PDFFindController(pdfViewer); + // // this._pdfFindController._linkService = pdfLinkService; + // pdfViewer.findController = this._pdfFindController; + // } + // } + // else { + // this._pdfFindController.executeCommand('find', + // { + // caseSensitive: false, + // findPrevious: undefined, + // highlightAll: true, + // phraseSearch: true, + // query: searchString + // }); + // } + } + + searchStringChanged = (e: React.ChangeEvent<HTMLInputElement>) => { + this._searchString = e.currentTarget.value; + } + + @action + toggleSearch = (e: React.MouseEvent) => { + e.stopPropagation(); + this._searching = !this._searching; + + if (this._searching) { + let container = this._mainCont.current; + let viewer = this._viewer.current; + + if (!this._pdfFindController) { + if (container && viewer) { + let simpleLinkService = new SimpleLinkService(); + this._pdfViewer = new PDFJSViewer.PDFViewer({ + container: container, + viewer: viewer, + linkService: simpleLinkService + }); + simpleLinkService.setPdf(this.props.pdf); + container.addEventListener("pagesinit", () => { + this._pdfViewer.currentScaleValue = 1; + }); + container.addEventListener("pagerendered", () => { + console.log("rendered"); + }); + this._pdfViewer.setDocument(this.props.pdf); + this._pdfFindController = new PDFJSViewer.PDFFindController(this._pdfViewer); + // this._pdfFindController._linkService = pdfLinkService; + this._pdfViewer.findController = this._pdfFindController; + } + } + } + else { + this._pdfFindController = null; + if (this._viewer.current) { + let cns = this._viewer.current.childNodes; + for (let i = cns.length - 1; i >= 0; i--) { + cns.item(i).remove(); + } + } + } + } + + @action + prevAnnotation = (e: React.MouseEvent) => { + e.stopPropagation(); + + // if (this.Index > 0) { + // this.Index--; + // } + this.Index = Math.max(this.Index - 1, 0); + } + + @action + nextAnnotation = (e: React.MouseEvent) => { + e.stopPropagation(); + + let compiled = this._script; + let filtered = this._annotations.filter(anno => { + if (compiled && compiled.compiled) { + let run = compiled.run({ this: anno }); + if (run.success) { + return run.result; + } } + return true; }); - return res; + this.Index = Math.min(this.Index + 1, filtered.length - 1); + } + + nextResult = () => { + // if (this._viewer.current) { + // let results = this._pdfFindController.pageMatches; + // if (results && results.length) { + // if (this._pageIndex === this.props.pdf.numPages && this._matchIndex === results[this._pageIndex].length - 1) { + // return; + // } + // if (this._pageIndex === -1 || this._matchIndex === results[this._pageIndex].length - 1) { + // this._matchIndex = 0; + // this._pageIndex++; + // } + // else { + // this._matchIndex++; + // } + // this._pdfFindController._nextMatch() + // let nextMatch = this._viewer.current.children[this._pageIndex].children[1].children[results[this._pageIndex][this._matchIndex]]; + // rconsole.log(nextMatch); + // this.props.parent.scrollTo(nextMatch.getBoundingClientRect().top); + // nextMatch.setAttribute("style", nextMatch.getAttribute("style") ? nextMatch.getAttribute("style") + ", background-color: green" : "background-color: green"); + // } + // } } render() { + let compiled = this._script; return ( - <div ref={this.mainCont} style={{ pointerEvents: "all" }}> - <div className="viewer"> + <div ref={this._mainCont} style={{ pointerEvents: "all" }} onPointerDown={this.pointerDown}> + <div className="viewer" style={this._searching ? { position: "absolute", top: 0 } : {}}> {this._visibleElements} </div> + <div className="pdfViewer-text" ref={this._viewer} style={{ transform: "scale(1.5)", transformOrigin: "top left" }} /> <div className="pdfViewer-annotationLayer" style={{ height: this.props.parent.Document.nativeHeight, width: `100%`, pointerEvents: this.props.parent.props.active() ? "none" : "all" }}> <div className="pdfViewer-annotationLayer-subCont" ref={this._annotationLayer}> - {this._annotations.map(anno => this.renderAnnotation(anno))} + {this._annotations.filter(anno => { + if (compiled && compiled.compiled) { + let run = compiled.run({ this: anno }); + if (run.success) { + return run.result; + } + } + return true; + }).sort((a: Doc, b: Doc) => NumCast(a.y) - NumCast(b.y)) + .map((anno: Doc, index: number) => this.renderAnnotation(anno, index))} </div> </div> + <div className="pdfViewer-overlayCont" onPointerDown={(e) => e.stopPropagation()} + style={{ + bottom: -this.props.scrollY, + left: `${this._searching ? 0 : 100}%` + }}> + <button className="pdfViewer-overlayButton" title="Open Search Bar"></button> + {/* <button title="Previous Result" onClick={() => this.search(this._searchString)}><FontAwesomeIcon icon="arrow-up" size="3x" color="white" /></button> + <button title="Next Result" onClick={this.nextResult}><FontAwesomeIcon icon="arrow-down" size="3x" color="white" /></button> */} + <input onKeyDown={(e: React.KeyboardEvent) => e.keyCode === KeyCodes.ENTER ? this.search(this._searchString) : e.keyCode === KeyCodes.BACKSPACE ? e.stopPropagation() : true} placeholder="Search" className="pdfViewer-overlaySearchBar" onChange={this.searchStringChanged} /> + <button title="Search" onClick={() => this.search(this._searchString)}><FontAwesomeIcon icon="search" size="3x" color="white" /></button> + </div> + <button className="pdfViewer-overlayButton" onClick={this.prevAnnotation} title="Previous Annotation" + style={{ bottom: -this.props.scrollY + 280, right: 10, display: this.props.parent.props.active() ? "flex" : "none" }}> + <div className="pdfViewer-overlayButton-iconCont" onPointerDown={(e) => e.stopPropagation()}> + <FontAwesomeIcon style={{ color: "white" }} icon={"arrow-up"} size="3x" /> + </div> + </button> + <button className="pdfViewer-overlayButton" onClick={this.nextAnnotation} title="Next Annotation" + style={{ bottom: -this.props.scrollY + 200, right: 10, display: this.props.parent.props.active() ? "flex" : "none" }}> + <div className="pdfViewer-overlayButton-iconCont" onPointerDown={(e) => e.stopPropagation()}> + <FontAwesomeIcon style={{ color: "white" }} icon={"arrow-down"} size="3x" /> + </div> + </button> + <button className="pdfViewer-overlayButton" onClick={this.toggleSearch} title="Open Search Bar" + style={{ bottom: -this.props.scrollY + 10, right: 0, display: this.props.parent.props.active() ? "flex" : "none" }}> + <div className="pdfViewer-overlayButton-arrow" onPointerDown={(e) => e.stopPropagation()}></div> + <div className="pdfViewer-overlayButton-iconCont" onPointerDown={(e) => e.stopPropagation()}> + <FontAwesomeIcon style={{ color: "white" }} icon={this._searching ? "times" : "search"} size="3x" /> + </div> + </button> </div > ); } @@ -384,203 +669,37 @@ export enum AnnotationTypes { Region } -interface IAnnotationProps { - x: number; - y: number; - width: number; - height: number; - parent: Viewer; - document: Doc; -} - -// @observer -// class PinAnnotation extends React.Component<IAnnotationProps> { -// @observable private _backgroundColor: string = "green"; -// @observable private _display: string = "initial"; - -// private _mainCont: React.RefObject<HTMLDivElement>; - -// constructor(props: IAnnotationProps) { -// super(props); -// this._mainCont = React.createRef(); -// } - -// componentDidMount = () => { -// let selected = this.props.document.selected; -// if (!BoolCast(selected)) { -// runInAction(() => { -// this._backgroundColor = "red"; -// this._display = "none"; -// }); -// } -// if (selected) { -// if (BoolCast(selected)) { -// runInAction(() => { -// this._backgroundColor = "green"; -// this._display = "initial"; -// }); -// } -// else { -// runInAction(() => { -// this._backgroundColor = "red"; -// this._display = "none"; -// }); -// } -// } -// else { -// runInAction(() => { -// this._backgroundColor = "red"; -// this._display = "none"; -// }); -// } -// } - -// @action -// pointerDown = (e: React.PointerEvent) => { -// let selected = this.props.document.selected; -// if (selected && BoolCast(selected)) { -// this._backgroundColor = "red"; -// this._display = "none"; -// this.props.document.selected = false; -// } -// else { -// this._backgroundColor = "green"; -// this._display = "initial"; -// this.props.document.selected = true; -// } -// e.preventDefault(); -// e.stopPropagation(); -// } - -// @action -// doubleClick = (e: React.MouseEvent) => { -// if (this._mainCont.current) { -// let annotations = DocListCast(this.props.parent.props.parent.Document.annotations); -// if (annotations && annotations.length) { -// let index = annotations.indexOf(this.props.document); -// annotations.splice(index, 1); -// this.props.parent.props.parent.Document.annotations = new List<Doc>(annotations); -// } -// // this._mainCont.current.childNodes.forEach(e => e.remove()); -// this._mainCont.current.style.display = "none"; -// // if (this._mainCont.current.parentElement) { -// // this._mainCont.current.remove(); -// // } -// } -// e.stopPropagation(); -// } - -// render() { -// let targetDoc = Cast(this.props.document.target, Doc); -// if (targetDoc instanceof Doc) { -// return ( -// <div className="pdfViewer-pinAnnotation" onPointerDown={this.pointerDown} -// onDoubleClick={this.doubleClick} ref={this._mainCont} -// style={{ -// top: this.props.y * scale - PinRadius / 2, left: this.props.x * scale - PinRadius / 2, width: PinRadius, -// height: PinRadius, pointerEvents: "all", backgroundColor: this._backgroundColor -// }}> -// <div style={{ -// position: "absolute", top: "25px", left: "25px", transform: "scale(3)", transformOrigin: "top left", -// display: this._display, width: targetDoc[WidthSym](), height: targetDoc[HeightSym]() -// }}> -// <DocumentView Document={targetDoc} -// ContainingCollectionView={undefined} -// ScreenToLocalTransform={this.props.parent.props.parent.props.ScreenToLocalTransform} -// isTopMost={false} -// ContentScaling={() => 1} -// PanelWidth={() => NumCast(this.props.parent.props.parent.Document.nativeWidth)} -// PanelHeight={() => NumCast(this.props.parent.props.parent.Document.nativeHeight)} -// focus={emptyFunction} -// selectOnLoad={false} -// parentActive={this.props.parent.props.parent.props.active} -// whenActiveChanged={this.props.parent.props.parent.props.whenActiveChanged} -// bringToFront={emptyFunction} -// addDocTab={this.props.parent.props.parent.props.addDocTab} -// /> -// </div> -// </div > -// ); -// } -// return null; -// } -// } - -class RegionAnnotation extends React.Component<IAnnotationProps> { - @observable private _backgroundColor: string = "red"; - - private _reactionDisposer?: IReactionDisposer; - private _mainCont: React.RefObject<HTMLDivElement>; +class SimpleLinkService { + externalLinkTarget: any = null; + externalLinkRel: any = null; + pdf: any = null; - constructor(props: IAnnotationProps) { - super(props); + navigateTo() { } - this._mainCont = React.createRef(); - } + getDestinationHash() { return "#"; } - componentDidMount() { - this._reactionDisposer = reaction( - () => BoolCast(this.props.document.delete), - () => { - if (BoolCast(this.props.document.delete)) { - if (this._mainCont.current) { - this._mainCont.current.style.display = "none"; - } - } - }, - { fireImmediately: true } - ); - } + getAnchorUrl() { return "#"; } - componentWillUnmount() { - this._reactionDisposer && this._reactionDisposer(); - } + setHash() { } - deleteAnnotation = () => { - let annotation = DocListCast(this.props.parent.props.parent.Document.annotations); - let group = FieldValue(Cast(this.props.document.group, Doc)); - if (group && annotation.indexOf(group) !== -1) { - let newAnnotations = annotation.filter(a => a !== FieldValue(Cast(this.props.document.group, Doc))); - this.props.parent.props.parent.Document.annotations = new List<Doc>(newAnnotations); - } + executeNamedAction() { } - if (group) { - let groupAnnotations = DocListCast(group.annotations); - groupAnnotations.forEach(anno => anno.delete = true); - } + cachePageRef() { } - PDFMenu.Instance.fadeOut(true); + get pagesCount() { + return this.pdf ? this.pdf.numPages : 0; } + get page() { + return 0; + } - // annotateThis = (e: PointerEvent) => { - // e.preventDefault(); - // e.stopPropagation(); - // // document that this annotation is linked to - // let targetDoc = Docs.TextDocument({ width: 200, height: 200, title: "New Annotation" }); - // let group = FieldValue(Cast(this.props.document.group, Doc)); - // } - - @action - onPointerDown = (e: React.PointerEvent) => { - if (e.button === 0) { - let targetDoc = Cast(this.props.document.target, Doc, null); - if (targetDoc) { - DocumentManager.Instance.jumpToDocument(targetDoc); - } - } - if (e.button === 2) { - PDFMenu.Instance.Status = "annotation"; - PDFMenu.Instance.Delete = this.deleteAnnotation; - PDFMenu.Instance.Pinned = false; - PDFMenu.Instance.jumpTo(e.clientX, e.clientY, true); - } + setPdf(pdf: any) { + this.pdf = pdf; } - render() { - return ( - <div className="pdfViewer-annotationBox" onPointerDown={this.onPointerDown} ref={this._mainCont} - style={{ top: this.props.y * scale, left: this.props.x * scale, width: this.props.width * scale, height: this.props.height * scale, pointerEvents: "all", backgroundColor: StrCast(this.props.document.color) }}></div> - ); + get rotation() { + return 0; } + set rotation(value: any) { } }
\ No newline at end of file diff --git a/src/client/views/pdf/Page.tsx b/src/client/views/pdf/Page.tsx index 734dff7fc..c9d442fe5 100644 --- a/src/client/views/pdf/Page.tsx +++ b/src/client/views/pdf/Page.tsx @@ -2,7 +2,7 @@ import { observer } from "mobx-react"; import React = require("react"); import { observable, action, runInAction, IReactionDisposer, reaction } from "mobx"; import * as Pdfjs from "pdfjs-dist"; -import { Opt, Doc, FieldResult, Field, DocListCast, WidthSym, HeightSym } from "../../../new_fields/Doc"; +import { Opt, Doc, FieldResult, Field, DocListCast, WidthSym, HeightSym, DocListCastAsync } from "../../../new_fields/Doc"; import "./PDFViewer.scss"; import "pdfjs-dist/web/pdf_viewer.css"; import { PDFBox } from "../nodes/PDFBox"; @@ -10,15 +10,17 @@ import { DragManager } from "../../util/DragManager"; import { Docs, DocUtils } from "../../documents/Documents"; import { List } from "../../../new_fields/List"; import { emptyFunction } from "../../../Utils"; -import { Cast, NumCast, StrCast } from "../../../new_fields/Types"; +import { Cast, NumCast, StrCast, BoolCast } from "../../../new_fields/Types"; import { listSpec } from "../../../new_fields/Schema"; import { menuBar } from "prosemirror-menu"; import { AnnotationTypes, PDFViewer, scale } from "./PDFViewer"; import PDFMenu from "./PDFMenu"; import { UndoManager } from "../../util/UndoManager"; +import { copy } from "typescript-collections/dist/lib/arrays"; interface IPageProps { + size: { width: number, height: number }; pdf: Opt<Pdfjs.PDFDocumentProxy>; name: string; numPages: number; @@ -28,16 +30,16 @@ interface IPageProps { renderAnnotations: (annotations: Doc[], removeOld: boolean) => void; makePin: (x: number, y: number, page: number) => void; sendAnnotations: (annotations: HTMLDivElement[], page: number) => void; - receiveAnnotations: (page: number) => HTMLDivElement[] | undefined; createAnnotation: (div: HTMLDivElement, page: number) => void; - makeAnnotationDocuments: (doc: Doc | undefined, scale: number, color: string) => Doc; + makeAnnotationDocuments: (doc: Doc | undefined, scale: number, color: string, linkTo: boolean) => Doc; + getScrollFromPage: (page: number) => number; } @observer export default class Page extends React.Component<IPageProps> { @observable private _state: string = "N/A"; - @observable private _width: number = 0; - @observable private _height: number = 0; + @observable private _width: number = this.props.size.width; + @observable private _height: number = this.props.size.height; @observable private _page: Opt<Pdfjs.PDFPageProxy>; @observable private _currPage: number = this.props.page + 1; @observable private _marqueeX: number = 0; @@ -50,9 +52,11 @@ export default class Page extends React.Component<IPageProps> { private _textLayer: React.RefObject<HTMLDivElement>; private _annotationLayer: React.RefObject<HTMLDivElement>; private _marquee: React.RefObject<HTMLDivElement>; - private _curly: React.RefObject<HTMLImageElement>; + // private _curly: React.RefObject<HTMLImageElement>; private _marqueeing: boolean = false; private _reactionDisposer?: IReactionDisposer; + private _startY: number = 0; + private _startX: number = 0; constructor(props: IPageProps) { super(props); @@ -60,7 +64,7 @@ export default class Page extends React.Component<IPageProps> { this._textLayer = React.createRef(); this._annotationLayer = React.createRef(); this._marquee = React.createRef(); - this._curly = React.createRef(); + // this._curly = React.createRef(); } componentDidMount = (): void => { @@ -135,10 +139,10 @@ export default class Page extends React.Component<IPageProps> { @action highlight = (targetDoc?: Doc, color: string = "red") => { // creates annotation documents for current highlights - let annotationDoc = this.props.makeAnnotationDocuments(targetDoc, scale, color); - let targetAnnotations = Cast(this.props.parent.Document.annotations, listSpec(Doc)); + let annotationDoc = this.props.makeAnnotationDocuments(targetDoc, scale, color, false); + let targetAnnotations = Cast(this.props.parent.fieldExtensionDoc.annotations, listSpec(Doc)); if (targetAnnotations === undefined) { - Doc.GetProto(this.props.parent.Document).annotations = new List([annotationDoc]); + Doc.GetProto(this.props.parent.fieldExtensionDoc).annotations = new List([annotationDoc]); } else { targetAnnotations.push(annotationDoc); } @@ -150,20 +154,34 @@ export default class Page extends React.Component<IPageProps> { * start a drag event and create or put the necessary info into the drag event. */ @action - startDrag = (e: PointerEvent): void => { + startDrag = (e: PointerEvent, ele: HTMLElement): void => { e.preventDefault(); e.stopPropagation(); let thisDoc = this.props.parent.Document; // document that this annotation is linked to - let targetDoc = Docs.TextDocument({ width: 200, height: 200, title: "New Annotation" }); + let targetDoc = Docs.Create.TextDocument({ width: 200, height: 200, title: "New Annotation" }); targetDoc.targetPage = this.props.page; - let annotationDoc = this.highlight(targetDoc, "red"); + let annotationDoc = this.highlight(undefined, "red"); + annotationDoc.linkedToDoc = false; // create dragData and star tdrag let dragData = new DragManager.AnnotationDragData(thisDoc, annotationDoc, targetDoc); if (this._textLayer.current) { - DragManager.StartAnnotationDrag([this._textLayer.current], dragData, e.pageX, e.pageY, { + DragManager.StartAnnotationDrag([ele], dragData, e.pageX, e.pageY, { handlers: { - dragComplete: emptyFunction, + dragComplete: async () => { + if (!(await annotationDoc.linkedToDoc)) { + let annotations = await DocListCastAsync(annotationDoc.annotations); + if (annotations) { + annotations.forEach(anno => { + anno.target = targetDoc; + }); + } + let pdfDoc = await Cast(annotationDoc.pdfDoc, Doc); + if (pdfDoc) { + DocUtils.MakeLink(annotationDoc, targetDoc, undefined, `Annotation from ${StrCast(pdfDoc.title)}`, "", StrCast(pdfDoc.title)); + } + } + } }, hideSource: false }); @@ -177,6 +195,21 @@ export default class Page extends React.Component<IPageProps> { e.stopPropagation(); } + createSnippet = (marquee: { left: number, top: number, width: number, height: number }): void => { + let doc = this.props.parent.Document; + let view = Doc.MakeAlias(doc); + let data = Doc.MakeDelegate(doc.proto!); + data.title = StrCast(data.title) + "_snippet"; + view.proto = data; + view.nativeHeight = marquee.height; + view.height = (doc[WidthSym]() / NumCast(doc.nativeWidth)) * marquee.height; + view.nativeWidth = doc.nativeWidth; + view.startY = marquee.top + this.props.getScrollFromPage(this.props.page); + view.width = doc[WidthSym](); + let dragData = new DragManager.DocumentDragData([view], [undefined]); + DragManager.StartDocumentDrag([], dragData, 0, 0); + } + @action onPointerDown = (e: React.PointerEvent): void => { // if alt+left click, drag and annotate @@ -191,6 +224,7 @@ export default class Page extends React.Component<IPageProps> { else if (e.button === 0) { PDFMenu.Instance.StartDrag = this.startDrag; PDFMenu.Instance.Highlight = this.highlight; + PDFMenu.Instance.Snippet = this.createSnippet; PDFMenu.Instance.Status = "pdf"; PDFMenu.Instance.fadeOut(true); let target: any = e.target; @@ -202,8 +236,8 @@ export default class Page extends React.Component<IPageProps> { let current = this._textLayer.current; if (current) { let boundingRect = current.getBoundingClientRect(); - this._marqueeX = (e.clientX - boundingRect.left) * (current.offsetWidth / boundingRect.width); - this._marqueeY = (e.clientY - boundingRect.top) * (current.offsetHeight / boundingRect.height); + this._startX = this._marqueeX = (e.clientX - boundingRect.left) * (current.offsetWidth / boundingRect.width); + this._startY = this._marqueeY = (e.clientY - boundingRect.top) * (current.offsetHeight / boundingRect.height); } this._marqueeing = true; if (this._marquee.current) this._marquee.current.style.opacity = "0.2"; @@ -226,12 +260,16 @@ export default class Page extends React.Component<IPageProps> { if (current) { // transform positions and find the width and height to set the marquee to let boundingRect = current.getBoundingClientRect(); - this._marqueeWidth = (e.clientX - boundingRect.left) * (current.offsetWidth / boundingRect.width) - this._marqueeX; - this._marqueeHeight = (e.clientY - boundingRect.top) * (current.offsetHeight / boundingRect.height) - this._marqueeY; + this._marqueeWidth = ((e.clientX - boundingRect.left) * (current.offsetWidth / boundingRect.width)) - this._startX; + this._marqueeHeight = ((e.clientY - boundingRect.top) * (current.offsetHeight / boundingRect.height)) - this._startY; + 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); let { background, opacity, transform: transform } = this.getCurlyTransform(); - if (this._marquee.current && this._curly.current) { + if (this._marquee.current /*&& this._curly.current*/) { this._marquee.current.style.background = background; - this._curly.current.style.opacity = opacity; + // this._curly.current.style.opacity = opacity; this._rotate = transform; } } @@ -244,33 +282,33 @@ export default class Page extends React.Component<IPageProps> { } getCurlyTransform = (): { background: string, opacity: string, transform: string } => { - let background = "", opacity = "", transform = ""; - if (this._marquee.current && this._curly.current) { - if (this._marqueeWidth > 100 && this._marqueeHeight > 100) { - background = "red"; - opacity = "0"; - } - else { - background = "transparent"; - opacity = "1"; - } + // let background = "", opacity = "", transform = ""; + // if (this._marquee.current && this._curly.current) { + // if (this._marqueeWidth > 100 && this._marqueeHeight > 100) { + // background = "red"; + // opacity = "0"; + // } + // else { + // background = "transparent"; + // opacity = "1"; + // } - // split up for simplicity, could be done in a nested ternary. please do not. -syip2 - let ratio = this._marqueeWidth / this._marqueeHeight; - if (ratio > 1.5) { - // vertical - transform = "rotate(90deg) scale(1, 5)"; - } - else if (ratio < 0.5) { - // horizontal - transform = "scale(2, 1)"; - } - else { - // diagonal - transform = "rotate(45deg) scale(1.5, 1.5)"; - } - } - return { background: background, opacity: opacity, transform: transform }; + // // split up for simplicity, could be done in a nested ternary. please do not. -syip2 + // let ratio = this._marqueeWidth / this._marqueeHeight; + // if (ratio > 1.5) { + // // vertical + // transform = "rotate(90deg) scale(1, 5)"; + // } + // else if (ratio < 0.5) { + // // horizontal + // transform = "scale(2, 1)"; + // } + // else { + // // diagonal + // transform = "rotate(45deg) scale(1.5, 1.5)"; + // } + // } + return { background: "red", opacity: "0.5", transform: "" }; } @action @@ -280,31 +318,37 @@ export default class Page extends React.Component<IPageProps> { if (this._marquee.current) { let copy = document.createElement("div"); // make a copy of the marquee - copy.style.left = this._marquee.current.style.left; - copy.style.top = this._marquee.current.style.top; - copy.style.width = this._marquee.current.style.width; - copy.style.height = this._marquee.current.style.height; + let style = this._marquee.current.style; + copy.style.left = style.left; + copy.style.top = style.top; + copy.style.width = style.width; + copy.style.height = style.height; // apply the appropriate background, opacity, and transform let { background, opacity, transform } = this.getCurlyTransform(); copy.style.background = background; // if curly bracing, add a curly brace - if (opacity === "1" && this._curly.current) { - copy.style.opacity = opacity; - let img = this._curly.current.cloneNode(); - (img as any).style.opacity = opacity; - (img as any).style.transform = transform; - copy.appendChild(img); - } - else { - copy.style.opacity = this._marquee.current.style.opacity; - } + // if (opacity === "1" && this._curly.current) { + // copy.style.opacity = opacity; + // let img = this._curly.current.cloneNode(); + // (img as any).style.opacity = opacity; + // (img as any).style.transform = transform; + // copy.appendChild(img); + // } + // else { + copy.style.border = style.border; + copy.style.opacity = style.opacity; + // } copy.className = this._marquee.current.className; this.props.createAnnotation(copy, this.props.page); this._marquee.current.style.opacity = "0"; } if (this._marqueeWidth > 10 || this._marqueeHeight > 10) { + if (!e.ctrlKey) { + PDFMenu.Instance.Status = "snippet"; + PDFMenu.Instance.Marquee = { left: this._marqueeX, top: this._marqueeY, width: this._marqueeWidth, height: this._marqueeHeight }; + } PDFMenu.Instance.jumpTo(e.clientX, e.clientY); } @@ -320,7 +364,7 @@ export default class Page extends React.Component<IPageProps> { if (PDFMenu.Instance.Highlighting) { - this.highlight(undefined, "#f4f442"); + this.highlight(undefined, "goldenrod"); } else { PDFMenu.Instance.StartDrag = this.startDrag; @@ -381,8 +425,8 @@ export default class Page extends React.Component<IPageProps> { </div> <div className="pdfInkingLayer-cont" ref={this._annotationLayer} style={{ width: "100%", height: "100%", position: "relative", top: "-100%" }}> <div className="pdfViewer-annotationBox" ref={this._marquee} - style={{ left: `${this._marqueeX}px`, top: `${this._marqueeY}px`, width: `${this._marqueeWidth}px`, height: `${this._marqueeHeight}px`, background: "transparent" }}> - <img ref={this._curly} src="https://static.thenounproject.com/png/331760-200.png" style={{ width: "100%", height: "100%", transform: `${this._rotate}` }} /> + style={{ left: `${this._marqueeX}px`, top: `${this._marqueeY}px`, width: `${this._marqueeWidth}px`, height: `${this._marqueeHeight}px`, background: "red", border: `${this._marqueeWidth === 0 ? "" : "10px dashed black"}` }}> + {/* <img ref={this._curly} src="https://static.thenounproject.com/png/331760-200.png" style={{ width: "100%", height: "100%", transform: `${this._rotate}` }} /> */} </div> </div> <div className="textlayer" ref={this._textLayer} style={{ "position": "relative", "top": `-${2 * this._height}px`, "height": `${this._height}px` }} /> |
