From 49fcb0f6613344fb62db618c0b466c6155c20eb0 Mon Sep 17 00:00:00 2001
From: Bob Zeleznik <zzzman@gmail.com>
Date: Mon, 27 Jul 2020 00:32:50 -0400
Subject: extended pdfviewer interactions to WebBox's.

---
 src/client/views/nodes/WebBox.tsx | 262 +++++++++++++++++++++++++++++++++++---
 1 file changed, 242 insertions(+), 20 deletions(-)

(limited to 'src/client/views/nodes/WebBox.tsx')

diff --git a/src/client/views/nodes/WebBox.tsx b/src/client/views/nodes/WebBox.tsx
index 724044295..bc30b15e6 100644
--- a/src/client/views/nodes/WebBox.tsx
+++ b/src/client/views/nodes/WebBox.tsx
@@ -1,46 +1,62 @@
-import { library } from "@fortawesome/fontawesome-svg-core";
-import { faStickyNote, faPen, faMousePointer } from '@fortawesome/free-solid-svg-icons';
-import { action, computed, observable, trace, IReactionDisposer, reaction, runInAction } from "mobx";
+import { faMousePointer, faPen, faStickyNote } from '@fortawesome/free-solid-svg-icons';
+import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
+import { action, computed, IReactionDisposer, observable, reaction, runInAction } from "mobx";
 import { observer } from "mobx-react";
-import { Doc, FieldResult, DocListCast } from "../../../fields/Doc";
+import { Dictionary } from "typescript-collections";
+import * as WebRequest from 'web-request';
+import { Doc, DocListCast, Opt } from "../../../fields/Doc";
 import { documentSchema } from "../../../fields/documentSchemas";
+import { Id } from "../../../fields/FieldSymbols";
 import { HtmlField } from "../../../fields/HtmlField";
 import { InkTool } from "../../../fields/InkField";
-import { makeInterface, listSpec } from "../../../fields/Schema";
-import { Cast, NumCast, BoolCast, StrCast } from "../../../fields/Types";
+import { List } from "../../../fields/List";
+import { listSpec, makeInterface } from "../../../fields/Schema";
+import { Cast, NumCast, StrCast } from "../../../fields/Types";
 import { WebField } from "../../../fields/URLField";
-import { Utils, returnOne, emptyFunction, returnZero } from "../../../Utils";
-import { Docs } from "../../documents/Documents";
+import { TraceMobx } from "../../../fields/util";
+import { addStyleSheet, clearStyleSheetRules, emptyFunction, returnOne, returnZero, Utils } from "../../../Utils";
+import { Docs, DocUtils } from "../../documents/Documents";
 import { DragManager } from "../../util/DragManager";
 import { ImageUtils } from "../../util/Import & Export/ImageUtils";
+import { undoBatch } from "../../util/UndoManager";
+import { CollectionFreeFormView } from "../collections/collectionFreeForm/CollectionFreeFormView";
+import { ContextMenu } from "../ContextMenu";
+import { ContextMenuProps } from "../ContextMenuItem";
 import { ViewBoxAnnotatableComponent } from "../DocComponent";
 import { DocumentDecorations } from "../DocumentDecorations";
+import Annotation from "../pdf/Annotation";
+import PDFMenu from "../pdf/PDFMenu";
+import { PdfViewerMarquee } from "../pdf/PDFViewer";
 import { FieldView, FieldViewProps } from './FieldView';
 import "./WebBox.scss";
+import "../pdf/PDFViewer.scss";
 import React = require("react");
-import * as WebRequest from 'web-request';
-import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
-import { CollectionFreeFormView } from "../collections/collectionFreeForm/CollectionFreeFormView";
-import { ContextMenu } from "../ContextMenu";
-import { ContextMenuProps } from "../ContextMenuItem";
-import { undoBatch } from "../../util/UndoManager";
-import { List } from "../../../fields/List";
 const htmlToText = require("html-to-text");
 
-library.add(faStickyNote);
-
 type WebDocument = makeInterface<[typeof documentSchema]>;
 const WebDocument = makeInterface(documentSchema);
 
 @observer
 export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps, WebDocument>(WebDocument) {
 
+    private _annotationLayer: React.RefObject<HTMLDivElement> = React.createRef();
+    static _annotationStyle: any = addStyleSheet();
+    private _mainCont: React.RefObject<HTMLDivElement> = React.createRef();
+    private _startX: number = 0;
+    private _startY: number = 0;
+    @observable private _marqueeX: number = 0;
+    @observable private _marqueeY: number = 0;
+    @observable private _marqueeWidth: number = 0;
+    @observable private _marqueeHeight: number = 0;
+    @observable private _marqueeing: boolean = false;
     public static LayoutString(fieldKey: string) { return FieldView.LayoutString(WebBox, fieldKey); }
     get _collapsed() { return StrCast(this.layoutDoc._chromeStatus) !== "enabled"; }
     set _collapsed(value) { this.layoutDoc._chromeStatus = !value ? "enabled" : "disabled"; }
     @observable private _url: string = "hello";
     @observable private _pressX: number = 0;
     @observable private _pressY: number = 0;
+    @observable private _savedAnnotations: Dictionary<number, HTMLDivElement[]> = new Dictionary<number, HTMLDivElement[]>();
+    private _selectionReactionDisposer?: IReactionDisposer;
     private _keyInput = React.createRef<HTMLInputElement>();
     private _longPressSecondsHack?: NodeJS.Timeout;
     private _outerRef = React.createRef<HTMLDivElement>();
@@ -75,6 +91,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps, WebDocum
             { fireImmediately: true }
         );
     });
+
     setPreviewCursor = (func?: (x: number, y: number, drag: boolean) => void) => this._setPreviewCursor = func;
     iframedown = (e: PointerEvent) => {
         this._setPreviewCursor?.(e.screenX, e.screenY, false);
@@ -89,6 +106,17 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps, WebDocum
         const urlField = Cast(this.dataDoc[this.props.fieldKey], WebField);
         runInAction(() => this._url = urlField?.url.toString() || "");
 
+
+        this._selectionReactionDisposer = reaction(() => this.props.isSelected(),
+            selected => {
+                if (!selected) {
+                    this._savedAnnotations.values().forEach(v => v.forEach(a => a.remove()));
+                    this._savedAnnotations.keys().forEach(k => this._savedAnnotations.setValue(k, []));
+                    PDFMenu.Instance.fadeOut(true);
+                }
+            },
+            { fireImmediately: true });
+
         document.addEventListener("pointerup", this.onLongPressUp);
         document.addEventListener("pointermove", this.onLongPressMove);
         const field = Cast(this.rootDoc[this.props.fieldKey], WebField);
@@ -112,6 +140,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps, WebDocum
     }
 
     componentWillUnmount() {
+        this._selectionReactionDisposer?.();
         this._reactionDisposer?.();
         document.removeEventListener("pointerup", this.onLongPressUp);
         document.removeEventListener("pointermove", this.onLongPressMove);
@@ -267,11 +296,14 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps, WebDocum
         );
     }
 
+
     @action
     toggleCollapse = () => {
         this._collapsed = !this._collapsed;
     }
 
+
+
     _ignore = 0;
     onPreWheel = (e: React.WheelEvent) => {
         this._ignore = e.timeStamp;
@@ -284,6 +316,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps, WebDocum
             e.stopPropagation();
         }
     }
+
     onPostWheel = (e: React.WheelEvent) => {
         if (this._ignore !== e.timeStamp) {
             e.stopPropagation();
@@ -430,6 +463,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps, WebDocum
 
         return (<>
             <div className={"webBox-cont" + (this.props.isSelected() && Doc.GetSelectedTool() === InkTool.None && !decInteracting ? "-interactive" : "")}
+                style={{ width: Number.isFinite(this.props.ContentScaling()) ? `${Math.max(100, 100 / this.props.ContentScaling())}% ` : "100%" }}
                 onWheel={this.onPostWheel} onPointerDown={this.onPostPointer} onPointerMove={this.onPostPointer} onPointerUp={this.onPostPointer}>
                 {view}
             </div>;
@@ -444,9 +478,188 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps, WebDocum
             {this.urlEditor()}
         </>);
     }
+
+
+
+    @computed get allAnnotations() { return DocListCast(this.dataDoc[this.props.fieldKey + "-annotations"]); }
+    @computed get nonDocAnnotations() { return this.allAnnotations.filter(a => a.annotations); }
+
+    @undoBatch
+    @action
+    makeAnnotationDocument = (color: string): Opt<Doc> => {
+        if (this._savedAnnotations.size() === 0) return undefined;
+        const anno = this._savedAnnotations.values()[0][0];
+        const annoDoc = Docs.Create.FreeformDocument([], { backgroundColor: color, annotationOn: this.props.Document, title: "Annotation on " + this.Document.title });
+        if (anno.style.left) annoDoc.x = parseInt(anno.style.left);
+        if (anno.style.top) annoDoc.y = NumCast(this.layoutDoc._scrollTop) + parseInt(anno.style.top);
+        if (anno.style.height) annoDoc._height = parseInt(anno.style.height);
+        if (anno.style.width) annoDoc._width = parseInt(anno.style.width);
+        anno.remove();
+        this._savedAnnotations.clear();
+        return annoDoc;
+    }
+    @computed get annotationLayer() {
+        TraceMobx();
+        return <div className="webBox-annotationLayer" style={{ height: NumCast(this.Document._nativeHeight) }} ref={this._annotationLayer}>
+            {this.nonDocAnnotations.sort((a, b) => NumCast(a.y) - NumCast(b.y)).map(anno =>
+                <Annotation {...this.props} focus={this.props.focus} dataDoc={this.dataDoc} fieldKey={this.props.fieldKey} anno={anno} key={`${anno[Id]}-annotation`} />)
+            }
+        </div>;
+    }
+    @action
+    createAnnotation = (div: HTMLDivElement, page: number) => {
+        if (this._annotationLayer.current) {
+            if (div.style.top) {
+                div.style.top = (parseInt(div.style.top)).toString();
+            }
+            this._annotationLayer.current.append(div);
+            div.style.backgroundColor = "#ACCEF7";
+            div.style.opacity = "0.5";
+            const savedPage = this._savedAnnotations.getValue(page);
+            if (savedPage) {
+                savedPage.push(div);
+                this._savedAnnotations.setValue(page, savedPage);
+            }
+            else {
+                this._savedAnnotations.setValue(page, [div]);
+            }
+        }
+    }
+
+    @action
+    highlight = (color: string) => {
+        // creates annotation documents for current highlights
+        const annotationDoc = this.makeAnnotationDocument(color);
+        annotationDoc && Doc.AddDocToList(this.props.Document, this.annotationKey, 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 = async (e: PointerEvent, ele: HTMLElement) => {
+        e.preventDefault();
+        e.stopPropagation();
+
+        const clipDoc = Doc.MakeAlias(this.dataDoc);
+        clipDoc._fitWidth = true;
+        clipDoc._width = this.marqueeWidth();
+        clipDoc._height = this.marqueeHeight();
+        clipDoc._scrollTop = this.marqueeY();
+        const targetDoc = Docs.Create.TextDocument("", { _width: 200, _height: 200, title: "Note linked to " + this.props.Document.title });
+        Doc.GetProto(targetDoc).data = new List<Doc>([clipDoc]);
+        clipDoc.rootDocument = targetDoc;
+        targetDoc.layoutKey = "layout";
+        const annotationDoc = this.highlight("rgba(146, 245, 95, 0.467)"); // yellowish highlight color when dragging out a text selection
+        if (annotationDoc) {
+            DragManager.StartPdfAnnoDrag([ele], new DragManager.PdfAnnoDragData(this.props.Document, annotationDoc, targetDoc), e.pageX, e.pageY, {
+                dragComplete: e => {
+                    if (!e.aborted && e.annoDragData && !e.annoDragData.linkedToDoc) {
+                        DocUtils.MakeLink({ doc: annotationDoc }, { doc: e.annoDragData.dropDocument }, "Annotation");
+                        annotationDoc.isLinkButton = true;
+                        e.annoDragData.dropDocument.isPushpin = true;
+                        e.annoDragData.dropDocument.isLinkButton = true;
+                    }
+                }
+            });
+        }
+    }
+    @action
+    onMarqueeDown = (e: React.PointerEvent) => {
+        this._marqueeing = false;
+        if (!e.altKey && e.button === 0 && this.active(true)) {
+            // clear out old marquees and initialize menu for new selection
+            PDFMenu.Instance.StartDrag = this.startDrag;
+            PDFMenu.Instance.Highlight = this.highlight;
+            PDFMenu.Instance.Status = "pdf";
+            PDFMenu.Instance.fadeOut(true);
+            this._savedAnnotations.values().forEach(v => v.forEach(a => a.remove()));
+            this._savedAnnotations.keys().forEach(k => this._savedAnnotations.setValue(k, []));
+            if ((e.target as any)?.parentElement.className === "textLayer") {
+                // start selecting text if mouse down on textLayer spans
+            }
+            else if (this._mainCont.current) {
+                // set marquee x and y positions to the spatially transformed position
+                const boundingRect = this._mainCont.current.getBoundingClientRect();
+                this._startX = (e.clientX - boundingRect.left) / boundingRect.width * (this.Document._nativeWidth || 1);
+                this._startY = (e.clientY - boundingRect.top) / boundingRect.height * (this.Document._nativeHeight || 1);
+                this._marqueeHeight = this._marqueeWidth = 0;
+                this._marqueeing = true;
+            }
+            document.removeEventListener("pointermove", this.onSelectMove);
+            document.addEventListener("pointermove", this.onSelectMove);
+            document.removeEventListener("pointerup", this.onSelectEnd);
+            document.addEventListener("pointerup", this.onSelectEnd);
+        }
+    }
+    @action
+    onSelectMove = (e: PointerEvent): void => {
+        if (this._marqueeing && this._mainCont.current) {
+            // transform positions and find the width and height to set the marquee to
+            const boundingRect = this._mainCont.current.getBoundingClientRect();
+            const curX = (e.clientX - boundingRect.left) / boundingRect.width * (this.Document._nativeWidth || 1);
+            const curY = (e.clientY - boundingRect.top) / boundingRect.height * (this.Document._nativeHeight || 1);
+            this._marqueeWidth = curX - this._startX;
+            this._marqueeHeight = curY - 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);
+            e.stopPropagation();
+            e.preventDefault();
+        }
+        else if (e.target && (e.target as any).parentElement === this._mainCont.current) {
+            e.stopPropagation();
+        }
+    }
+
+    @action
+    onSelectEnd = (e: PointerEvent): void => {
+        clearStyleSheetRules(WebBox._annotationStyle);
+        this._savedAnnotations.clear();
+        if (this._marqueeWidth > 10 || this._marqueeHeight > 10) {
+            const marquees = this._mainCont.current!.getElementsByClassName("pdfViewerDash-dragAnnotationBox");
+            if (marquees?.length) { // copy the marquee and convert it to a permanent annotation.
+                const style = (marquees[0] as HTMLDivElement).style;
+                const copy = document.createElement("div");
+                copy.style.left = style.left;
+                copy.style.top = style.top;
+                copy.style.width = style.width;
+                copy.style.height = style.height;
+                copy.style.border = style.border;
+                copy.style.opacity = style.opacity;
+                (copy as any).marqueeing = true;
+                copy.className = "webBox-annotationBox";
+                this.createAnnotation(copy, 0);
+            }
+
+            if (!e.ctrlKey) {
+                PDFMenu.Instance.Marquee = { left: this._marqueeX, top: this._marqueeY, width: this._marqueeWidth, height: this._marqueeHeight };
+            }
+            PDFMenu.Instance.jumpTo(e.clientX, e.clientY);
+        }
+        //this._marqueeing = false;
+
+        if (PDFMenu.Instance.Highlighting) {// when highlighter has been toggled when menu is pinned, we auto-highlight immediately on mouse up
+            this.highlight("rgba(245, 230, 95, 0.616)");  // yellowish highlight color for highlighted text (should match PDFMenu's highlight color)
+        }
+        else {
+            PDFMenu.Instance.StartDrag = this.startDrag;
+            PDFMenu.Instance.Highlight = this.highlight;
+        }
+        document.removeEventListener("pointermove", this.onSelectMove);
+        document.removeEventListener("pointerup", this.onSelectEnd);
+    }
+
+    marqueeWidth = () => this._marqueeWidth;
+    marqueeHeight = () => this._marqueeHeight;
+    marqueeX = () => this._marqueeX;
+    marqueeY = () => this._marqueeY;
+    marqueeing = () => this._marqueeing;
     scrollXf = () => this.props.ScreenToLocalTransform().translate(NumCast(this.layoutDoc._scrollLeft), NumCast(this.layoutDoc._scrollTop));
     render() {
-        return (<div className="webBox" style={{ width: Number.isFinite(this.props.ContentScaling()) ? `${Math.max(100, 100 / this.props.ContentScaling())}% ` : "100%" }}>
+        return (<div className="webBox" ref={this._mainCont} >
             <div className={`webBox-container`}
                 style={{
                     transform: `scale(${this.props.ContentScaling()})`,
@@ -458,8 +671,12 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps, WebDocum
                 <base target="_blank" />
                 {this.content}
                 <div className={"webBox-outerContent"} ref={this._outerRef}
-                    style={{ pointerEvents: this.layoutDoc.isAnnotating && !this.layoutDoc.isBackground ? "all" : "none" }}
+                    style={{
+                        width: Number.isFinite(this.props.ContentScaling()) ? `${100 / this.props.ContentScaling()}% ` : "100%",
+                        pointerEvents: this.layoutDoc.isAnnotating && !this.layoutDoc.isBackground ? "all" : "none"
+                    }}
                     onWheel={e => e.stopPropagation()}
+                    onPointerDown={this.onMarqueeDown}
                     onScroll={e => {
                         const iframe = this._iframeRef?.current?.contentDocument;
                         const outerFrame = this._outerRef.current;
@@ -473,7 +690,10 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps, WebDocum
                         }
                         //this._outerRef.current!.scrollTop !== this._scrollTop && (this._outerRef.current!.scrollTop = this._scrollTop)
                     }}>
-                    <div className={"webBox-innerContent"} style={{ height: NumCast(this.layoutDoc.scrollHeight), width: 4000 }}>
+                    <div className={"webBox-innerContent"} style={{
+                        height: NumCast(this.layoutDoc.scrollHeight),
+                        pointerEvents: this.layoutDoc.isBackground ? "none" : undefined
+                    }}>
                         <CollectionFreeFormView {...this.props}
                             PanelHeight={this.props.PanelHeight}
                             PanelWidth={this.props.PanelWidth}
@@ -499,6 +719,8 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps, WebDocum
                         </CollectionFreeFormView>
                     </div>
                 </div>
+                {this.annotationLayer}
+                <PdfViewerMarquee isMarqueeing={this.marqueeing} width={this.marqueeWidth} height={this.marqueeHeight} x={this.marqueeX} y={this.marqueeY} />
             </div >
         </div>);
     }
-- 
cgit v1.2.3-70-g09d2

