aboutsummaryrefslogtreecommitdiff
path: root/src/client/views/nodes/PDFNode.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'src/client/views/nodes/PDFNode.tsx')
-rw-r--r--src/client/views/nodes/PDFNode.tsx453
1 files changed, 453 insertions, 0 deletions
diff --git a/src/client/views/nodes/PDFNode.tsx b/src/client/views/nodes/PDFNode.tsx
new file mode 100644
index 000000000..755994d6d
--- /dev/null
+++ b/src/client/views/nodes/PDFNode.tsx
@@ -0,0 +1,453 @@
+import 'react-image-lightbox/style.css';
+import "./ImageBox.scss";
+import React = require("react")
+import { observer } from "mobx-react"
+import { observable, action } from 'mobx';
+import 'react-pdf/dist/Page/AnnotationLayer.css'
+//@ts-ignore
+import { Document, Page, PDFPageProxy, PageAnnotation } from "react-pdf";
+import { Utils } from '../../../Utils';
+import { Sticky } from './Sticky'; //you should look at sticky and annotation, because they are used here
+import { Annotation } from './Annotation';
+import { ObjectPositionProperty } from 'csstype';
+import { keydownHandler } from 'prosemirror-keymap';
+import { FieldViewProps, FieldView } from './FieldView';
+
+/** ALSO LOOK AT: Annotation.tsx, Sticky.tsx
+ * This method renders PDF and puts all kinds of functionalities such as annotation, highlighting,
+ * area selection (I call it stickies), embedded ink node for directly annotating using a pen or
+ * mouse, and pagination.
+ *
+ *
+ * HOW TO USE:
+ * AREA selection:
+ * 1) Click on Area button.
+ * 2) click on any part of the PDF, and drag to get desired sized area shape
+ * 3) You can write on the area (hence the reason why it's called sticky)
+ * 4) to make another area, you need to click on area button AGAIN.
+ *
+ * HIGHLIGHT: (Buggy. No multiline/multidiv text highlighting for now...)
+ * 1) just click and drag on a text
+ * 2) click highlight
+ * 3) for annotation, just pull your cursor over to that text
+ * 4) another method: click on highlight first and then drag on your desired text
+ * 5) To make another highlight, you need to reclick on the button
+ *
+ * Draw:
+ * 1) click draw and select color. then just draw like there's no tomorrow.
+ * 2) once you finish drawing your masterpiece, just reclick on the draw button to end your drawing session.
+ *
+ * Pagination:
+ * 1) click on arrows. You'll notice that stickies will stay in those page. But... highlights won't.
+ * 2) to test this out, make few area/stickies and then click on next page then come back. You'll see that they are all saved.
+ *
+ *
+ * written by: Andrew Kim
+ */
+@observer
+export class PDFNode extends React.Component<FieldViewProps> {
+ public static LayoutString() { return FieldView.LayoutString(PDFNode); }
+
+ private _mainDiv = React.createRef<HTMLDivElement>()
+ private _pdf = React.createRef<HTMLCanvasElement>();
+
+ //very useful for keeping track of X and y position throughout the PDF Canvas
+ private initX: number = 0;
+ private initY: number = 0;
+
+ //checks if tool is on
+ private _toolOn: boolean = false; //checks if tool is on
+ private _pdfContext: any = null; //gets pdf context
+ private bool: Boolean = false; //general boolean debounce
+ private currSpan: any;//keeps track of current span (for highlighting)
+
+ private _currTool: any; //keeps track of current tool button reference
+ private _drawToolOn: boolean = false; //boolean that keeps track of the drawing tool
+ private _drawTool = React.createRef<HTMLButtonElement>()//drawing tool button reference
+
+ private _colorTool = React.createRef<HTMLButtonElement>(); //color button reference
+ private _currColor: string = "black"; //current color that user selected (for ink/pen)
+
+ private _highlightTool = React.createRef<HTMLButtonElement>(); //highlighter button reference
+ private _highlightToolOn: boolean = false;
+
+ @observable perPage: Object[] = []; //stores pageInfo
+ @observable pageInfo: any = { area: [], divs: [], anno: [] }; //divs is array of objects linked to anno
+
+ @observable private page: number = 1; //default is the first page.
+ @observable private numPage: number = 1; //default number of pages
+ private _pdfCanvas: any;
+
+ /**
+ * for pagination backwards
+ */
+ @action
+ onPageBack = () => {
+ if (this.page > 1) {
+ this.page -= 1;
+ this.currAnno = [];
+ this.perPage[this.page] = this.pageInfo
+ this.pageInfo = { area: [], divs: [], anno: [] }; //resets the object to default
+ if (this.perPage[this.page - 1]) {
+ this.pageInfo = this.perPage[this.page - 1];
+ }
+ }
+ }
+
+ /**
+ * for pagination forwards
+ */
+ @action
+ onPageForward = () => {
+ if (this.page < this.numPage) {
+ this.page += 1;
+ this.currAnno = [];
+ this.perPage[this.page - 2] = this.pageInfo;
+ this.pageInfo = { area: [], divs: [], anno: [] }; //resets the object to default
+ if (this.perPage[this.page - 1]) {
+ this.pageInfo = this.perPage[this.page - 1];
+ }
+ }
+ }
+
+ /**
+ * selection tool used for area highlighting (stickies). Kinda temporary
+ */
+ selectionTool = () => {
+ this._toolOn = true;
+ }
+ /**
+ * when user draws on the canvas. When mouse pointer is down
+ */
+ drawDown = (e: PointerEvent) => {
+ this.initX = e.offsetX;
+ this.initY = e.offsetY;
+ this._pdfContext.beginPath();
+ this._pdfContext.lineTo(this.initX, this.initY);
+ this._pdfContext.strokeStyle = this._currColor;
+ this._pdfCanvas.addEventListener("pointermove", this.drawMove);
+ this._pdfCanvas.addEventListener("pointerup", this.drawUp);
+
+ }
+ //when user drags
+ drawMove = (e: PointerEvent): void => {
+ //x and y mouse movement
+ let x = this.initX += e.movementX,
+ y = this.initY += e.movementY;
+ //connects the point
+ this._pdfContext.lineTo(x, y);
+ this._pdfContext.stroke();
+ }
+
+ drawUp = (e: PointerEvent) => {
+ this._pdfContext.closePath();
+ this._pdfCanvas.removeEventListener("pointermove", this.drawMove);
+ this._pdfCanvas.removeEventListener("pointerdown", this.drawDown);
+ this._pdfCanvas.addEventListener("pointerdown", this.drawDown);
+ }
+
+
+ /**
+ * highlighting helper function
+ */
+ makeEditableAndHighlight = (colour: string) => {
+ var range, sel = window.getSelection();
+ if (sel.rangeCount && sel.getRangeAt) {
+ range = sel.getRangeAt(0);
+ }
+ document.designMode = "on";
+ if (!document.execCommand("HiliteColor", false, colour)) {
+ document.execCommand("HiliteColor", false, colour);
+ }
+
+ if (range) {
+ sel.removeAllRanges();
+ sel.addRange(range);
+
+ let obj: Object = { parentDivs: [], spans: [] };
+ //@ts-ignore
+ if (range.commonAncestorContainer.className == 'react-pdf__Page__textContent') { //multiline highlighting case
+ obj = this.highlightNodes(range.commonAncestorContainer.childNodes)
+ } else { //single line highlighting case
+ let parentDiv = range.commonAncestorContainer.parentElement
+ if (parentDiv) {
+ if (parentDiv.className == 'react-pdf__Page__textContent') { //when highlight is overwritten
+ obj = this.highlightNodes(parentDiv.childNodes)
+ } else {
+ parentDiv.childNodes.forEach((child) => {
+ if (child.nodeName == 'SPAN') {
+ //@ts-ignore
+ obj.parentDivs.push(parentDiv)
+ //@ts-ignore
+ child.id = "highlighted"
+ //@ts-ignore
+ obj.spans.push(child)
+ child.addEventListener("mouseover", this.onEnter); //adds mouseover annotation handler
+ }
+ })
+ }
+ }
+ }
+ this.pageInfo.divs.push(obj);
+
+ }
+ document.designMode = "off";
+ }
+
+ highlightNodes = (nodes: NodeListOf<ChildNode>) => {
+ let temp = { parentDivs: [], spans: [] }
+ nodes.forEach((div) => {
+ div.childNodes.forEach((child) => {
+ if (child.nodeName == 'SPAN') {
+ //@ts-ignore
+ temp.parentDivs.push(div)
+ //@ts-ignore
+ child.id = "highlighted"
+ //@ts-ignore
+ temp.spans.push(child)
+ child.addEventListener("mouseover", this.onEnter); //adds mouseover annotation handler
+ }
+ })
+
+ })
+ return temp;
+ }
+
+ /**
+ * when the cursor enters the highlight, it pops out annotation. ONLY WORKS FOR SINGLE DIV LINES
+ */
+ @observable private currAnno: any = []
+ @action
+ onEnter = (e: any) => {
+ let span: HTMLSpanElement = e.toElement;
+ let index: any;
+ this.pageInfo.divs.forEach((obj: any) => {
+ obj.spans.forEach((element: any) => {
+ if (element == span) {
+ if (!index) {
+ index = this.pageInfo.divs.indexOf(obj);
+ }
+ }
+ })
+ })
+
+ if (this.pageInfo.anno.length >= index + 1) {
+ if (this.currAnno.length == 0) {
+ this.currAnno.push(this.pageInfo.anno[index]);
+ }
+ } else {
+ if (this.currAnno.length == 0) { //if there are no current annotation
+ let div = span.offsetParent;
+ //@ts-ignore
+ let divX = div.style.left
+ //@ts-ignore
+ let divY = div.style.top
+ //slicing "px" from the end
+ divX = divX.slice(0, divX.length - 2); //gets X of the DIV element (parent of Span)
+ divY = divY.slice(0, divY.length - 2); //gets Y of the DIV element (parent of Span)
+ let annotation = <Annotation key={Utils.GenerateGuid()} Span={span} X={divX} Y={divY - 300} Highlights={this.pageInfo.divs} Annotations={this.pageInfo.anno} CurrAnno={this.currAnno} />
+ this.pageInfo.anno.push(annotation);
+ this.currAnno.push(annotation);
+ }
+ }
+
+ }
+
+ /**
+ * highlight function for highlighting actual text. This works fine.
+ */
+ highlight = (color: string) => {
+ if (window.getSelection()) {
+ try {
+ if (!document.execCommand("hiliteColor", false, color)) {
+ this.makeEditableAndHighlight(color);
+ }
+ } catch (ex) {
+ this.makeEditableAndHighlight(color)
+ }
+ }
+ }
+
+ /**
+ * controls the area highlighting (stickies) Kinda temporary
+ */
+ onPointerDown = (e: React.PointerEvent) => {
+ if (this._toolOn) {
+ let mouse = e.nativeEvent;
+ this.initX = mouse.offsetX;
+ this.initY = mouse.offsetY;
+
+ }
+ }
+
+ /**
+ * controls area highlighting and partially highlighting. Kinda temporary
+ */
+ @action
+ onPointerUp = (e: React.PointerEvent) => {
+
+ if (this._highlightToolOn) {
+ this.highlight("rgba(76, 175, 80, 0.3)"); //highlights to this default color.
+ this._highlightToolOn = false;
+ }
+ if (this._toolOn) {
+ let mouse = e.nativeEvent;
+ let finalX = mouse.offsetX;
+ let finalY = mouse.offsetY;
+ let width = Math.abs(finalX - this.initX); //width
+ let height = Math.abs(finalY - this.initY); //height
+
+ //these two if statements are bidirectional dragging. You can drag from any point to another point and generate sticky
+ if (finalX < this.initX) {
+ this.initX = finalX;
+ }
+ if (finalY < this.initY) {
+ this.initY = finalY;
+ }
+
+ if (this._mainDiv.current) {
+ let sticky = <Sticky key={Utils.GenerateGuid()} Height={height} Width={width} X={this.initX} Y={this.initY} />
+ this.pageInfo.area.push(sticky);
+ }
+ this._toolOn = false;
+ }
+
+ }
+
+ /**
+ * starts drawing the line when user presses down.
+ */
+ onDraw = () => {
+ if (this._currTool != null) {
+ this._currTool.style.backgroundColor = "grey";
+ }
+
+ if (this._drawTool.current) {
+ this._currTool = this._drawTool.current;
+ if (this._drawToolOn) {
+ this._drawToolOn = false;
+ this._pdfCanvas.removeEventListener("pointerdown", this.drawDown);
+ this._pdfCanvas.removeEventListener("pointerup", this.drawUp);
+ this._pdfCanvas.removeEventListener("pointermove", this.drawMove);
+ this._drawTool.current.style.backgroundColor = "grey";
+ } else {
+ this._drawToolOn = true;
+ this._pdfCanvas.addEventListener("pointerdown", this.drawDown);
+ this._drawTool.current.style.backgroundColor = "cyan";
+ }
+ }
+ }
+
+
+ /**
+ * for changing color (for ink/pen)
+ */
+ onColorChange = (e: React.PointerEvent) => {
+ if (e.currentTarget.innerHTML == "Red") {
+ this._currColor = "red";
+ } else if (e.currentTarget.innerHTML == "Blue") {
+ this._currColor = "blue";
+ } else if (e.currentTarget.innerHTML == "Green") {
+ this._currColor = "green";
+ } else if (e.currentTarget.innerHTML == "Black") {
+ this._currColor = "black";
+ }
+
+ }
+
+
+ /**
+ * For highlighting (text drag highlighting)
+ */
+ onHighlight = () => {
+ this._drawToolOn = false;
+ if (this._currTool != null) {
+ this._currTool.style.backgroundColor = "grey";
+ }
+ if (this._highlightTool.current) {
+ this._currTool = this._drawTool.current;
+ if (this._highlightToolOn) {
+ this._highlightToolOn = false;
+ this._highlightTool.current.style.backgroundColor = "grey";
+ } else {
+ this._highlightToolOn = true;
+ this._highlightTool.current.style.backgroundColor = "orange";
+ }
+ }
+ }
+
+
+ /**
+ * renders whole lot of shets, including pdf, stickies, and annotations.
+ */
+
+ reHighlight = () => {
+ let div = document.getElementsByClassName("react-pdf__Page__textContent");
+ if (div) {
+
+ }
+
+ }
+
+
+ render() {
+ return (
+ <div ref={this._mainDiv}
+ onPointerDown={this.onPointerDown}
+ onPointerUp={this.onPointerUp}
+ >
+
+ {this.pageInfo.area.filter(() => {
+ return this.pageInfo.area
+ }).map((element: any) => {
+ return element
+ })
+ }
+ {this.currAnno.map((element: any) => {
+ return element
+ })}
+
+ <button onClick={this.onPageBack}>{"<"}</button>
+ <button onClick={this.onPageForward}>{">"}</button>
+ <button onClick={this.selectionTool}>{"Area"}</button>
+ <button style={{ color: "white", backgroundColor: "grey" }} onClick={this.onHighlight} ref={this._highlightTool}>Highlight</button>
+ <button style={{ color: "white", backgroundColor: "grey" }} ref={this._drawTool} onClick={this.onDraw}>{"Draw"}</button>
+ <button ref={this._colorTool} onPointerDown={this.onColorChange}>{"Red"}</button>
+ <button ref={this._colorTool} onPointerDown={this.onColorChange}>{"Blue"}</button>
+ <button ref={this._colorTool} onPointerDown={this.onColorChange}>{"Green"}</button>
+ <button ref={this._colorTool} onPointerDown={this.onColorChange}>{"Black"}</button>
+
+ <Document file={"https://www.adobe.com/support/products/enterprise/knowledgecenter/media/c4611_sample_explain.pdf"}>
+ <Page
+ pageNumber={this.page}
+ onLoadSuccess={
+ (page: any) => {
+ if (this._mainDiv.current) {
+ this._mainDiv.current.childNodes.forEach((element) => {
+ if (element.nodeName == "DIV") {
+ element.childNodes[0].childNodes.forEach((e) => {
+
+ if (e.nodeName == "CANVAS") {
+ this._pdfCanvas = e;
+ //@ts-ignore
+ this._pdfContext = e.getContext("2d")
+
+ }
+
+ })
+ }
+ })
+ }
+ this.numPage = page.transport.numPages
+ if (this.perPage.length == 0) { //Makes sure it only runs once
+ this.perPage = [...Array(this.numPage)]
+ }
+ }
+ }
+ />
+ </Document>
+ </div>
+ );
+ }
+
+} \ No newline at end of file