aboutsummaryrefslogtreecommitdiff
path: root/src/client/views
diff options
context:
space:
mode:
Diffstat (limited to 'src/client/views')
-rw-r--r--src/client/views/nodes/Annotation.tsx117
-rw-r--r--src/client/views/nodes/PDFNode.tsx453
-rw-r--r--src/client/views/nodes/Sticky.tsx83
3 files changed, 653 insertions, 0 deletions
diff --git a/src/client/views/nodes/Annotation.tsx b/src/client/views/nodes/Annotation.tsx
new file mode 100644
index 000000000..a2c7be1a8
--- /dev/null
+++ b/src/client/views/nodes/Annotation.tsx
@@ -0,0 +1,117 @@
+import "./ImageBox.scss";
+import React = require("react")
+import { observer } from "mobx-react"
+import { observable, action } from 'mobx';
+import 'react-pdf/dist/Page/AnnotationLayer.css'
+
+interface IProps{
+ Span: HTMLSpanElement;
+ X: number;
+ Y: number;
+ Highlights: any[];
+ Annotations: any[];
+ CurrAnno: any[];
+
+}
+
+/**
+ * Annotation class is used to take notes on a particular highlight. You can also change highlighted span's color
+ * Improvements to be made: Removing the annotation when onRemove is called. (Removing this, not just the highlighted span).
+ * Also need to support multiline highlighting
+ *
+ * Written by: Andrew Kim
+ */
+@observer
+export class Annotation extends React.Component<IProps> {
+
+ /**
+ * changes color of the span (highlighted section)
+ */
+ onColorChange = (e:React.PointerEvent) => {
+ if (e.currentTarget.innerHTML == "r"){
+ this.props.Span.style.backgroundColor = "rgba(255,0,0, 0.3)"
+ } else if (e.currentTarget.innerHTML == "b"){
+ this.props.Span.style.backgroundColor = "rgba(0,255, 255, 0.3)"
+ } else if (e.currentTarget.innerHTML == "y"){
+ this.props.Span.style.backgroundColor = "rgba(255,255,0, 0.3)"
+ } else if (e.currentTarget.innerHTML == "g"){
+ this.props.Span.style.backgroundColor = "rgba(76, 175, 80, 0.3)"
+ }
+
+ }
+
+ /**
+ * removes the highlighted span. Supposed to remove Annotation too, but I don't know how to unmount this
+ */
+ @action
+ onRemove = (e:any) => {
+ let index:number = -1;
+ //finding the highlight in the highlight array
+ this.props.Highlights.forEach((e) => {
+ for (let i = 0; i < e.spans.length; i++){
+ if (e.spans[i] == this.props.Span){
+ index = this.props.Highlights.indexOf(e);
+ this.props.Highlights.splice(index, 1);
+ }
+ }
+ })
+
+ //removing from CurrAnno and Annotation array
+ this.props.Annotations.splice(index, 1);
+ this.props.CurrAnno.pop()
+
+ //removing span from div
+ if(this.props.Span.parentElement){
+ let nodesArray = this.props.Span.parentElement.childNodes;
+ nodesArray.forEach((e) => {
+ if (e == this.props.Span){
+ if (this.props.Span.parentElement){
+ this.props.Highlights.forEach((item) => {
+ if (item == e){
+ item.remove();
+ }
+ })
+ e.remove();
+ }
+ }
+ })
+ }
+
+
+ }
+
+ render() {
+ return (
+ <div
+ style = {{
+ position: "absolute",
+ top: "20px",
+ left: "0px",
+ zIndex: 1,
+ transform: `translate(${this.props.X}px, ${this.props.Y}px)`,
+
+ }}>
+ <div style = {{width:"200px", height:"50px", backgroundColor: "orange"}}>
+ <button
+ style = {{borderRadius: "25px", width:"25%", height:"100%"}}
+ onClick = {this.onRemove}
+ >x</button>
+ <div style = {{width:"75%", height: "100%" , display:"inline-block"}}>
+ <button onPointerDown = {this.onColorChange} style = {{backgroundColor:"red", borderRadius:"50%", color: "transparent"}}>r</button>
+ <button onPointerDown = {this.onColorChange} style = {{backgroundColor:"blue", borderRadius:"50%", color: "transparent"}}>b</button>
+ <button onPointerDown = {this.onColorChange} style = {{backgroundColor:"yellow", borderRadius:"50%", color:"transparent"}}>y</button>
+ <button onPointerDown = {this.onColorChange} style = {{backgroundColor:"green", borderRadius:"50%", color:"transparent"}}>g</button>
+ </div>
+
+ </div>
+ <div style = {{width:"200px", height:"200"}}>
+ <textarea style = {{width: "100%", height: "100%"}}
+ defaultValue = "Enter Text Here..."
+
+ ></textarea>
+ </div>
+ </div>
+
+ );
+ }
+} \ No newline at end of file
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
diff --git a/src/client/views/nodes/Sticky.tsx b/src/client/views/nodes/Sticky.tsx
new file mode 100644
index 000000000..d57dd5c0b
--- /dev/null
+++ b/src/client/views/nodes/Sticky.tsx
@@ -0,0 +1,83 @@
+import 'react-image-lightbox/style.css'; // This only needs to be imported once in your app
+import React = require("react")
+import { observer } from "mobx-react"
+import 'react-pdf/dist/Page/AnnotationLayer.css'
+
+interface IProps {
+ Height: number;
+ Width: number;
+ X: number;
+ Y: number;
+}
+
+/**
+ * Sticky, also known as area highlighting, is used to highlight large selection of the PDF file.
+ * Improvements that could be made: maybe store line array and store that somewhere for future rerendering.
+ *
+ * Written By: Andrew Kim
+ */
+@observer
+export class Sticky extends React.Component<IProps> {
+
+ private initX: number = 0;
+ private initY: number = 0;
+
+ private _ref = React.createRef<HTMLCanvasElement>();
+ private ctx: any; //context that keeps track of sticky canvas
+
+ /**
+ * drawing. Registers the first point that user clicks when mouse button is pressed down on canvas
+ */
+ drawDown = (e: React.PointerEvent) => {
+ if (this._ref.current) {
+ this.ctx = this._ref.current.getContext("2d");
+ let mouse = e.nativeEvent;
+ this.initX = mouse.offsetX;
+ this.initY = mouse.offsetY;
+ this.ctx.beginPath();
+ this.ctx.lineTo(this.initX, this.initY);
+ this.ctx.strokeStyle = "black";
+ document.addEventListener("pointermove", this.drawMove);
+ document.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.ctx.lineTo(x, y);
+ this.ctx.stroke();
+
+ }
+
+ /**
+ * when user lifts the mouse, the drawing ends
+ */
+ drawUp = (e: PointerEvent) => {
+ this.ctx.closePath();
+ console.log(this.ctx);
+ document.removeEventListener("pointermove", this.drawMove);
+ }
+
+ render() {
+ return (
+ <div onPointerDown={this.drawDown}>
+ <canvas ref={this._ref} height={this.props.Height} width={this.props.Width}
+ style={{
+ position: "absolute",
+ top: "20px",
+ left: "0px",
+ zIndex: 1,
+ background: "yellow",
+ transform: `translate(${this.props.X}px, ${this.props.Y}px)`,
+ opacity: 0.4
+ }}
+ />
+
+ </div>
+ );
+ }
+} \ No newline at end of file