aboutsummaryrefslogtreecommitdiff
path: root/src/client/views/pdf/Page.tsx
diff options
context:
space:
mode:
authorloudonclear <loudon_cohen@brown.edu>2019-06-07 18:14:39 -0400
committerloudonclear <loudon_cohen@brown.edu>2019-06-07 18:14:39 -0400
commit5d799d1a26fa12d666b11b80776151edcbbd67f8 (patch)
tree0f9b2387643367cc7e541c1d33957c6dc72a3ac3 /src/client/views/pdf/Page.tsx
parente96d0be82b19a4e108447ba5afc6cdc5deb07110 (diff)
some comments, switching computers
Diffstat (limited to 'src/client/views/pdf/Page.tsx')
-rw-r--r--src/client/views/pdf/Page.tsx382
1 files changed, 382 insertions, 0 deletions
diff --git a/src/client/views/pdf/Page.tsx b/src/client/views/pdf/Page.tsx
new file mode 100644
index 000000000..7cd72b27f
--- /dev/null
+++ b/src/client/views/pdf/Page.tsx
@@ -0,0 +1,382 @@
+import { observer } from "mobx-react";
+import React = require("react");
+import { observable, action, runInAction } from "mobx";
+import * as Pdfjs from "pdfjs-dist";
+import { Opt, Doc, FieldResult, Field } from "../../../new_fields/Doc";
+import "./PDFViewer.scss";
+import "pdfjs-dist/web/pdf_viewer.css";
+import { PDFBox } from "../nodes/PDFBox";
+import { DragManager } from "../../util/DragManager";
+import { Docs } from "../../documents/Documents";
+import { List } from "../../../new_fields/List";
+import { emptyFunction } from "../../../Utils";
+import { Cast, NumCast } from "../../../new_fields/Types";
+import { listSpec } from "../../../new_fields/Schema";
+
+interface IPageProps {
+ pdf: Opt<Pdfjs.PDFDocumentProxy>;
+ name: string;
+ numPages: number;
+ page: number;
+ pageLoaded: (index: number, page: Pdfjs.PDFPageViewport) => void;
+ parent: PDFBox;
+}
+
+@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 _page: Opt<Pdfjs.PDFPageProxy>;
+ @observable private _currPage: number = this.props.page + 1;
+ @observable private _marqueeX: number = 0;
+ @observable private _marqueeY: number = 0;
+ @observable private _marqueeWidth: number = 0;
+ @observable private _marqueeHeight: number = 0;
+ @observable private _rotate: string = "";
+ @observable private _annotations: List<Doc> = new List<Doc>();
+
+ private _canvas: React.RefObject<HTMLCanvasElement>;
+ private _textLayer: React.RefObject<HTMLDivElement>;
+ private _annotationLayer: React.RefObject<HTMLDivElement>;
+ private _marquee: React.RefObject<HTMLDivElement>;
+ private _curly: React.RefObject<HTMLImageElement>;
+ private _currentAnnotations: HTMLDivElement[] = [];
+ private _marqueeing: boolean = false;
+ private _dragging: boolean = false;
+
+ constructor(props: IPageProps) {
+ super(props);
+ this._canvas = React.createRef();
+ this._textLayer = React.createRef();
+ this._annotationLayer = React.createRef();
+ this._marquee = React.createRef();
+ this._curly = React.createRef();
+ }
+
+ componentDidMount() {
+ if (this.props.pdf) {
+ this.update(this.props.pdf);
+ }
+ }
+
+ componentDidUpdate() {
+ if (this.props.pdf) {
+ this.update(this.props.pdf);
+ }
+ }
+
+ private update = (pdf: Pdfjs.PDFDocumentProxy) => {
+ if (pdf) {
+ this.loadPage(pdf);
+ }
+ else {
+ this._state = "loading";
+ }
+ }
+
+ private loadPage = (pdf: Pdfjs.PDFDocumentProxy) => {
+ if (this._state === "rendering" || this._page) return;
+
+ pdf.getPage(this._currPage).then(
+ (page: Pdfjs.PDFPageProxy) => {
+ this._state = "rendering";
+ this.renderPage(page);
+ }
+ );
+ }
+
+ @action
+ private renderPage = (page: Pdfjs.PDFPageProxy) => {
+ // lower scale = easier to read at small sizes, higher scale = easier to read at large sizes
+ let scale = 2;
+ let viewport = page.getViewport(scale);
+ let canvas = this._canvas.current;
+ let textLayer = this._textLayer.current;
+ if (canvas && textLayer) {
+ let ctx = canvas.getContext("2d");
+ canvas.width = viewport.width;
+ this._width = viewport.width;
+ canvas.height = viewport.height;
+ this._height = viewport.height;
+ this.props.pageLoaded(this._currPage, viewport);
+ if (ctx) {
+ // renders the page onto the canvas context
+ page.render({ canvasContext: ctx, viewport: viewport });
+ // renders text onto the text container
+ page.getTextContent().then((res: Pdfjs.TextContent) => {
+ //@ts-ignore
+ Pdfjs.renderTextLayer({
+ textContent: res,
+ container: textLayer,
+ viewport: viewport
+ });
+ });
+
+ this._page = page;
+ }
+ }
+ }
+
+ /**
+ * @param targetDoc: Document that annotations are linked to
+ * This method makes the list of current annotations into documents linked to
+ * the parameter passed in.
+ */
+ makeAnnotationDocuments = (targetDoc: Doc): Doc[] => {
+ let annoDocs: Doc[] = [];
+ for (let anno of this._currentAnnotations) {
+ let annoDoc = new Doc();
+ annoDoc.x = anno.offsetLeft;
+ annoDoc.y = anno.offsetTop;
+ annoDoc.height = anno.offsetHeight;
+ annoDoc.width = anno.offsetWidth;
+ annoDoc.target = targetDoc;
+ annoDocs.push(annoDoc);
+ anno.remove();
+ }
+ this._currentAnnotations = [];
+ return annoDocs;
+ }
+
+ /**
+ * This is temporary for creating annotations from highlights. It will
+ * start a drag event and create or put the necessary info into the drag event.
+ */
+ @action
+ startDrag = (e: PointerEvent) => {
+ // the first 5 lines is a hack to prevent text selection while dragging
+ e.preventDefault();
+ e.stopPropagation();
+ if (this._dragging) {
+ return;
+ }
+ this._dragging = true;
+ let thisDoc = this.props.parent.Document;
+ // document that this annotation is linked to
+ let targetDoc = Docs.TextDocument({ width: 200, height: 200, title: "New Annotation" });
+ targetDoc.targetPage = this.props.page;
+ // creates annotation documents for current highlights
+ let annotationDocs = this.makeAnnotationDocuments(targetDoc);
+ let targetAnnotations = Cast(targetDoc.annotations, listSpec(Doc));
+ if (targetAnnotations) {
+ targetAnnotations.push(...annotationDocs);
+ targetDoc.annotations = targetAnnotations;
+ }
+ // temporary code (currently broken ? 6/7/19) to get annotations to rerender on pdf
+ let thisAnnotations = Cast(this.props.parent.Document.annotations, listSpec(Doc));
+ if (thisAnnotations) {
+ thisAnnotations.push(...annotationDocs);
+ this.props.parent.Document.annotations = thisAnnotations;
+ let temp = new List<Doc>(thisAnnotations);
+ temp.push(...this._annotations);
+ this._annotations = temp;
+ }
+ // create dragData and star tdrag
+ let dragData = new DragManager.AnnotationDragData(thisDoc, annotationDocs, targetDoc);
+ if (this._textLayer.current) {
+ DragManager.StartAnnotationDrag([this._textLayer.current], dragData, e.pageX, e.pageY, {
+ handlers: {
+ dragComplete: action(emptyFunction),
+ },
+ hideSource: false
+ });
+ }
+ }
+
+ // cleans up events and boolean
+ endDrag = (e: PointerEvent): void => {
+ document.removeEventListener("pointermove", this.startDrag);
+ document.removeEventListener("pointerup", this.endDrag);
+ this._dragging = false;
+ e.stopPropagation();
+ }
+
+ @action
+ onPointerDown = (e: React.PointerEvent) => {
+ // if alt+left click, drag and annotate
+ if (e.altKey && e.button === 0) {
+ e.stopPropagation();
+
+ document.removeEventListener("pointermove", this.startDrag);
+ document.addEventListener("pointermove", this.startDrag);
+ document.removeEventListener("pointerup", this.endDrag);
+ document.addEventListener("pointerup", this.endDrag);
+ }
+ else if (e.button === 0) {
+ let target: any = e.target;
+ if (target && target.parentElement === this._textLayer.current) {
+ e.stopPropagation();
+ }
+ else {
+ e.stopPropagation();
+ // set marquee x and y positions to the spatially transformed position
+ 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._marqueeing = true;
+ if (this._marquee.current) this._marquee.current.style.opacity = "0.2";
+ }
+ document.removeEventListener("pointermove", this.onPointerMove);
+ document.addEventListener("pointermove", this.onPointerMove);
+ document.removeEventListener("pointerup", this.onPointerUp);
+ document.addEventListener("pointerup", this.onPointerUp);
+ if (!e.ctrlKey) {
+ for (let anno of this._currentAnnotations) {
+ anno.remove();
+ }
+ this._currentAnnotations = [];
+ }
+ }
+ }
+
+ @action
+ onPointerMove = (e: PointerEvent) => {
+ let target: any = e.target;
+ if (this._marqueeing) {
+ let current = this._textLayer.current;
+ if (current) {
+ 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;
+ if (this._marquee.current && this._curly.current) {
+ if (this._marqueeWidth > 100 && this._marqueeHeight > 100) {
+ this._marquee.current.style.background = "red";
+ this._curly.current.style.opacity = "0";
+ }
+ else {
+ this._marquee.current.style.background = "transparent";
+ this._curly.current.style.opacity = "1";
+ }
+
+ let ratio = this._marqueeWidth / this._marqueeHeight;
+ if (ratio > 1.5) {
+ // vertical
+ this._rotate = "rotate(90deg) scale(1, 2)";
+ }
+ else if (ratio < 0.5) {
+ // horizontal
+ this._rotate = "scale(2, 1)";
+ }
+ else {
+ // diagonal
+ this._rotate = "rotate(45deg) scale(1.5, 1.5)";
+ }
+ }
+ }
+ e.stopPropagation();
+ e.preventDefault();
+ }
+ else if (target && target.parentElement === this._textLayer.current) {
+ e.stopPropagation();
+ }
+ }
+
+ startAnnotation = () => {
+ console.log("drag starting");
+ }
+
+ pointerDownCancel = (e: PointerEvent) => {
+ e.stopPropagation();
+ }
+
+ @action
+ onPointerUp = () => {
+ if (this._marqueeing) {
+ this._marqueeing = false;
+ if (this._marquee.current) {
+ let copy = document.createElement("div");
+ 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;
+ copy.style.opacity = this._marquee.current.style.opacity;
+ copy.className = this._marquee.current.className;
+ if (this._annotationLayer.current) {
+ this._annotationLayer.current.append(copy);
+ this._currentAnnotations.push(copy);
+ }
+ this._marquee.current.style.opacity = "0";
+ }
+
+ this._marqueeHeight = this._marqueeWidth = 0;
+ }
+ else {
+ let sel = window.getSelection();
+ // if selecting over a range of things
+ if (sel && sel.type === "Range") {
+ let clientRects = sel.getRangeAt(0).getClientRects();
+ if (this._textLayer.current) {
+ let boundingRect = this._textLayer.current.getBoundingClientRect();
+ for (let i = 0; i < clientRects.length; i++) {
+ let rect = clientRects.item(i);
+ if (rect) {
+ let annoBox = document.createElement("div");
+ annoBox.className = "pdfViewer-annotationBox";
+ // transforms the positions from screen onto the pdf div
+ annoBox.style.top = ((rect.top - boundingRect.top) * (this._textLayer.current.offsetHeight / boundingRect.height)).toString();
+ annoBox.style.left = ((rect.left - boundingRect.left) * (this._textLayer.current.offsetWidth / boundingRect.width)).toString();
+ annoBox.style.width = (rect.width * this._textLayer.current.offsetWidth / boundingRect.width).toString();
+ annoBox.style.height = (rect.height * this._textLayer.current.offsetHeight / boundingRect.height).toString();
+ annoBox.ondragstart = this.startAnnotation;
+ annoBox.onpointerdown = this.pointerDownCancel;
+ if (this._annotationLayer.current) this._annotationLayer.current.append(annoBox);
+ this._currentAnnotations.push(annoBox);
+ }
+ }
+ }
+ if (sel.empty) { // Chrome
+ sel.empty();
+ } else if (sel.removeAllRanges) { // Firefox
+ sel.removeAllRanges();
+ }
+ }
+ }
+ document.removeEventListener("pointermove", this.onPointerMove);
+ document.removeEventListener("pointerup", this.onPointerUp);
+ }
+
+ renderAnnotation = (anno: Doc | undefined): HTMLDivElement => {
+ let annoBox = document.createElement("div");
+ if (anno) {
+ annoBox.className = "pdfViewer-annotationBox";
+ // transforms the positions from screen onto the pdf div
+ annoBox.style.top = NumCast(anno.x).toString();
+ annoBox.style.left = NumCast(anno.y).toString();
+ annoBox.style.width = NumCast(anno.width).toString();
+ annoBox.style.height = NumCast(anno.height).toString()
+ annoBox.onpointerdown = this.pointerDownCancel;
+ }
+ return annoBox;
+ }
+
+ annotationPointerDown = () => {
+ console.log("annotation");
+ }
+
+ // imgVisible = () => {
+ // return this._marqueeWidth < 100 && this._marqueeHeight < 100 ? { opacity: "1" } : { opacity: "0" }
+ // }
+
+ render() {
+ let annotations = this._annotations;
+ return (
+ <div onPointerDown={this.onPointerDown} className={this.props.name} style={{ "width": this._width, "height": this._height }}>
+ <div className="canvasContainer">
+ <canvas ref={this._canvas} />
+ </div>
+ <div className="pdfAnnotationLayer-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}` }} />
+ </div>
+ {annotations.map(anno => this.renderAnnotation(anno instanceof Doc ? anno : undefined))}
+ </div>
+ <div className="textlayer" ref={this._textLayer} style={{ "position": "relative", "top": `-${2 * this._height}px`, "height": `${this._height}px` }} />
+ </div>
+ );
+ }
+} \ No newline at end of file