diff options
Diffstat (limited to 'src/client/views/pdf')
| -rw-r--r-- | src/client/views/pdf/Annotation.tsx | 153 | ||||
| -rw-r--r-- | src/client/views/pdf/PDFMenu.scss | 4 | ||||
| -rw-r--r-- | src/client/views/pdf/PDFMenu.tsx | 35 | ||||
| -rw-r--r-- | src/client/views/pdf/PDFViewer.scss | 3 | ||||
| -rw-r--r-- | src/client/views/pdf/PDFViewer.tsx | 232 | ||||
| -rw-r--r-- | src/client/views/pdf/Page.tsx | 138 |
6 files changed, 328 insertions, 237 deletions
diff --git a/src/client/views/pdf/Annotation.tsx b/src/client/views/pdf/Annotation.tsx new file mode 100644 index 000000000..104241237 --- /dev/null +++ b/src/client/views/pdf/Annotation.tsx @@ -0,0 +1,153 @@ +import React = require("react"); +import { Doc, DocListCast, WidthSym, HeightSym } from "../../../new_fields/Doc"; +import { AnnotationTypes, Viewer, scale } from "./PDFViewer"; +import { observer } from "mobx-react"; +import { observable, IReactionDisposer, reaction, action } from "mobx"; +import { BoolCast, NumCast, FieldValue, Cast, StrCast } from "../../../new_fields/Types"; +import { Id } from "../../../new_fields/FieldSymbols"; +import { List } from "../../../new_fields/List"; +import PDFMenu from "./PDFMenu"; +import { DocumentManager } from "../../util/DocumentManager"; +import { PresentationView } from "../presentationview/PresentationView"; + +interface IAnnotationProps { + anno: Doc; + index: number; + parent: Viewer; +} + +export default class Annotation extends React.Component<IAnnotationProps> { + render() { + let annotationDocs = DocListCast(this.props.anno.annotations); + let res = annotationDocs.map(a => { + let type = NumCast(a.type); + switch (type) { + // case AnnotationTypes.Pin: + // return <PinAnnotation parent={this} document={a} x={NumCast(a.x)} y={NumCast(a.y)} width={a[WidthSym]()} height={a[HeightSym]()} key={a[Id]} />; + case AnnotationTypes.Region: + return <RegionAnnotation parent={this.props.parent} document={a} index={this.props.index} x={NumCast(a.x)} y={NumCast(a.y)} width={a[WidthSym]()} height={a[HeightSym]()} key={a[Id]} />; + default: + return <div></div>; + } + }); + return res; + } +} + +interface IRegionAnnotationProps { + x: number; + y: number; + width: number; + height: number; + index: number; + parent: Viewer; + document: Doc; +} + +@observer +class RegionAnnotation extends React.Component<IRegionAnnotationProps> { + @observable private _backgroundColor: string = "red"; + + private _reactionDisposer?: IReactionDisposer; + private _scrollDisposer?: IReactionDisposer; + private _mainCont: React.RefObject<HTMLDivElement>; + + constructor(props: IRegionAnnotationProps) { + super(props); + + this._mainCont = React.createRef(); + } + + componentDidMount() { + this._reactionDisposer = reaction( + () => BoolCast(this.props.document.delete), + () => { + if (BoolCast(this.props.document.delete)) { + if (this._mainCont.current) { + this._mainCont.current.style.display = "none"; + } + } + }, + { fireImmediately: true } + ); + + this._scrollDisposer = reaction( + () => this.props.parent.Index, + () => { + if (this.props.parent.Index === this.props.index) { + this.props.parent.scrollTo(this.props.y * scale - (NumCast(this.props.parent.props.parent.Document.pdfHeight) / 2)); + } + } + ); + } + + componentWillUnmount() { + this._reactionDisposer && this._reactionDisposer(); + this._scrollDisposer && this._scrollDisposer(); + } + + deleteAnnotation = () => { + let annotation = DocListCast(this.props.parent.props.parent.fieldExtensionDoc.annotations); + let group = FieldValue(Cast(this.props.document.group, Doc)); + if (group && annotation.indexOf(group) !== -1) { + let newAnnotations = annotation.filter(a => a !== FieldValue(Cast(this.props.document.group, Doc))); + this.props.parent.props.parent.fieldExtensionDoc.annotations = new List<Doc>(newAnnotations); + } + + if (group) { + let groupAnnotations = DocListCast(group.annotations); + groupAnnotations.forEach(anno => anno.delete = true); + } + + PDFMenu.Instance.fadeOut(true); + } + + pinToPres = () => { + let group = FieldValue(Cast(this.props.document.group, Doc)); + if (group) { + PresentationView.Instance.PinDoc(group); + } + } + + @action + onPointerDown = (e: React.PointerEvent) => { + if (e.button === 0) { + let targetDoc = Cast(this.props.document.target, Doc, null); + if (targetDoc) { + DocumentManager.Instance.jumpToDocument(targetDoc, false); + } + } + if (e.button === 2) { + PDFMenu.Instance.Status = "annotation"; + PDFMenu.Instance.Delete = this.deleteAnnotation.bind(this); + PDFMenu.Instance.Pinned = false; + PDFMenu.Instance.AddTag = this.addTag.bind(this); + PDFMenu.Instance.PinToPres = this.pinToPres; + PDFMenu.Instance.jumpTo(e.clientX, e.clientY, true); + } + } + + addTag = (key: string, value: string): boolean => { + let group = FieldValue(Cast(this.props.document.group, Doc)); + if (group) { + let valNum = parseInt(value); + group[key] = isNaN(valNum) ? value : valNum; + return true; + } + return false; + } + + render() { + return ( + <div className="pdfViewer-annotationBox" onPointerDown={this.onPointerDown} ref={this._mainCont} + style={{ + top: this.props.y * scale, + left: this.props.x * scale, + width: this.props.width * scale, + height: this.props.height * scale, + pointerEvents: "all", + backgroundColor: this.props.parent.Index === this.props.index ? "green" : StrCast(this.props.document.color) + }}></div> + ); + } +}
\ No newline at end of file diff --git a/src/client/views/pdf/PDFMenu.scss b/src/client/views/pdf/PDFMenu.scss index a4624b1f6..b06d19c53 100644 --- a/src/client/views/pdf/PDFMenu.scss +++ b/src/client/views/pdf/PDFMenu.scss @@ -21,6 +21,10 @@ .pdfMenu-dragger { height: 100%; transition: width .2s; + background-image: url("https://logodix.com/logo/1020374.png"); + background-size: 90% 100%; + background-repeat: no-repeat; + background-position: left center; } .pdfMenu-addTag { diff --git a/src/client/views/pdf/PDFMenu.tsx b/src/client/views/pdf/PDFMenu.tsx index aeed5213c..27c2a8f1a 100644 --- a/src/client/views/pdf/PDFMenu.tsx +++ b/src/client/views/pdf/PDFMenu.tsx @@ -3,10 +3,9 @@ import "./PDFMenu.scss"; import { observable, action, runInAction } from "mobx"; import { observer } from "mobx-react"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { emptyFunction, returnZero, returnTrue, returnFalse } from "../../../Utils"; +import { emptyFunction, returnFalse } from "../../../Utils"; import { Doc } from "../../../new_fields/Doc"; -import { DragManager } from "../../util/DragManager"; -import { DocUtils } from "../../documents/Documents"; +import { handleBackspace } from "../nodes/PDFBox"; @observer export default class PDFMenu extends React.Component { @@ -19,11 +18,12 @@ export default class PDFMenu extends React.Component { @observable private _transitionDelay: string = ""; - StartDrag: (e: PointerEvent) => void = emptyFunction; + StartDrag: (e: PointerEvent, ele: HTMLElement) => void = emptyFunction; Highlight: (d: Doc | undefined, color: string | undefined) => void = emptyFunction; Delete: () => void = emptyFunction; Snippet: (marquee: { left: number, top: number, width: number, height: number }) => void = emptyFunction; AddTag: (key: string, value: string) => boolean = returnFalse; + PinToPres: () => void = emptyFunction; @observable public Highlighting: boolean = false; @observable public Status: "pdf" | "annotation" | "snippet" | "" = ""; @@ -33,9 +33,10 @@ export default class PDFMenu extends React.Component { private _offsetY: number = 0; private _offsetX: number = 0; - private _mainCont: React.RefObject<HTMLDivElement>; + private _mainCont: React.RefObject<HTMLDivElement> = React.createRef(); + private _commentCont = React.createRef<HTMLButtonElement>(); + private _snippetButton: React.RefObject<HTMLButtonElement> = React.createRef(); private _dragging: boolean = false; - private _snippetButton: React.RefObject<HTMLButtonElement>; @observable private _keyValue: string = ""; @observable private _valueValue: string = ""; @observable private _added: boolean = false; @@ -44,9 +45,6 @@ export default class PDFMenu extends React.Component { super(props); PDFMenu.Instance = this; - - this._mainCont = React.createRef(); - this._snippetButton = React.createRef(); } pointerDown = (e: React.PointerEvent) => { @@ -67,7 +65,7 @@ export default class PDFMenu extends React.Component { return; } - this.StartDrag(e); + this.StartDrag(e, this._commentCont.current!); this._dragging = true; } @@ -240,23 +238,24 @@ export default class PDFMenu extends React.Component { render() { let buttons = this.Status === "pdf" || this.Status === "snippet" ? [ - <button className="pdfMenu-button" title="Click to Highlight" onClick={this.highlightClicked} key="1" + <button key="1" className="pdfMenu-button" title="Click to Highlight" onClick={this.highlightClicked} style={this.Highlighting ? { backgroundColor: "#121212" } : {}}> <FontAwesomeIcon icon="highlighter" size="lg" style={{ transition: "transform 0.1s", transform: this.Highlighting ? "" : "rotate(-45deg)" }} /> </button>, - <button className="pdfMenu-button" title="Drag to Annotate" onPointerDown={this.pointerDown}><FontAwesomeIcon icon="comment-alt" size="lg" key="2" /></button>, + <button className="pdfMenu-button" title="Drag to Annotate" ref={this._commentCont} onPointerDown={this.pointerDown}><FontAwesomeIcon icon="comment-alt" size="lg" key="2" /></button>, this.Status === "snippet" ? <button className="pdfMenu-button" title="Drag to Snippetize Selection" onPointerDown={this.snippetStart} ref={this._snippetButton}><FontAwesomeIcon icon="cut" size="lg" /></button> : undefined, - <button className="pdfMenu-button" title="Pin Menu" onClick={this.togglePin} key="3" + <button key="3" className="pdfMenu-button" title="Pin Menu" onClick={this.togglePin} style={this.Pinned ? { backgroundColor: "#121212" } : {}}> <FontAwesomeIcon icon="thumbtack" size="lg" style={{ transition: "transform 0.1s", transform: this.Pinned ? "rotate(45deg)" : "" }} /> </button> ] : [ - <button className="pdfMenu-button" title="Delete Anchor" onPointerDown={this.deleteClicked}><FontAwesomeIcon icon="trash-alt" size="lg" key="1" /></button>, - <div className="pdfMenu-addTag" key="2"> - <input onChange={this.keyChanged} placeholder="Key" style={{ gridColumn: 1 }} /> - <input onChange={this.valueChanged} placeholder="Value" style={{ gridColumn: 3 }} /> + <button key="4" className="pdfMenu-button" title="Delete Anchor" onPointerDown={this.deleteClicked}><FontAwesomeIcon icon="trash-alt" size="lg" key="1" /></button>, + <button key="5" className="pdfMenu-button" title="Pin to Presentation" onPointerDown={this.PinToPres}><FontAwesomeIcon icon="map-pin" size="lg" key="2" /></button>, + <div className="pdfMenu-addTag" key="3"> + <input onKeyDown={handleBackspace} onChange={this.keyChanged} placeholder="Key" style={{ gridColumn: 1 }} /> + <input onKeyDown={handleBackspace} onChange={this.valueChanged} placeholder="Value" style={{ gridColumn: 3 }} /> </div>, - <button className="pdfMenu-button" title={`Add tag: ${this._keyValue} with value: ${this._valueValue}`} onPointerDown={this.addTag}><FontAwesomeIcon style={{ transition: "all .2s" }} color={this._added ? "#42f560" : "white"} icon="check" size="lg" key="3" /></button>, + <button key="6" className="pdfMenu-button" title={`Add tag: ${this._keyValue} with value: ${this._valueValue}`} onPointerDown={this.addTag}><FontAwesomeIcon style={{ transition: "all .2s" }} color={this._added ? "#42f560" : "white"} icon="check" size="lg" key="4" /></button>, ]; return ( diff --git a/src/client/views/pdf/PDFViewer.scss b/src/client/views/pdf/PDFViewer.scss index 11d3d7e27..0fde764d0 100644 --- a/src/client/views/pdf/PDFViewer.scss +++ b/src/client/views/pdf/PDFViewer.scss @@ -41,6 +41,9 @@ } } } +.pdfViewer-viewerCont { + width:100%; +} .page-cont { .textLayer { diff --git a/src/client/views/pdf/PDFViewer.tsx b/src/client/views/pdf/PDFViewer.tsx index a440a1f27..c560e581c 100644 --- a/src/client/views/pdf/PDFViewer.tsx +++ b/src/client/views/pdf/PDFViewer.tsx @@ -14,7 +14,7 @@ import { Docs, DocUtils, DocumentOptions } from "../../documents/Documents"; import { DocumentManager } from "../../util/DocumentManager"; import { DragManager } from "../../util/DragManager"; import { DocumentView } from "../nodes/DocumentView"; -import { PDFBox } from "../nodes/PDFBox"; +import { PDFBox, handleBackspace } from "../nodes/PDFBox"; import Page from "./Page"; import "./PDFViewer.scss"; import React = require("react"); @@ -23,6 +23,8 @@ import { UndoManager } from "../../util/UndoManager"; import { CompileScript, CompiledScript, CompileResult } from "../../util/Scripting"; import { ScriptField } from "../../../new_fields/ScriptField"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import Annotation from "./Annotation"; +import { KeyCodes } from "../../northstar/utils/KeyCodes"; const PDFJSViewer = require("pdfjs-dist/web/pdf_viewer"); export const scale = 2; @@ -48,7 +50,7 @@ export class PDFViewer extends React.Component<IPDFViewerProps> { render() { return ( - <div ref={this._mainDiv}> + <div className="pdfViewer-viewerCont" ref={this._mainDiv}> {!this._pdf ? (null) : <Viewer pdf={this._pdf} loaded={this.props.loaded} scrollY={this.props.scrollY} parent={this.props.parent} mainCont={this._mainDiv} url={this.props.url} />} </div> @@ -69,7 +71,7 @@ interface IViewerProps { * Handles rendering and virtualization of the pdf */ @observer -class Viewer extends React.Component<IViewerProps> { +export class Viewer extends React.Component<IViewerProps> { // _visibleElements is the array of JSX elements that gets rendered @observable.shallow private _visibleElements: JSX.Element[] = []; // _isPage is an array that tells us whether or not an index is rendered as a page or as a placeholder @@ -91,6 +93,7 @@ class Viewer extends React.Component<IViewerProps> { private _activeReactionDisposer?: IReactionDisposer; private _viewer: React.RefObject<HTMLDivElement>; private _mainCont: React.RefObject<HTMLDivElement>; + private _pdfViewer: any; // private _textContent: Pdfjs.TextContent[] = []; private _pdfFindController: any; private _searchString: string = ""; @@ -124,9 +127,12 @@ class Viewer extends React.Component<IViewerProps> { }, { fireImmediately: true }); this._annotationReactionDisposer = reaction( - () => this.props.parent.Document && DocListCast(this.props.parent.Document.annotations), - (annotations: Doc[]) => - annotations && annotations.length && this.renderAnnotations(annotations, true), + () => { + return this.props.parent && this.props.parent.fieldExtensionDoc && DocListCast(this.props.parent.fieldExtensionDoc.annotations); + }, + (annotations: Doc[]) => { + annotations && annotations.length && this.renderAnnotations(annotations, true); + }, { fireImmediately: true }); this._activeReactionDisposer = reaction( @@ -155,7 +161,9 @@ class Viewer extends React.Component<IViewerProps> { let scriptfield = Cast(this.props.parent.Document.filterScript, ScriptField); this._script = scriptfield ? scriptfield.script : CompileScript("return true"); if (this.props.parent.props.ContainingCollectionView) { - let ccvAnnos = DocListCast(this.props.parent.props.ContainingCollectionView.props.Document.annotations); + let fieldDoc = Doc.resolvedFieldDataDoc(this.props.parent.props.ContainingCollectionView.props.DataDoc ? + this.props.parent.props.ContainingCollectionView.props.DataDoc : this.props.parent.props.ContainingCollectionView.props.Document, this.props.parent.props.ContainingCollectionView.props.fieldKey, "true"); + let ccvAnnos = DocListCast(fieldDoc.annotations); ccvAnnos.forEach(d => { if (this._script && this._script.compiled) { let run = this._script.run(d); @@ -165,6 +173,7 @@ class Viewer extends React.Component<IViewerProps> { } }); } + this.Index = -1; }); } ); @@ -192,10 +201,14 @@ class Viewer extends React.Component<IViewerProps> { let pageSizes = Array<{ width: number, height: number }>(this.props.pdf.numPages); this._isPage = Array<string>(this.props.pdf.numPages); // this._textContent = Array<Pdfjs.TextContent>(this.props.pdf.numPages); + const proms: Pdfjs.PDFPromise<any>[] = []; for (let i = 0; i < this.props.pdf.numPages; i++) { - await this.props.pdf.getPage(i + 1).then(page => runInAction(() => { - // pageSizes[i] = { width: page.view[2] * scale, height: page.view[3] * scale }; - let x = page.getViewport(scale); + proms.push(this.props.pdf.getPage(i + 1).then(page => runInAction(() => { + pageSizes[i] = { + width: (page.view[page.rotate === 0 || page.rotate === 180 ? 2 : 3] - page.view[page.rotate === 0 || page.rotate === 180 ? 0 : 1]) * scale, + height: (page.view[page.rotate === 0 || page.rotate === 180 ? 3 : 2] - page.view[page.rotate === 0 || page.rotate === 180 ? 1 : 0]) * scale + }; + // let x = page.getViewport(scale); // page.getTextContent().then((text: Pdfjs.TextContent) => { // // let tc = new Pdfjs.TextContentItem() // // let tc = {str: } @@ -204,9 +217,10 @@ class Viewer extends React.Component<IViewerProps> { // // tcStr += t.str; // // }) // }); - pageSizes[i] = { width: x.width, height: x.height }; - })); + // pageSizes[i] = { width: x.width, height: x.height }; + }))); } + await Promise.all(proms); runInAction(() => Array.from(Array((this._pageSizes = pageSizes).length).keys()).map(this.getPlaceholderPage)); this.props.loaded(Math.max(...pageSizes.map(i => i.width)), pageSizes[0].height, this.props.pdf.numPages); @@ -222,17 +236,22 @@ class Viewer extends React.Component<IViewerProps> { } } + @action makeAnnotationDocument = (sourceDoc: Doc | undefined, s: number, color: string): Doc => { let annoDocs: Doc[] = []; - let mainAnnoDoc = Docs.CreateInstance(new Doc(), "", {}); + let mainAnnoDoc = Docs.Create.InstanceFromProto(new Doc(), "", {}); mainAnnoDoc.title = "Annotation on " + StrCast(this.props.parent.Document.title); - mainAnnoDoc.pdfDoc = this.props.parent.Document; + mainAnnoDoc.pdfDoc = this.props.parent.props.Document; + let minY = Number.MAX_VALUE; this._savedAnnotations.forEach((key: number, value: HTMLDivElement[]) => { for (let anno of value) { let annoDoc = new Doc(); if (anno.style.left) annoDoc.x = parseInt(anno.style.left) / scale; - if (anno.style.top) annoDoc.y = parseInt(anno.style.top) / scale; + if (anno.style.top) { + annoDoc.y = parseInt(anno.style.top) / scale; + minY = Math.min(parseInt(anno.style.top), minY); + } if (anno.style.height) annoDoc.height = parseInt(anno.style.height) / scale; if (anno.style.width) annoDoc.width = parseInt(anno.style.width) / scale; annoDoc.page = key; @@ -245,12 +264,13 @@ class Viewer extends React.Component<IViewerProps> { } }); - mainAnnoDoc.y = Math.max((NumCast(annoDocs[0].y) * scale) - 100, 0); + mainAnnoDoc.y = Math.max(minY, 0); mainAnnoDoc.annotations = new List<Doc>(annoDocs); if (sourceDoc) { DocUtils.MakeLink(sourceDoc, mainAnnoDoc, undefined, `Annotation from ${StrCast(this.props.parent.Document.title)}`, "", StrCast(this.props.parent.Document.title)); } this._savedAnnotations.clear(); + this.Index = -1; return mainAnnoDoc; } @@ -258,13 +278,14 @@ class Viewer extends React.Component<IViewerProps> { if (de.data instanceof DragManager.LinkDragData) { let sourceDoc = de.data.linkSourceDocument; let destDoc = this.makeAnnotationDocument(sourceDoc, 1, "red"); - let targetAnnotations = DocListCast(this.props.parent.Document.annotations); + de.data.droppedDocuments.push(destDoc); + let targetAnnotations = DocListCast(this.props.parent.fieldExtensionDoc.annotations); if (targetAnnotations) { targetAnnotations.push(destDoc); - this.props.parent.Document.annotations = new List<Doc>(targetAnnotations); + this.props.parent.fieldExtensionDoc.annotations = new List<Doc>(targetAnnotations); } else { - this.props.parent.Document.annotations = new List<Doc>([destDoc]); + this.props.parent.fieldExtensionDoc.annotations = new List<Doc>([destDoc]); } e.stopPropagation(); } @@ -419,20 +440,8 @@ class Viewer extends React.Component<IViewerProps> { } } - renderAnnotation = (anno: Doc, index: number): 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} index={index} x={NumCast(a.x)} y={NumCast(a.y)} width={a[WidthSym]()} height={a[HeightSym]()} key={a[Id]} />; - default: - return <div></div>; - } - }); - return res; + renderAnnotation = (anno: Doc, index: number): JSX.Element => { + return <Annotation anno={anno} index={index} parent={this} key={`${anno[Id]}-annotation`} />; } @action @@ -442,11 +451,7 @@ class Viewer extends React.Component<IViewerProps> { @action search = (searchString: string) => { - if (searchString.length === 0) { - return; - } - - if (this._rendered) { + if (this._pdfViewer._pageViewsReady) { this._pdfFindController.executeCommand('find', { caseSensitive: false, @@ -459,6 +464,18 @@ class Viewer extends React.Component<IViewerProps> { else { let container = this._mainCont.current; if (container) { + container.addEventListener("pagesloaded", () => { + console.log("rendered"); + this._pdfFindController.executeCommand('find', + { + caseSensitive: false, + findPrevious: undefined, + highlightAll: true, + phraseSearch: true, + query: searchString + }); + this._rendered = true; + }); container.addEventListener("pagerendered", () => { console.log("rendered"); this._pdfFindController.executeCommand('find', @@ -533,23 +550,23 @@ class Viewer extends React.Component<IViewerProps> { if (!this._pdfFindController) { if (container && viewer) { let simpleLinkService = new SimpleLinkService(); - let pdfViewer = new PDFJSViewer.PDFViewer({ + this._pdfViewer = new PDFJSViewer.PDFViewer({ container: container, viewer: viewer, linkService: simpleLinkService }); simpleLinkService.setPdf(this.props.pdf); container.addEventListener("pagesinit", () => { - pdfViewer.currentScaleValue = 1; + this._pdfViewer.currentScaleValue = 1; }); container.addEventListener("pagerendered", () => { console.log("rendered"); this._rendered = true; }); - pdfViewer.setDocument(this.props.pdf); - this._pdfFindController = new PDFJSViewer.PDFFindController(pdfViewer); + this._pdfViewer.setDocument(this.props.pdf); + this._pdfFindController = new PDFJSViewer.PDFFindController(this._pdfViewer); // this._pdfFindController._linkService = pdfLinkService; - pdfViewer.findController = this._pdfFindController; + this._pdfViewer.findController = this._pdfFindController; } } } @@ -568,9 +585,10 @@ class Viewer extends React.Component<IViewerProps> { prevAnnotation = (e: React.MouseEvent) => { e.stopPropagation(); - if (this.Index > 0) { - this.Index--; - } + // if (this.Index > 0) { + // this.Index--; + // } + this.Index = Math.max(this.Index - 1, 0); } @action @@ -578,7 +596,7 @@ class Viewer extends React.Component<IViewerProps> { e.stopPropagation(); let compiled = this._script; - if (this.Index < this._annotations.filter(anno => { + let filtered = this._annotations.filter(anno => { if (compiled && compiled.compiled) { let run = compiled.run({ this: anno }); if (run.success) { @@ -586,9 +604,8 @@ class Viewer extends React.Component<IViewerProps> { } } return true; - }).length) { - this.Index++; - } + }); + this.Index = Math.min(this.Index + 1, filtered.length - 1); } nextResult = () => { @@ -636,7 +653,8 @@ class Viewer extends React.Component<IViewerProps> { } } return true; - }).map((anno: Doc, index: number) => this.renderAnnotation(anno, index))} + }).sort((a: Doc, b: Doc) => NumCast(a.y) - NumCast(b.y)) + .map((anno: Doc, index: number) => this.renderAnnotation(anno, index))} </div> </div> <div className="pdfViewer-overlayCont" onPointerDown={(e) => e.stopPropagation()} @@ -647,7 +665,7 @@ class Viewer extends React.Component<IViewerProps> { <button className="pdfViewer-overlayButton" title="Open Search Bar"></button> {/* <button title="Previous Result" onClick={() => this.search(this._searchString)}><FontAwesomeIcon icon="arrow-up" size="3x" color="white" /></button> <button title="Next Result" onClick={this.nextResult}><FontAwesomeIcon icon="arrow-down" size="3x" color="white" /></button> */} - <input placeholder="Search" className="pdfViewer-overlaySearchBar" onChange={this.searchStringChanged} /> + <input onKeyDown={(e: React.KeyboardEvent) => e.keyCode === KeyCodes.ENTER ? this.search(this._searchString) : e.keyCode === KeyCodes.BACKSPACE ? e.stopPropagation() : true} placeholder="Search" className="pdfViewer-overlaySearchBar" onChange={this.searchStringChanged} /> <button title="Search" onClick={() => this.search(this._searchString)}><FontAwesomeIcon icon="search" size="3x" color="white" /></button> </div> <button className="pdfViewer-overlayButton" onClick={this.prevAnnotation} title="Previous Annotation" @@ -678,116 +696,6 @@ export enum AnnotationTypes { Region } -interface IAnnotationProps { - x: number; - y: number; - width: number; - height: number; - index: number; - parent: Viewer; - document: Doc; -} - -@observer -class RegionAnnotation extends React.Component<IAnnotationProps> { - @observable private _backgroundColor: string = "red"; - - private _reactionDisposer?: IReactionDisposer; - private _scrollDisposer?: 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 } - ); - - this._scrollDisposer = reaction( - () => this.props.parent.Index, - () => { - if (this.props.parent.Index === this.props.index) { - this.props.parent.scrollTo(this.props.y - 50); - } - } - ); - } - - componentWillUnmount() { - this._reactionDisposer && this._reactionDisposer(); - this._scrollDisposer && this._scrollDisposer(); - } - - 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); - } - - @action - onPointerDown = (e: React.PointerEvent) => { - if (e.button === 0) { - let targetDoc = Cast(this.props.document.target, Doc, null); - if (targetDoc) { - DocumentManager.Instance.jumpToDocument(targetDoc, true); - } - } - if (e.button === 2) { - PDFMenu.Instance.Status = "annotation"; - PDFMenu.Instance.Delete = this.deleteAnnotation.bind(this); - PDFMenu.Instance.Pinned = false; - PDFMenu.Instance.AddTag = this.addTag.bind(this); - PDFMenu.Instance.jumpTo(e.clientX, e.clientY, true); - } - } - - addTag = (key: string, value: string): boolean => { - let group = FieldValue(Cast(this.props.document.group, Doc)); - if (group) { - let valNum = parseInt(value); - group[key] = isNaN(valNum) ? value : valNum; - return true; - } - return false; - } - - render() { - return ( - <div className="pdfViewer-annotationBox" onPointerDown={this.onPointerDown} ref={this._mainCont} - style={{ - top: this.props.y * scale, - left: this.props.x * scale, - width: this.props.width * scale, - height: this.props.height * scale, - pointerEvents: "all", - backgroundColor: this.props.parent.Index === this.props.index ? "goldenrod" : StrCast(this.props.document.color) - }}></div> - ); - } -} - class SimpleLinkService { externalLinkTarget: any = null; externalLinkRel: any = null; diff --git a/src/client/views/pdf/Page.tsx b/src/client/views/pdf/Page.tsx index 57e36be43..c9d442fe5 100644 --- a/src/client/views/pdf/Page.tsx +++ b/src/client/views/pdf/Page.tsx @@ -2,7 +2,7 @@ import { observer } from "mobx-react"; import React = require("react"); import { observable, action, runInAction, IReactionDisposer, reaction } from "mobx"; import * as Pdfjs from "pdfjs-dist"; -import { Opt, Doc, FieldResult, Field, DocListCast, WidthSym, HeightSym } from "../../../new_fields/Doc"; +import { Opt, Doc, FieldResult, Field, DocListCast, WidthSym, HeightSym, DocListCastAsync } from "../../../new_fields/Doc"; import "./PDFViewer.scss"; import "pdfjs-dist/web/pdf_viewer.css"; import { PDFBox } from "../nodes/PDFBox"; @@ -10,7 +10,7 @@ import { DragManager } from "../../util/DragManager"; import { Docs, DocUtils } from "../../documents/Documents"; import { List } from "../../../new_fields/List"; import { emptyFunction } from "../../../Utils"; -import { Cast, NumCast, StrCast } from "../../../new_fields/Types"; +import { Cast, NumCast, StrCast, BoolCast } from "../../../new_fields/Types"; import { listSpec } from "../../../new_fields/Schema"; import { menuBar } from "prosemirror-menu"; import { AnnotationTypes, PDFViewer, scale } from "./PDFViewer"; @@ -52,9 +52,11 @@ export default class Page extends React.Component<IPageProps> { private _textLayer: React.RefObject<HTMLDivElement>; private _annotationLayer: React.RefObject<HTMLDivElement>; private _marquee: React.RefObject<HTMLDivElement>; - private _curly: React.RefObject<HTMLImageElement>; + // private _curly: React.RefObject<HTMLImageElement>; private _marqueeing: boolean = false; private _reactionDisposer?: IReactionDisposer; + private _startY: number = 0; + private _startX: number = 0; constructor(props: IPageProps) { super(props); @@ -62,7 +64,7 @@ export default class Page extends React.Component<IPageProps> { this._textLayer = React.createRef(); this._annotationLayer = React.createRef(); this._marquee = React.createRef(); - this._curly = React.createRef(); + // this._curly = React.createRef(); } componentDidMount = (): void => { @@ -138,9 +140,9 @@ export default class Page extends React.Component<IPageProps> { highlight = (targetDoc?: Doc, color: string = "red") => { // creates annotation documents for current highlights let annotationDoc = this.props.makeAnnotationDocuments(targetDoc, scale, color, false); - let targetAnnotations = Cast(this.props.parent.Document.annotations, listSpec(Doc)); + let targetAnnotations = Cast(this.props.parent.fieldExtensionDoc.annotations, listSpec(Doc)); if (targetAnnotations === undefined) { - Doc.GetProto(this.props.parent.Document).annotations = new List([annotationDoc]); + Doc.GetProto(this.props.parent.fieldExtensionDoc).annotations = new List([annotationDoc]); } else { targetAnnotations.push(annotationDoc); } @@ -152,20 +154,34 @@ export default class Page extends React.Component<IPageProps> { * start a drag event and create or put the necessary info into the drag event. */ @action - startDrag = (e: PointerEvent): void => { + startDrag = (e: PointerEvent, ele: HTMLElement): void => { e.preventDefault(); e.stopPropagation(); let thisDoc = this.props.parent.Document; // document that this annotation is linked to - let targetDoc = Docs.TextDocument({ width: 200, height: 200, title: "New Annotation" }); + let targetDoc = Docs.Create.TextDocument({ width: 200, height: 200, title: "New Annotation" }); targetDoc.targetPage = this.props.page; - let annotationDoc = this.highlight(targetDoc, "red"); + let annotationDoc = this.highlight(undefined, "red"); + annotationDoc.linkedToDoc = false; // create dragData and star tdrag let dragData = new DragManager.AnnotationDragData(thisDoc, annotationDoc, targetDoc); if (this._textLayer.current) { - DragManager.StartAnnotationDrag([this._textLayer.current], dragData, e.pageX, e.pageY, { + DragManager.StartAnnotationDrag([ele], dragData, e.pageX, e.pageY, { handlers: { - dragComplete: emptyFunction, + dragComplete: async () => { + if (!(await annotationDoc.linkedToDoc)) { + let annotations = await DocListCastAsync(annotationDoc.annotations); + if (annotations) { + annotations.forEach(anno => { + anno.target = targetDoc; + }); + } + let pdfDoc = await Cast(annotationDoc.pdfDoc, Doc); + if (pdfDoc) { + DocUtils.MakeLink(annotationDoc, targetDoc, undefined, `Annotation from ${StrCast(pdfDoc.title)}`, "", StrCast(pdfDoc.title)); + } + } + } }, hideSource: false }); @@ -183,8 +199,11 @@ export default class Page extends React.Component<IPageProps> { let doc = this.props.parent.Document; let view = Doc.MakeAlias(doc); let data = Doc.MakeDelegate(doc.proto!); + data.title = StrCast(data.title) + "_snippet"; view.proto = data; view.nativeHeight = marquee.height; + view.height = (doc[WidthSym]() / NumCast(doc.nativeWidth)) * marquee.height; + view.nativeWidth = doc.nativeWidth; view.startY = marquee.top + this.props.getScrollFromPage(this.props.page); view.width = doc[WidthSym](); let dragData = new DragManager.DocumentDragData([view], [undefined]); @@ -217,8 +236,8 @@ export default class Page extends React.Component<IPageProps> { let current = this._textLayer.current; if (current) { let boundingRect = current.getBoundingClientRect(); - this._marqueeX = (e.clientX - boundingRect.left) * (current.offsetWidth / boundingRect.width); - this._marqueeY = (e.clientY - boundingRect.top) * (current.offsetHeight / boundingRect.height); + this._startX = this._marqueeX = (e.clientX - boundingRect.left) * (current.offsetWidth / boundingRect.width); + this._startY = this._marqueeY = (e.clientY - boundingRect.top) * (current.offsetHeight / boundingRect.height); } this._marqueeing = true; if (this._marquee.current) this._marquee.current.style.opacity = "0.2"; @@ -241,12 +260,16 @@ export default class Page extends React.Component<IPageProps> { if (current) { // transform positions and find the width and height to set the marquee to let boundingRect = current.getBoundingClientRect(); - this._marqueeWidth = (e.clientX - boundingRect.left) * (current.offsetWidth / boundingRect.width) - this._marqueeX; - this._marqueeHeight = (e.clientY - boundingRect.top) * (current.offsetHeight / boundingRect.height) - this._marqueeY; + this._marqueeWidth = ((e.clientX - boundingRect.left) * (current.offsetWidth / boundingRect.width)) - this._startX; + this._marqueeHeight = ((e.clientY - boundingRect.top) * (current.offsetHeight / boundingRect.height)) - this._startY; + this._marqueeX = Math.min(this._startX, this._startX + this._marqueeWidth); + this._marqueeY = Math.min(this._startY, this._startY + this._marqueeHeight); + this._marqueeWidth = Math.abs(this._marqueeWidth); + this._marqueeHeight = Math.abs(this._marqueeHeight); let { background, opacity, transform: transform } = this.getCurlyTransform(); - if (this._marquee.current && this._curly.current) { + if (this._marquee.current /*&& this._curly.current*/) { this._marquee.current.style.background = background; - this._curly.current.style.opacity = opacity; + // this._curly.current.style.opacity = opacity; this._rotate = transform; } } @@ -259,33 +282,33 @@ export default class Page extends React.Component<IPageProps> { } getCurlyTransform = (): { background: string, opacity: string, transform: string } => { - let background = "", opacity = "", transform = ""; - if (this._marquee.current && this._curly.current) { - if (this._marqueeWidth > 100 && this._marqueeHeight > 100) { - background = "red"; - opacity = "0"; - } - else { - background = "transparent"; - opacity = "1"; - } - - // 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 }; + // 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: "red", opacity: "0.5", transform: "" }; } @action @@ -305,16 +328,17 @@ export default class Page extends React.Component<IPageProps> { 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 = style.opacity; - } + // if (opacity === "1" && this._curly.current) { + // copy.style.opacity = opacity; + // let img = this._curly.current.cloneNode(); + // (img as any).style.opacity = opacity; + // (img as any).style.transform = transform; + // copy.appendChild(img); + // } + // else { + copy.style.border = style.border; + copy.style.opacity = style.opacity; + // } copy.className = this._marquee.current.className; this.props.createAnnotation(copy, this.props.page); this._marquee.current.style.opacity = "0"; @@ -340,7 +364,7 @@ export default class Page extends React.Component<IPageProps> { if (PDFMenu.Instance.Highlighting) { - this.highlight(undefined, "#f4f442"); + this.highlight(undefined, "goldenrod"); } else { PDFMenu.Instance.StartDrag = this.startDrag; @@ -401,8 +425,8 @@ export default class Page extends React.Component<IPageProps> { </div> <div className="pdfInkingLayer-cont" ref={this._annotationLayer} style={{ width: "100%", height: "100%", position: "relative", top: "-100%" }}> <div className="pdfViewer-annotationBox" ref={this._marquee} - style={{ left: `${this._marqueeX}px`, top: `${this._marqueeY}px`, width: `${this._marqueeWidth}px`, height: `${this._marqueeHeight}px`, background: "transparent" }}> - <img ref={this._curly} src="https://static.thenounproject.com/png/331760-200.png" style={{ width: "100%", height: "100%", transform: `${this._rotate}` }} /> + style={{ left: `${this._marqueeX}px`, top: `${this._marqueeY}px`, width: `${this._marqueeWidth}px`, height: `${this._marqueeHeight}px`, background: "red", border: `${this._marqueeWidth === 0 ? "" : "10px dashed black"}` }}> + {/* <img ref={this._curly} src="https://static.thenounproject.com/png/331760-200.png" style={{ width: "100%", height: "100%", transform: `${this._rotate}` }} /> */} </div> </div> <div className="textlayer" ref={this._textLayer} style={{ "position": "relative", "top": `-${2 * this._height}px`, "height": `${this._height}px` }} /> |
