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 { 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 { List } from "../../../fields/List"; import { listSpec, makeInterface } from "../../../fields/Schema"; import { Cast, NumCast, StrCast } from "../../../fields/Types"; import { WebField } from "../../../fields/URLField"; import { TraceMobx } from "../../../fields/util"; import { addStyleSheet, clearStyleSheetRules, emptyFunction, returnOne, returnZero, Utils, returnTrue } 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"); const htmlToText = require("html-to-text"); type WebDocument = makeInterface<[typeof documentSchema]>; const WebDocument = makeInterface(documentSchema); @observer export class WebBox extends ViewBoxAnnotatableComponent(WebDocument) { private _annotationLayer: React.RefObject = React.createRef(); static _annotationStyle: any = addStyleSheet(); private _mainCont: React.RefObject = 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 = new Dictionary(); private _selectionReactionDisposer?: IReactionDisposer; private _scrollReactionDisposer?: IReactionDisposer; private _moveReactionDisposer?: IReactionDisposer; private _keyInput = React.createRef(); private _longPressSecondsHack?: NodeJS.Timeout; private _outerRef = React.createRef(); private _iframeRef = React.createRef(); private _iframeIndicatorRef = React.createRef(); private _iframeDragRef = React.createRef(); private _setPreviewCursor: undefined | ((x: number, y: number, drag: boolean) => void); iframeLoaded = action((e: any) => { const iframe = this._iframeRef.current; if (iframe && iframe.contentDocument) { iframe.setAttribute("enable-annotation", "true"); iframe.contentDocument.addEventListener('pointerdown', this.iframedown, false); iframe.contentDocument.addEventListener('scroll', this.iframeScrolled, false); this.layoutDoc.scrollHeight = iframe.contentDocument.children?.[0].scrollHeight || 1000; iframe.contentDocument.children[0].scrollTop = NumCast(this.layoutDoc._scrollTop); iframe.contentDocument.children[0].scrollLeft = NumCast(this.layoutDoc._scrollLeft); } this._scrollReactionDisposer?.(); this._scrollReactionDisposer = reaction(() => ({ y: this.layoutDoc._scrollY, x: this.layoutDoc._scrollX }), ({ x, y }) => this.updateScroll(x, y), { fireImmediately: true } ); }); updateScroll = (x: Opt, y: Opt) => { if (y !== undefined) { this._outerRef.current!.scrollTop = y; this.layoutDoc._scrollY = undefined; } if (x !== undefined) { this._outerRef.current!.scrollLeft = x; this.layoutDoc.scrollX = undefined; } } setPreviewCursor = (func?: (x: number, y: number, drag: boolean) => void) => this._setPreviewCursor = func; iframedown = (e: PointerEvent) => { this._setPreviewCursor?.(e.screenX, e.screenY, false); } iframeScrolled = (e: any) => { const scrollTop = e.target?.children?.[0].scrollTop; const scrollLeft = e.target?.children?.[0].scrollLeft; this.layoutDoc._scrollTop = this._outerRef.current!.scrollTop = scrollTop; this.layoutDoc._scrollLeft = this._outerRef.current!.scrollLeft = scrollLeft; } async componentDidMount() { const urlField = Cast(this.dataDoc[this.props.fieldKey], WebField); runInAction(() => this._url = urlField?.url.toString() || ""); this._moveReactionDisposer = reaction(() => this.layoutDoc.x || this.layoutDoc.y, () => this.updateScroll(this.layoutDoc._scrollLeft, this.layoutDoc._scrollTop)); 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); if (field?.url.href.indexOf("youtube") !== -1) { const youtubeaspect = 400 / 315; const nativeWidth = NumCast(this.layoutDoc._nativeWidth); const nativeHeight = NumCast(this.layoutDoc._nativeHeight); if (field) { if (!nativeWidth || !nativeHeight || Math.abs(nativeWidth / nativeHeight - youtubeaspect) > 0.05) { if (!nativeWidth) this.layoutDoc._nativeWidth = 600; this.layoutDoc._nativeHeight = NumCast(this.layoutDoc._nativeWidth) / youtubeaspect; this.layoutDoc._height = NumCast(this.layoutDoc._width) / youtubeaspect; } } // else it's an HTMLfield } else if (field?.url) { const result = await WebRequest.get(Utils.CorsProxy(field.url.href)); if (result) { this.dataDoc.text = htmlToText.fromString(result.content); } } } componentWillUnmount() { this._moveReactionDisposer?.(); this._selectionReactionDisposer?.(); this._scrollReactionDisposer?.(); document.removeEventListener("pointerup", this.onLongPressUp); document.removeEventListener("pointermove", this.onLongPressMove); this._iframeRef.current?.contentDocument?.removeEventListener('pointerdown', this.iframedown); this._iframeRef.current?.contentDocument?.removeEventListener('scroll', this.iframeScrolled); } @action onURLChange = (e: React.ChangeEvent) => { this._url = e.target.value; } onUrlDragover = (e: React.DragEvent) => { e.preventDefault(); } @action onUrlDrop = (e: React.DragEvent) => { const { dataTransfer } = e; const html = dataTransfer.getData("text/html"); const uri = dataTransfer.getData("text/uri-list"); const url = uri || html || this._url; this._url = url.startsWith(window.location.origin) ? url.replace(window.location.origin, this._url.match(/http[s]?:\/\/[^\/]*/)?.[0] || "") : url; this.submitURL(); e.stopPropagation(); } @action forward = () => { const future = Cast(this.dataDoc[this.fieldKey + "-future"], listSpec("string"), null); const history = Cast(this.dataDoc[this.fieldKey + "-history"], listSpec("string"), null); if (future.length) { history.push(this._url); this.dataDoc[this.annotationKey + "-" + this.urlHash(this._url)] = new List(DocListCast(this.dataDoc[this.annotationKey])); this.dataDoc[this.fieldKey] = new WebField(new URL(this._url = future.pop()!)); this.dataDoc[this.annotationKey] = new List(DocListCast(this.dataDoc[this.annotationKey + "-" + this.urlHash(this._url)])); } } @action back = () => { const future = Cast(this.dataDoc[this.fieldKey + "-future"], listSpec("string"), null); const history = Cast(this.dataDoc[this.fieldKey + "-history"], listSpec("string"), null); if (history.length) { if (future === undefined) this.dataDoc[this.fieldKey + "-future"] = new List([this._url]); else future.push(this._url); this.dataDoc[this.annotationKey + "-" + this.urlHash(this._url)] = new List(DocListCast(this.dataDoc[this.annotationKey])); this.dataDoc[this.fieldKey] = new WebField(new URL(this._url = history.pop()!)); this.dataDoc[this.annotationKey] = new List(DocListCast(this.dataDoc[this.annotationKey + "-" + this.urlHash(this._url)])); } } urlHash(s: string) { return s.split('').reduce((a: any, b: any) => { a = ((a << 5) - a) + b.charCodeAt(0); return a & a; }, 0); } @action submitURL = () => { if (!this._url.startsWith("http")) this._url = "http://" + this._url; try { const URLy = new URL(this._url); const future = Cast(this.dataDoc[this.fieldKey + "-future"], listSpec("string"), null); const history = Cast(this.dataDoc[this.fieldKey + "-history"], listSpec("string"), null); const annos = DocListCast(this.dataDoc[this.annotationKey]); const url = Cast(this.dataDoc[this.fieldKey], WebField, null)?.url.toString(); if (url) { if (history === undefined) { this.dataDoc[this.fieldKey + "-history"] = new List([url]); } else { history.push(url); } future && (future.length = 0); this.dataDoc[this.annotationKey + "-" + this.urlHash(url)] = new List(annos); } this.dataDoc[this.fieldKey] = new WebField(URLy); this.dataDoc[this.annotationKey] = new List([]); } catch (e) { console.log("WebBox URL error:" + this._url); } } onValueKeyDown = async (e: React.KeyboardEvent) => { if (e.key === "Enter") { this.submitURL(); } e.stopPropagation(); } toggleAnnotationMode = () => { this.layoutDoc.isAnnotating = !this.layoutDoc.isAnnotating; } urlEditor() { return (
{ this._keyInput.current!.select(); e.stopPropagation(); }} ref={this._keyInput} />
); } @action toggleCollapse = () => { this._collapsed = !this._collapsed; } _ignore = 0; onPreWheel = (e: React.WheelEvent) => { this._ignore = e.timeStamp; } onPrePointer = (e: React.PointerEvent) => { this._ignore = e.timeStamp; } onPostPointer = (e: React.PointerEvent) => { if (this._ignore !== e.timeStamp) { e.stopPropagation(); } } onPostWheel = (e: React.WheelEvent) => { if (this._ignore !== e.timeStamp) { e.stopPropagation(); } } onLongPressDown = (e: React.PointerEvent) => { this._pressX = e.clientX; this._pressY = e.clientY; // find the pressed element in the iframe (currently only works if its an img) let pressedElement: HTMLElement | undefined; let pressedBound: ClientRect | undefined; let selectedText: string = ""; let pressedImg: boolean = false; if (this._iframeRef.current) { const B = this._iframeRef.current.getBoundingClientRect(); const iframeDoc = this._iframeRef.current.contentDocument; if (B && iframeDoc) { // TODO: this only works when scale = 1 as it is currently only inteded for mobile upload const element = iframeDoc.elementFromPoint(this._pressX - B.left, this._pressY - B.top); if (element && element.nodeName === "IMG") { pressedBound = element.getBoundingClientRect(); pressedElement = element.cloneNode(true) as HTMLElement; pressedImg = true; } else { // check if there is selected text const text = iframeDoc.getSelection(); if (text && text.toString().length > 0) { selectedText = text.toString(); // get html of the selected text const range = text.getRangeAt(0); const contents = range.cloneContents(); const div = document.createElement("div"); div.appendChild(contents); pressedElement = div; pressedBound = range.getBoundingClientRect(); } } } } // mark the pressed element if (pressedElement && pressedBound) { if (this._iframeIndicatorRef.current) { this._iframeIndicatorRef.current.style.top = pressedBound.top + "px"; this._iframeIndicatorRef.current.style.left = pressedBound.left + "px"; this._iframeIndicatorRef.current.style.width = pressedBound.width + "px"; this._iframeIndicatorRef.current.style.height = pressedBound.height + "px"; this._iframeIndicatorRef.current.classList.add("active"); } } // start dragging the pressed element if long pressed this._longPressSecondsHack = setTimeout(() => { if (pressedImg && pressedElement && pressedBound) { e.stopPropagation(); e.preventDefault(); if (pressedElement.nodeName === "IMG") { const src = pressedElement.getAttribute("src"); // TODO: may not always work if (src) { const doc = Docs.Create.ImageDocument(src); ImageUtils.ExtractExif(doc); // add clone to div so that dragging ghost is placed properly if (this._iframeDragRef.current) this._iframeDragRef.current.appendChild(pressedElement); const dragData = new DragManager.DocumentDragData([doc]); DragManager.StartDocumentDrag([pressedElement], dragData, this._pressX, this._pressY, { hideSource: true }); } } } else if (selectedText && pressedBound && pressedElement) { e.stopPropagation(); e.preventDefault(); // create doc with the selected text's html const doc = Docs.Create.HtmlDocument(pressedElement.innerHTML); // create dragging ghost with the selected text if (this._iframeDragRef.current) this._iframeDragRef.current.appendChild(pressedElement); // start the drag const dragData = new DragManager.DocumentDragData([doc]); DragManager.StartDocumentDrag([pressedElement], dragData, this._pressX - pressedBound.top, this._pressY - pressedBound.top, { hideSource: true }); } }, 1500); } onLongPressMove = (e: PointerEvent) => { // this._pressX = e.clientX; // this._pressY = e.clientY; } onLongPressUp = (e: PointerEvent) => { if (this._longPressSecondsHack) { clearTimeout(this._longPressSecondsHack); } if (this._iframeIndicatorRef.current) { this._iframeIndicatorRef.current.classList.remove("active"); } if (this._iframeDragRef.current) { while (this._iframeDragRef.current.firstChild) this._iframeDragRef.current.removeChild(this._iframeDragRef.current.firstChild); } } @undoBatch @action toggleNativeDimensions = () => { Doc.toggleNativeDimensions(this.layoutDoc, this.props.ContentScaling(), this.props.NativeWidth(), this.props.NativeHeight()); } specificContextMenu = (e: React.MouseEvent): void => { const cm = ContextMenu.Instance; const funcs: ContextMenuProps[] = []; funcs.push({ description: (this.layoutDoc.UseCors ? "Don't Use" : "Use") + " Cors", event: () => this.layoutDoc.UseCors = !this.layoutDoc.UseCors, icon: "snowflake" }); cm.addItem({ description: "Options...", subitems: funcs, icon: "asterisk" }); } //const href = "https://brown365-my.sharepoint.com/personal/bcz_ad_brown_edu/_layouts/15/Doc.aspx?sourcedoc={31aa3178-4c21-4474-b367-877d0a7135e4}&action=embedview&wdStartOn=1"; @computed get urlContent() { const field = this.dataDoc[this.props.fieldKey]; let view; if (field instanceof HtmlField) { view = ; } else if (field instanceof WebField) { const url = this.layoutDoc.UseCors ? Utils.CorsProxy(field.url.href) : field.url.href; view =