aboutsummaryrefslogtreecommitdiff
path: root/src/client/views/pdf
diff options
context:
space:
mode:
Diffstat (limited to 'src/client/views/pdf')
-rw-r--r--src/client/views/pdf/PDFAnnotationLayer.tsx24
-rw-r--r--src/client/views/pdf/PDFMenu.scss25
-rw-r--r--src/client/views/pdf/PDFMenu.tsx206
-rw-r--r--src/client/views/pdf/PDFViewer.scss44
-rw-r--r--src/client/views/pdf/PDFViewer.tsx586
-rw-r--r--src/client/views/pdf/Page.tsx392
6 files changed, 1277 insertions, 0 deletions
diff --git a/src/client/views/pdf/PDFAnnotationLayer.tsx b/src/client/views/pdf/PDFAnnotationLayer.tsx
new file mode 100644
index 000000000..1f49e0d2f
--- /dev/null
+++ b/src/client/views/pdf/PDFAnnotationLayer.tsx
@@ -0,0 +1,24 @@
+import React = require("react");
+import { observer } from "mobx-react";
+
+interface IAnnotationProps {
+
+}
+
+@observer
+export class PDFAnnotationLayer extends React.Component {
+ onPointerDown = (e: React.PointerEvent) => {
+ if (e.ctrlKey) {
+ console.log("annotating");
+ e.stopPropagation();
+ }
+ }
+
+ render() {
+ return (
+ <div className="pdfAnnotationLayer-cont" style={{ width: "100%", height: "100%", position: "relative", top: "-200%" }} onPointerDown={this.onPointerDown}>
+
+ </div>
+ );
+ }
+} \ No newline at end of file
diff --git a/src/client/views/pdf/PDFMenu.scss b/src/client/views/pdf/PDFMenu.scss
new file mode 100644
index 000000000..22868082a
--- /dev/null
+++ b/src/client/views/pdf/PDFMenu.scss
@@ -0,0 +1,25 @@
+.pdfMenu-cont {
+ position: absolute;
+ z-index: 10000;
+ height: 35px;
+ background: #323232;
+ box-shadow: 3px 3px 3px rgba(0, 0, 0, 0.25);
+ border-radius: 0px 6px 6px 6px;
+ overflow: hidden;
+ display: flex;
+
+ .pdfMenu-button {
+ background-color: transparent;
+ width: 35px;
+ height: 35px;
+ }
+
+ .pdfMenu-button:hover {
+ background-color: #121212;
+ }
+
+ .pdfMenu-dragger {
+ height: 100%;
+ transition: width .2s;
+ }
+} \ No newline at end of file
diff --git a/src/client/views/pdf/PDFMenu.tsx b/src/client/views/pdf/PDFMenu.tsx
new file mode 100644
index 000000000..39b15fb11
--- /dev/null
+++ b/src/client/views/pdf/PDFMenu.tsx
@@ -0,0 +1,206 @@
+import React = require("react");
+import "./PDFMenu.scss";
+import { observable, action } from "mobx";
+import { observer } from "mobx-react";
+import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
+import { emptyFunction } from "../../../Utils";
+import { Doc } from "../../../new_fields/Doc";
+
+@observer
+export default class PDFMenu extends React.Component {
+ static Instance: PDFMenu;
+
+ @observable private _top: number = -300;
+ @observable private _left: number = -300;
+ @observable private _opacity: number = 1;
+ @observable private _transition: string = "opacity 0.5s";
+ @observable private _transitionDelay: string = "";
+
+ @observable public Pinned: boolean = false;
+
+ StartDrag: (e: PointerEvent) => void = emptyFunction;
+ Highlight: (d: Doc | undefined, color: string | undefined) => void = emptyFunction;
+ Delete: () => void = emptyFunction;
+
+ @observable public Highlighting: boolean = false;
+ @observable public Status: "pdf" | "annotation" | "" = "";
+
+ private _offsetY: number = 0;
+ private _offsetX: number = 0;
+ private _mainCont: React.RefObject<HTMLDivElement>;
+ private _dragging: boolean = false;
+
+ constructor(props: Readonly<{}>) {
+ super(props);
+
+ PDFMenu.Instance = this;
+
+ this._mainCont = React.createRef();
+ }
+
+ pointerDown = (e: React.PointerEvent) => {
+ document.removeEventListener("pointermove", this.pointerMove);
+ document.addEventListener("pointermove", this.pointerMove);
+ document.removeEventListener("pointerup", this.pointerUp);
+ document.addEventListener("pointerup", this.pointerUp);
+
+ e.stopPropagation();
+ e.preventDefault();
+ }
+
+ pointerMove = (e: PointerEvent) => {
+ e.stopPropagation();
+ e.preventDefault();
+
+ if (this._dragging) {
+ return;
+ }
+
+ this.StartDrag(e);
+ this._dragging = true;
+ }
+
+ pointerUp = (e: PointerEvent) => {
+ this._dragging = false;
+ document.removeEventListener("pointermove", this.pointerMove);
+ document.removeEventListener("pointerup", this.pointerUp);
+ e.stopPropagation();
+ e.preventDefault();
+ }
+
+ @action
+ jumpTo = (x: number, y: number, forceJump: boolean = false) => {
+ if (!this.Pinned || forceJump) {
+ this._transition = this._transitionDelay = "";
+ this._opacity = 1;
+ this._left = x;
+ this._top = y;
+ }
+ }
+
+ @action
+ fadeOut = (forceOut: boolean) => {
+ if (!this.Pinned) {
+ if (this._opacity === 0.2) {
+ this._transition = "opacity 0.1s";
+ this._transitionDelay = "";
+ this._opacity = 0;
+ this._left = this._top = -300;
+ }
+
+ if (forceOut) {
+ this._transition = "";
+ this._transitionDelay = "";
+ this._opacity = 0;
+ this._left = this._top = -300;
+ }
+ }
+ }
+
+ @action
+ pointerLeave = (e: React.PointerEvent) => {
+ if (!this.Pinned) {
+ this._transition = "opacity 0.5s";
+ this._transitionDelay = "1s";
+ this._opacity = 0.2;
+ setTimeout(() => this.fadeOut(false), 3000);
+ }
+ }
+
+ @action
+ pointerEntered = (e: React.PointerEvent) => {
+ this._transition = "opacity 0.1s";
+ this._transitionDelay = "";
+ this._opacity = 1;
+ }
+
+ @action
+ togglePin = (e: React.MouseEvent) => {
+ this.Pinned = !this.Pinned;
+ if (!this.Pinned) {
+ this.Highlighting = false;
+ }
+ }
+
+ @action
+ dragging = (e: PointerEvent) => {
+ this._left = e.pageX - this._offsetX;
+ this._top = e.pageY - this._offsetY;
+
+ e.stopPropagation();
+ e.preventDefault();
+ }
+
+ dragEnd = (e: PointerEvent) => {
+ document.removeEventListener("pointermove", this.dragging);
+ document.removeEventListener("pointerup", this.dragEnd);
+ e.stopPropagation();
+ e.preventDefault();
+ }
+
+ dragStart = (e: React.PointerEvent) => {
+ document.removeEventListener("pointermove", this.dragging);
+ document.addEventListener("pointermove", this.dragging);
+ document.removeEventListener("pointerup", this.dragEnd);
+ document.addEventListener("pointerup", this.dragEnd);
+
+ this._offsetX = this._mainCont.current!.getBoundingClientRect().width - e.nativeEvent.offsetX;
+ this._offsetY = e.nativeEvent.offsetY;
+
+ e.stopPropagation();
+ e.preventDefault();
+ }
+
+ @action
+ highlightClicked = (e: React.MouseEvent) => {
+ if (!this.Pinned) {
+ this.Highlight(undefined, "#f4f442");
+ }
+ else {
+ this.Highlighting = !this.Highlighting;
+ this.Highlight(undefined, "#f4f442");
+ }
+ }
+
+ deleteClicked = (e: React.PointerEvent) => {
+ this.Delete();
+ }
+
+ handleContextMenu = (e: React.MouseEvent) => {
+ e.stopPropagation();
+ e.preventDefault();
+ }
+
+ render() {
+ let buttons = this.Status === "pdf" ? [
+ <button 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}
+ 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>
+ ];
+
+ 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 >
+ );
+ }
+} \ No newline at end of file
diff --git a/src/client/views/pdf/PDFViewer.scss b/src/client/views/pdf/PDFViewer.scss
new file mode 100644
index 000000000..53c33ce0b
--- /dev/null
+++ b/src/client/views/pdf/PDFViewer.scss
@@ -0,0 +1,44 @@
+.textLayer {
+ div {
+ user-select: text;
+ }
+}
+
+.viewer-button-cont {
+ position: absolute;
+ display: flex;
+ justify-content: space-evenly;
+ align-items: center;
+}
+
+.viewer-previousPage,
+.viewer-nextPage {
+ background: grey;
+ font-weight: bold;
+ opacity: 0.5;
+ padding: 0 10px;
+ border-radius: 5px;
+}
+
+.textLayer {
+ user-select: auto;
+}
+
+.pdfViewer-annotationBox {
+ position: absolute;
+ background-color: red;
+ opacity: 0.1;
+}
+
+.pdfViewer-annotationLayer {
+ position: absolute;
+ top: 0;
+}
+
+
+
+.pdfViewer-pinAnnotation {
+ background-color: red;
+ position: absolute;
+ border-radius: 100%;
+} \ No newline at end of file
diff --git a/src/client/views/pdf/PDFViewer.tsx b/src/client/views/pdf/PDFViewer.tsx
new file mode 100644
index 000000000..7000352e7
--- /dev/null
+++ b/src/client/views/pdf/PDFViewer.tsx
@@ -0,0 +1,586 @@
+import { action, computed, IReactionDisposer, observable, reaction, runInAction } from "mobx";
+import { observer } from "mobx-react";
+import * as Pdfjs from "pdfjs-dist";
+import "pdfjs-dist/web/pdf_viewer.css";
+import * as rp from "request-promise";
+import { Dictionary } from "typescript-collections";
+import { Doc, DocListCast, HeightSym, Opt, WidthSym } 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 { 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";
+
+export const scale = 2;
+interface IPDFViewerProps {
+ url: string;
+ loaded: (nw: number, nh: number, np: number) => void;
+ scrollY: number;
+ parent: PDFBox;
+}
+
+/**
+ * Wrapper that loads the PDF and cascades the pdf down
+ */
+@observer
+export class PDFViewer extends React.Component<IPDFViewerProps> {
+ @observable _pdf: Opt<Pdfjs.PDFDocumentProxy>;
+ private _mainDiv = React.createRef<HTMLDivElement>();
+
+ @action
+ componentDidMount() {
+ Pdfjs.getDocument(this.props.url).promise.then(pdf => runInAction(() => this._pdf = pdf));
+ }
+
+ render() {
+ return (
+ <div 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>
+ );
+ }
+}
+
+interface IViewerProps {
+ pdf: Pdfjs.PDFDocumentProxy;
+ loaded: (nw: number, nh: number, np: number) => void;
+ scrollY: number;
+ parent: PDFBox;
+ mainCont: React.RefObject<HTMLDivElement>;
+ url: string;
+}
+
+const PinRadius = 25;
+
+/**
+ * Handles rendering and virtualization of the pdf
+ */
+@observer
+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
+ @observable private _isPage: string[] = [];
+ @observable private _pageSizes: { width: number, height: number }[] = [];
+ @observable private _annotations: Doc[] = [];
+ @observable private _savedAnnotations: Dictionary<number, HTMLDivElement[]> = new Dictionary<number, HTMLDivElement[]>();
+
+ private _pageBuffer: number = 1;
+ private _annotationLayer: React.RefObject<HTMLDivElement> = React.createRef();
+ private _reactionDisposer?: IReactionDisposer;
+ private _annotationReactionDisposer?: IReactionDisposer;
+ private _dropDisposer?: DragManager.DragDropDisposer;
+
+ componentDidUpdate = (prevProps: IViewerProps) => {
+ if (this.scrollY !== prevProps.scrollY) {
+ this.renderPages();
+ }
+ }
+
+ @action
+ componentDidMount = () => {
+ this._reactionDisposer = reaction(
+
+ () => [this.props.parent.props.active(), this.startIndex, this._pageSizes.length ? this.endIndex : 0],
+ async () => {
+ await this.initialLoad();
+ this.renderPages();
+ }, { fireImmediately: true });
+
+ this._annotationReactionDisposer = reaction(
+ () => this.props.parent.Document && DocListCast(this.props.parent.Document.annotations),
+ (annotations: Doc[]) =>
+ annotations && annotations.length && this.renderAnnotations(annotations, true),
+ { fireImmediately: true });
+ }
+
+ componentWillUnmount = () => {
+ this._reactionDisposer && this._reactionDisposer();
+ this._annotationReactionDisposer && this._annotationReactionDisposer();
+ }
+
+ @action
+ initialLoad = async () => {
+ if (this._pageSizes.length === 0) {
+ let pageSizes = Array<{ width: number, height: number }>(this.props.pdf.numPages);
+ this._isPage = Array<string>(this.props.pdf.numPages);
+ 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 };
+ }));
+ }
+ 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) } });
+ }
+ }
+
+ makeAnnotationDocument = (sourceDoc: Doc | undefined, s: number, color: string): Doc => {
+ let annoDocs: Doc[] = [];
+ let mainAnnoDoc = new Doc();
+ 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.height) annoDoc.height = parseInt(anno.style.height) / scale;
+ if (anno.style.width) annoDoc.width = parseInt(anno.style.width) / scale;
+ annoDoc.page = key;
+ annoDoc.target = sourceDoc;
+ annoDoc.group = mainAnnoDoc;
+ annoDoc.color = color;
+ annoDoc.type = AnnotationTypes.Region;
+ annoDocs.push(annoDoc);
+ anno.remove();
+ }
+ });
+
+ 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();
+ return mainAnnoDoc;
+ }
+
+ drop = async (e: Event, de: DragManager.DropEvent) => {
+ 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);
+ if (targetAnnotations) {
+ targetAnnotations.push(destDoc);
+ this.props.parent.Document.annotations = new List<Doc>(targetAnnotations);
+ }
+ else {
+ this.props.parent.Document.annotations = new List<Doc>([destDoc]);
+ }
+ e.stopPropagation();
+ }
+ }
+ /**
+ * Called by the Page class when it gets rendered, initializes the lists and
+ * puts a placeholder with all of the correct page sizes when all of the pages have been loaded.
+ */
+ @action
+ 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") {
+ this._isPage[page] = "none";
+ this._visibleElements[page] = (
+ <div key={`${this.props.url}-placeholder-${page + 1}`} className="pdfviewer-placeholder"
+ style={{ width: this._pageSizes[page].width, height: this._pageSizes[page].height }} />
+ );
+ }
+ }
+ @action
+ getRenderedPage = (page: number) => {
+ if (this._isPage[page] !== "page") {
+ this._isPage[page] = "page";
+ this._visibleElements[page] = (
+ <Page
+ pdf={this.props.pdf}
+ page={page}
+ numPages={this.props.pdf.numPages}
+ key={`${this.props.url}-rendered-${page + 1}`}
+ name={`${this.props.pdf.fingerprint + `-page${page + 1}`}`}
+ pageLoaded={this.pageLoaded}
+ parent={this.props.parent}
+ makePin={emptyFunction}
+ renderAnnotations={this.renderAnnotations}
+ createAnnotation={this.createAnnotation}
+ sendAnnotations={this.receiveAnnotations}
+ makeAnnotationDocuments={this.makeAnnotationDocument}
+ receiveAnnotations={this.sendAnnotations}
+ {...this.props} />
+ );
+ }
+ }
+
+ // change the address to be the file address of the PNG version of each page
+ // file address of the pdf
+ @action
+ getPageImage = async (page: number) => {
+ let handleError = () => this.getRenderedPage(page);
+ 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` }} />);
+ }
+ }
+
+ @computed get scrollY(): number { return this.props.scrollY; }
+
+ // startIndex: where to start rendering pages
+ @computed get startIndex(): number { return Math.max(0, this.getPageFromScroll(this.scrollY) - this._pageBuffer); }
+
+ // endIndex: where to end rendering pages
+ @computed get endIndex(): number {
+ return Math.min(this.props.pdf.numPages - 1, this.getPageFromScroll(this.scrollY + this._pageSizes[0].height) + this._pageBuffer);
+ }
+
+ @action
+ renderPages = () => {
+ for (let i = 0; i < this.props.pdf.numPages; i++) {
+ if (i < this.startIndex || i > this.endIndex) {
+ this.getPlaceholderPage(i); // pages outside of the pdf use empty stand-in divs
+ } else {
+ if (this.props.parent.props.active()) {
+ this.getRenderedPage(i);
+ } else {
+ this.getPageImage(i);
+ }
+ }
+ }
+ }
+
+ @action
+ private renderAnnotations = (annotations: Doc[], removeOldAnnotations: boolean): void => {
+ if (removeOldAnnotations) {
+ this._annotations = annotations;
+ }
+ else {
+ this._annotations.push(...annotations);
+ this._annotations = new Array<Doc>(...this._annotations);
+ }
+ }
+
+ @action
+ receiveAnnotations = (annotations: HTMLDivElement[], page: number) => {
+ if (page === -1) {
+ this._savedAnnotations.values().forEach(v => v.forEach(a => a.remove()));
+ this._savedAnnotations.keys().forEach(k => this._savedAnnotations.setValue(k, annotations));
+ }
+ else {
+ this._savedAnnotations.setValue(page, annotations);
+ }
+ }
+
+ sendAnnotations = (page: number): HTMLDivElement[] | undefined => {
+ 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;
+ let currOffset = vOffset;
+ while (index < this._pageSizes.length && currOffset - this._pageSizes[index].height > 0) {
+ currOffset -= this._pageSizes[index++].height;
+ }
+ return index;
+ }
+
+ getScrollFromPage = (index: number): number => {
+ let counter = 0;
+ for (let i = 0; i < Math.min(this.props.pdf.numPages, index); i++) {
+ counter += this._pageSizes[i].height;
+ }
+ return counter;
+ }
+
+ createAnnotation = (div: HTMLDivElement, page: number) => {
+ if (this._annotationLayer.current) {
+ if (div.style.top) {
+ div.style.top = (parseInt(div.style.top) + this.getScrollFromPage(page)).toString();
+ }
+ this._annotationLayer.current.append(div);
+ let savedPage = this._savedAnnotations.getValue(page);
+ if (savedPage) {
+ savedPage.push(div);
+ this._savedAnnotations.setValue(page, savedPage);
+ }
+ else {
+ this._savedAnnotations.setValue(page, [div]);
+ }
+ }
+ }
+
+ 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>;
+ }
+ });
+ return res;
+ }
+
+ render() {
+ return (
+ <div ref={this.mainCont} style={{ pointerEvents: "all" }}>
+ <div className="viewer">
+ {this._visibleElements}
+ </div>
+ <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))}
+ </div>
+ </div>
+ </div >
+ );
+ }
+}
+
+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>;
+
+ constructor(props: IAnnotationProps) {
+ 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 }
+ );
+ }
+
+ componentWillUnmount() {
+ this._reactionDisposer && this._reactionDisposer();
+ }
+
+ 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);
+ }
+
+ if (group) {
+ let groupAnnotations = DocListCast(group.annotations);
+ groupAnnotations.forEach(anno => anno.delete = true);
+ }
+
+ PDFMenu.Instance.fadeOut(true);
+ }
+
+
+ // 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);
+ }
+ }
+
+ 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>
+ );
+ }
+} \ No newline at end of file
diff --git a/src/client/views/pdf/Page.tsx b/src/client/views/pdf/Page.tsx
new file mode 100644
index 000000000..734dff7fc
--- /dev/null
+++ b/src/client/views/pdf/Page.tsx
@@ -0,0 +1,392 @@
+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 "./PDFViewer.scss";
+import "pdfjs-dist/web/pdf_viewer.css";
+import { PDFBox } from "../nodes/PDFBox";
+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 { 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";
+
+
+interface IPageProps {
+ pdf: Opt<Pdfjs.PDFDocumentProxy>;
+ name: string;
+ numPages: number;
+ page: number;
+ pageLoaded: (index: number, page: Pdfjs.PDFPageViewport) => void;
+ parent: PDFBox;
+ 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;
+}
+
+@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 = "";
+
+ 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 _marqueeing: boolean = false;
+ private _reactionDisposer?: IReactionDisposer;
+
+ 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 = (): void => {
+ if (this.props.pdf) {
+ this.update(this.props.pdf);
+ }
+ }
+
+ componentWillUnmount = (): void => {
+ if (this._reactionDisposer) {
+ this._reactionDisposer();
+ }
+ }
+
+ componentDidUpdate = (): void => {
+ if (this.props.pdf) {
+ this.update(this.props.pdf);
+ }
+ }
+
+ private update = (pdf: Pdfjs.PDFDocumentProxy): void => {
+ if (pdf) {
+ this.loadPage(pdf);
+ }
+ else {
+ this._state = "loading";
+ }
+ }
+
+ private loadPage = (pdf: Pdfjs.PDFDocumentProxy): void => {
+ if (this._state === "rendering" || this._page) return;
+
+ pdf.getPage(this._currPage).then(
+ (page: Pdfjs.PDFPageProxy): void => {
+ this._state = "rendering";
+ this.renderPage(page);
+ }
+ );
+ }
+
+ @action
+ private renderPage = (page: Pdfjs.PDFPageProxy): void => {
+ // lower scale = easier to read at small sizes, higher scale = easier to read at large sizes
+ 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): void => {
+ //@ts-ignore
+ Pdfjs.renderTextLayer({
+ textContent: res,
+ container: textLayer,
+ viewport: viewport
+ });
+ });
+
+ this._page = page;
+ }
+ }
+ }
+
+ @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));
+ if (targetAnnotations === undefined) {
+ Doc.GetProto(this.props.parent.Document).annotations = new List([annotationDoc]);
+ } else {
+ targetAnnotations.push(annotationDoc);
+ }
+ return annotationDoc;
+ }
+
+ /**
+ * 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): 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" });
+ targetDoc.targetPage = this.props.page;
+ let annotationDoc = this.highlight(targetDoc, "red");
+ // 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, {
+ handlers: {
+ dragComplete: emptyFunction,
+ },
+ hideSource: false
+ });
+ }
+ }
+
+ // cleans up events and boolean
+ endDrag = (e: PointerEvent): void => {
+ // document.removeEventListener("pointermove", this.startDrag);
+ // document.removeEventListener("pointerup", this.endDrag);
+ e.stopPropagation();
+ }
+
+ @action
+ onPointerDown = (e: React.PointerEvent): void => {
+ // 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) {
+ PDFMenu.Instance.StartDrag = this.startDrag;
+ PDFMenu.Instance.Highlight = this.highlight;
+ PDFMenu.Instance.Status = "pdf";
+ PDFMenu.Instance.fadeOut(true);
+ let target: any = e.target;
+ if (target && target.parentElement === this._textLayer.current) {
+ e.stopPropagation();
+ }
+ else {
+ // 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.onSelectStart);
+ document.addEventListener("pointermove", this.onSelectStart);
+ document.removeEventListener("pointerup", this.onSelectEnd);
+ document.addEventListener("pointerup", this.onSelectEnd);
+ if (!e.ctrlKey) {
+ this.props.sendAnnotations([], -1);
+ }
+ }
+ }
+
+ @action
+ onSelectStart = (e: PointerEvent): void => {
+ let target: any = e.target;
+ if (this._marqueeing) {
+ let current = this._textLayer.current;
+ 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;
+ let { background, opacity, transform: transform } = this.getCurlyTransform();
+ if (this._marquee.current && this._curly.current) {
+ this._marquee.current.style.background = background;
+ this._curly.current.style.opacity = opacity;
+ this._rotate = transform;
+ }
+ }
+ e.stopPropagation();
+ e.preventDefault();
+ }
+ else if (target && target.parentElement === this._textLayer.current) {
+ e.stopPropagation();
+ }
+ }
+
+ 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";
+ }
+
+ // 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 };
+ }
+
+ @action
+ onSelectEnd = (e: PointerEvent): void => {
+ if (this._marqueeing) {
+ this._marqueeing = false;
+ 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;
+
+ // 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;
+ }
+ 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) {
+ PDFMenu.Instance.jumpTo(e.clientX, e.clientY);
+ }
+
+ this._marqueeHeight = this._marqueeWidth = 0;
+ }
+ else {
+ let sel = window.getSelection();
+ if (sel && sel.type === "Range") {
+ this.createTextAnnotation(sel);
+ PDFMenu.Instance.jumpTo(e.clientX, e.clientY);
+ }
+ }
+
+
+ if (PDFMenu.Instance.Highlighting) {
+ this.highlight(undefined, "#f4f442");
+ }
+ else {
+ PDFMenu.Instance.StartDrag = this.startDrag;
+ PDFMenu.Instance.Highlight = this.highlight;
+ }
+ document.removeEventListener("pointermove", this.onSelectStart);
+ document.removeEventListener("pointerup", this.onSelectEnd);
+ }
+
+ @action
+ createTextAnnotation = (sel: Selection) => {
+ 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 && rect.width !== this._textLayer.current.getBoundingClientRect().width && rect.height !== this._textLayer.current.getBoundingClientRect().height) {
+ 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();
+ this.props.createAnnotation(annoBox, this.props.page);
+ }
+ }
+ }
+ // clear selection
+ if (sel.empty) { // Chrome
+ sel.empty();
+ } else if (sel.removeAllRanges) { // Firefox
+ sel.removeAllRanges();
+ }
+ }
+
+ doubleClick = (e: React.MouseEvent) => {
+ let target: any = e.target;
+ // if double clicking text
+ if (target && target.parentElement === this._textLayer.current) {
+ // do something to select the paragraph ideally
+ }
+
+ let current = this._textLayer.current;
+ if (current) {
+ let boundingRect = current.getBoundingClientRect();
+ let x = (e.clientX - boundingRect.left) * (current.offsetWidth / boundingRect.width);
+ let y = (e.clientY - boundingRect.top) * (current.offsetHeight / boundingRect.height);
+ this.props.makePin(x, y, this.props.page);
+ }
+ }
+
+ render() {
+ return (
+ <div onPointerDown={this.onPointerDown} onDoubleClick={this.doubleClick} className={"page-cont"} style={{ "width": this._width, "height": this._height }}>
+ <div className="canvasContainer">
+ <canvas ref={this._canvas} />
+ </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}` }} />
+ </div>
+ </div>
+ <div className="textlayer" ref={this._textLayer} style={{ "position": "relative", "top": `-${2 * this._height}px`, "height": `${this._height}px` }} />
+ </div>
+ );
+ }
+}