diff options
author | Bob Zeleznik <zzzman@gmail.com> | 2020-07-27 00:32:50 -0400 |
---|---|---|
committer | Bob Zeleznik <zzzman@gmail.com> | 2020-07-27 00:32:50 -0400 |
commit | 49fcb0f6613344fb62db618c0b466c6155c20eb0 (patch) | |
tree | b8a682f0f1c7b0c594e31dbd364bcd3184e11e3c | |
parent | a904a5fd2d33fd4783a45b3411c9309cc25ad0fd (diff) |
extended pdfviewer interactions to WebBox's.
-rw-r--r-- | src/client/views/DocComponent.tsx | 2 | ||||
-rw-r--r-- | src/client/views/nodes/WebBox.scss | 21 | ||||
-rw-r--r-- | src/client/views/nodes/WebBox.tsx | 262 | ||||
-rw-r--r-- | src/client/views/pdf/PDFViewer.tsx | 8 |
4 files changed, 265 insertions, 28 deletions
diff --git a/src/client/views/DocComponent.tsx b/src/client/views/DocComponent.tsx index cfabae8a1..9fd406407 100644 --- a/src/client/views/DocComponent.tsx +++ b/src/client/views/DocComponent.tsx @@ -122,7 +122,7 @@ export function ViewBoxAnnotatableComponent<P extends ViewBoxAnnotatableProps, T @action.bound removeDocument(doc: Doc | Doc[]): boolean { const docs = doc instanceof Doc ? [doc] : doc; - docs.map(doc => doc.annotationOn = undefined); + docs.map(doc => doc.isPushpin = doc.annotationOn = undefined); const targetDataDoc = this.dataDoc; const value = DocListCast(targetDataDoc[this.annotationKey]); const toRemove = value.filter(v => docs.includes(v)); diff --git a/src/client/views/nodes/WebBox.scss b/src/client/views/nodes/WebBox.scss index 4b2f3cc1a..f5c8745e7 100644 --- a/src/client/views/nodes/WebBox.scss +++ b/src/client/views/nodes/WebBox.scss @@ -5,6 +5,24 @@ height:100%; position: relative; display: flex; + + .pdfViewerDash-dragAnnotationBox { + position:absolute; + background-color: transparent; + opacity: 0.1; + } + .webBox-annotationLayer { + position: absolute; + transform-origin: left top; + top: 0; + width: 100%; + pointer-events: none; + mix-blend-mode: multiply; // bcz: makes text fuzzy! + } + .webBox-annotationBox { + position: absolute; + background-color: rgba(245, 230, 95, 0.616); + } .webBox-container, .webBox-container-dragging { transform-origin: top left; width: 100%; @@ -46,9 +64,6 @@ top: 0; left: 0; overflow: auto; - .webBox-innerContent { - width:100%; - } } div.webBox-outerContent::-webkit-scrollbar-thumb { display:none; 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>); } diff --git a/src/client/views/pdf/PDFViewer.tsx b/src/client/views/pdf/PDFViewer.tsx index 30c51d9ca..5ed842ab7 100644 --- a/src/client/views/pdf/PDFViewer.tsx +++ b/src/client/views/pdf/PDFViewer.tsx @@ -521,7 +521,7 @@ export class PDFViewer extends ViewBoxAnnotatableComponent<IViewerProps, PdfDocu if (this._marqueeing) { if (this._marqueeWidth > 10 || this._marqueeHeight > 10) { const marquees = this._mainCont.current!.getElementsByClassName("pdfViewerDash-dragAnnotationBox"); - if (marquees && marquees.length) { // copy the marquee and convert it to a permanent annotation. + 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; @@ -544,7 +544,7 @@ export class PDFViewer extends ViewBoxAnnotatableComponent<IViewerProps, PdfDocu } else { const sel = window.getSelection(); - if (sel && sel.type === "Range") { + if (sel?.type === "Range") { const selRange = sel.getRangeAt(0); this.createTextAnnotation(sel, selRange); PDFMenu.Instance.jumpTo(e.clientX, e.clientY); @@ -726,7 +726,7 @@ export class PDFViewer extends ViewBoxAnnotatableComponent<IViewerProps, PdfDocu } } -interface PdfViewerMarqueeProps { +export interface PdfViewerMarqueeProps { isMarqueeing: () => boolean; width: () => number; height: () => number; @@ -735,7 +735,7 @@ interface PdfViewerMarqueeProps { } @observer -class PdfViewerMarquee extends React.Component<PdfViewerMarqueeProps> { +export class PdfViewerMarquee extends React.Component<PdfViewerMarqueeProps> { render() { return !this.props.isMarqueeing() ? (null) : <div className="pdfViewerDash-dragAnnotationBox" style={{ |