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, HeightSym, Opt, StrListCast, WidthSym } 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 { emptyFunction, OmitKeys, getWordAtPoint, returnOne, returnTrue, returnZero, smoothScroll, Utils } from "../../../Utils"; import { Docs, DocUtils } from "../../documents/Documents"; import { DocumentType } from '../../documents/DocumentTypes'; import { CurrentUserUtils } from "../../util/CurrentUserUtils"; import { undoBatch } from "../../util/UndoManager"; import { CollectionFreeFormView } from "../collections/collectionFreeForm/CollectionFreeFormView"; import { CollectionStackingView } from "../collections/CollectionStackingView"; import { CollectionViewType } from "../collections/CollectionView"; import { ContextMenu } from "../ContextMenu"; import { ContextMenuProps } from "../ContextMenuItem"; import { ViewBoxAnnotatableComponent } from "../DocComponent"; import { DocumentDecorations } from "../DocumentDecorations"; import { MarqueeAnnotator } from "../MarqueeAnnotator"; import { AnchorMenu } from "../pdf/AnchorMenu"; import { Annotation } from "../pdf/Annotation"; import { SearchBox } from "../search/SearchBox"; import { StyleProp } from "../StyleProvider"; import { FieldView, FieldViewProps } from './FieldView'; import { FormattedTextBox } from "./formattedText/FormattedTextBox"; import { LinkDocPreview } from "./LinkDocPreview"; import "./WebBox.scss"; import React = require("react"); import { LightboxView } from "../LightboxView"; import { SnappingManager } from "../../util/SnappingManager"; const htmlToText = require("html-to-text"); type WebDocument = makeInterface<[typeof documentSchema]>; const WebDocument = makeInterface(documentSchema); @observer export class WebBox extends ViewBoxAnnotatableComponent(WebDocument) { public static LayoutString(fieldKey: string) { return FieldView.LayoutString(WebBox, fieldKey); } private _mainCont: React.RefObject = React.createRef(); private _outerRef: React.RefObject = React.createRef(); private _disposers: { [name: string]: IReactionDisposer } = {}; private _annotationLayer: React.RefObject = React.createRef(); private _keyInput = React.createRef(); @observable _scrollTimer: any; @observable private _overlayAnnoInfo: Opt; private _initialScroll: Opt; private _setPreviewCursor: undefined | ((x: number, y: number, drag: boolean) => void); @observable private _marqueeing: number[] | undefined; @observable private _url: string = "hello"; @observable private _isAnnotating = false; @observable private _iframe: HTMLIFrameElement | null = null; @observable private _savedAnnotations: Dictionary = new Dictionary(); @observable private _scrollHeight = 1500; @computed get scrollHeight() { return this._scrollHeight; } @computed get inlineTextAnnotations() { return this.allAnnotations.filter(a => a.textInlineAnnotations); } constructor(props: any) { super(props); if (this.dataDoc[this.fieldKey] instanceof WebField) { Doc.SetNativeWidth(this.dataDoc, Doc.NativeWidth(this.dataDoc) || 850); Doc.SetNativeHeight(this.dataDoc, Doc.NativeHeight(this.dataDoc) || this.Document[HeightSym]() / this.Document[WidthSym]() * 850); } if (this.layoutDoc[this.fieldKey + "-contentWidth"] === undefined) { this.layoutDoc[this.fieldKey + "-contentWidth"] = Doc.NativeWidth(this.layoutDoc); } this._annotationKey = "annotations-" + this.urlHash(this._url); } @action createTextAnnotation = (sel: Selection, selRange: Range) => { if (this._mainCont.current) { const clientRects = selRange.getClientRects(); for (let i = 0; i < clientRects.length; i++) { const rect = clientRects.item(i); if (rect && rect.width !== this._mainCont.current.clientWidth) { const annoBox = document.createElement("div"); annoBox.className = "marqueeAnnotator-annotationBox"; // transforms the positions from screen onto the pdf div annoBox.style.top = (rect.top + this._mainCont.current.scrollTop).toString(); annoBox.style.left = (rect.left).toString(); annoBox.style.width = (rect.width).toString(); annoBox.style.height = (rect.height).toString(); this._annotationLayer.current && MarqueeAnnotator.previewNewAnnotation(this._savedAnnotations, this._annotationLayer.current, annoBox, 1); } } } //this._selectionText = selRange.cloneContents().textContent || ""; // clear selection if (sel.empty) { // Chrome sel.empty(); } else if (sel.removeAllRanges) { // Firefox sel.removeAllRanges(); } } @action iframeUp = (e: PointerEvent) => { if (this._iframe?.contentWindow && this._iframe.contentDocument && !this._iframe.contentWindow.getSelection()?.isCollapsed) { this._iframe.contentDocument.addEventListener("pointerup", this.iframeUp); const mainContBounds = Utils.GetScreenTransform(this._mainCont.current!); const scale = (this.props.scaling?.() || 1) * mainContBounds.scale; const sel = this._iframe.contentWindow.getSelection(); if (sel) { this.createTextAnnotation(sel, sel.getRangeAt(0)) AnchorMenu.Instance.jumpTo(e.clientX * scale + mainContBounds.translateX, e.clientY * scale + mainContBounds.translateY - NumCast(this.layoutDoc._scrollTop) * scale); } } else AnchorMenu.Instance.fadeOut(true); } @action iframeDown = (e: PointerEvent) => { const mainContBounds = Utils.GetScreenTransform(this._mainCont.current!); const scale = (this.props.scaling?.() || 1) * mainContBounds.scale; const word = getWordAtPoint(e.target, e.clientX, e.clientY); this._marqueeing = [e.clientX * scale + mainContBounds.translateX, e.clientY * scale + mainContBounds.translateY - NumCast(this.layoutDoc._scrollTop) * scale]; if (word) { this._iframe?.contentDocument?.addEventListener("pointerup", this.iframeUp); setTimeout(action(() => this._marqueeing = undefined), 100); // bcz: hack .. anchor menu is setup within MarqueeAnnotator so we need to at least create the marqueeAnnotator even though we aren't using it. } else { this._isAnnotating = true; this.props.select(false); e.stopPropagation(); e.preventDefault(); } } @action iframeLoaded = (e: any) => { const iframe = this._iframe; if (iframe?.contentDocument) { iframe?.contentDocument.addEventListener("pointerdown", this.iframeDown); this._scrollHeight = Math.max(this.scrollHeight, iframe?.contentDocument.body.scrollHeight); if (this._initialScroll !== undefined && this._outerRef.current) { this._outerRef.current.scrollTop = this._initialScroll; this._initialScroll = undefined; } iframe.setAttribute("enable-annotation", "true"); iframe.contentDocument.addEventListener("click", undoBatch(action(e => { let href = ""; for (let ele = e.target; ele; ele = ele.parentElement) { href = (typeof (ele.href) === "string" ? ele.href : ele.href?.baseVal) || ele.parentElement?.href || href; } if (href) { this.submitURL(href.replace(Utils.prepend(""), Cast(this.dataDoc[this.fieldKey], WebField, null)?.url.origin)); if (this._outerRef.current) { this._outerRef.current.scrollTop = NumCast(this.layoutDoc._scrollTop); this._outerRef.current.scrollLeft = 0; } } }))); iframe.contentDocument.addEventListener('wheel', this.iframeWheel, false); iframe.contentDocument.addEventListener('scroll', () => !this.active() && this._iframe && (this._iframe.scrollTop = NumCast(this.layoutDoc._scrollTop), false)); } } @action setDashScrollTop = (scrollTop: number, timeout: number = 250) => { const iframeHeight = this._scrollHeight - this.panelHeight(); timeout = scrollTop > iframeHeight ? 0 : timeout; this._scrollTimer && clearTimeout(this._scrollTimer); this._scrollTimer = setTimeout(action(() => { this._scrollTimer = undefined; if (!LinkDocPreview.LinkInfo && this._outerRef.current && (!LightboxView.LightboxDoc || LightboxView.IsLightboxDocView(this.props.docViewPath()))) { if (scrollTop > iframeHeight) this.layoutDoc._scrollTop = this._outerRef.current.scrollTop = iframeHeight; else this.layoutDoc._scrollTop = this._outerRef.current.scrollTop = scrollTop; } }), timeout); } @action iframeWheel = (e: any) => { if (!this._scrollTimer) { this._scrollTimer = setTimeout(action(() => this._scrollTimer = undefined), 250); // this turns events off on the iframe which allows scrolling to change direction smoothly } } onWheel = (e: any) => { e.stopPropagation(); e.preventDefault(); } onScroll = (e: any) => this.setDashScrollTop(this._outerRef.current?.scrollTop || 0); scrollFocus = (doc: Doc, smooth: boolean) => { if (doc !== this.rootDoc && this._outerRef.current) { const scrollTo = doc.type === DocumentType.TEXTANCHOR ? NumCast(doc.y) : Utils.scrollIntoView(NumCast(doc.y), doc[HeightSym](), NumCast(this.layoutDoc._scrollTop), this.props.PanelHeight() / (this.props.scaling?.() || 1)); if (scrollTo !== undefined) { let focusSpeed = smooth ? 500 : 0; this._initialScroll !== undefined && (this._initialScroll = scrollTo); this.goTo(scrollTo, focusSpeed); return focusSpeed; } } this._initialScroll = NumCast(doc.y); return 0; } getAnchor = () => { const anchor = Docs.Create.TextanchorDocument({ title: StrCast(this.rootDoc.title + " " + this.layoutDoc._scrollTop), useLinkSmallAnchor: true, hideLinkButton: true, annotationOn: this.rootDoc, y: NumCast(this.layoutDoc._scrollTop), }); this.addDocument(anchor); return anchor; } async componentDidMount() { this.props.setContentView?.(this); // this tells the DocumentView that this AudioBox is the "content" of the document. this allows the DocumentView to indirectly call getAnchor() on the AudioBox when making a link. const urlField = Cast(this.dataDoc[this.props.fieldKey], WebField); runInAction(() => this._url = urlField?.url.toString() || ""); this._disposers.selection = reaction(() => this.props.isSelected(), selected => !selected && setTimeout(() => { this._savedAnnotations.values().forEach(v => v.forEach(a => a.remove())); this._savedAnnotations.clear(); })); const field = Cast(this.rootDoc[this.props.fieldKey], WebField); if (field?.url.href.indexOf("youtube") !== -1) { const youtubeaspect = 400 / 315; const nativeWidth = Doc.NativeWidth(this.layoutDoc); const nativeHeight = Doc.NativeHeight(this.layoutDoc); if (field) { if (!nativeWidth || !nativeHeight || Math.abs(nativeWidth / nativeHeight - youtubeaspect) > 0.05) { if (!nativeWidth) Doc.SetNativeWidth(this.layoutDoc, 600); Doc.SetNativeHeight(this.layoutDoc, (nativeWidth || 600) / youtubeaspect); this.layoutDoc._height = this.layoutDoc[WidthSym]() / youtubeaspect; } } // else it's an HTMLfield } else if (field?.url && !this.dataDoc.text) { const result = await WebRequest.get(Utils.CorsProxy(field.url.href)); if (result) { this.dataDoc.text = htmlToText.fromString(result.content); } } var quickScroll = true; this._disposers.scrollReaction = reaction(() => NumCast(this.layoutDoc._scrollTop), (scrollTop) => { if (quickScroll) { this._initialScroll = scrollTop; } else { const viewTrans = StrCast(this.Document._viewTransition); const durationMiliStr = viewTrans.match(/([0-9]*)ms/); const durationSecStr = viewTrans.match(/([0-9.]*)s/); const duration = durationMiliStr ? Number(durationMiliStr[1]) : durationSecStr ? Number(durationSecStr[1]) * 1000 : 0; this.goTo(scrollTop, duration); } }, { fireImmediately: true } ); quickScroll = false; } goTo = (scrollTop: number, duration: number) => { if (this._outerRef.current) { const iframeHeight = this._scrollHeight - this.panelHeight(); scrollTop = scrollTop > iframeHeight + 50 ? iframeHeight : scrollTop; if (duration) { smoothScroll(duration, [this._outerRef.current], scrollTop); this.setDashScrollTop(scrollTop, duration); } else { this.setDashScrollTop(scrollTop); } } } componentWillUnmount() { Object.values(this._disposers).forEach(disposer => disposer?.()); this._iframe?.removeEventListener('wheel', this.iframeWheel, true); } @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.fieldKey] = new WebField(new URL(this._url = future.pop()!)); this._annotationKey = "annotations-" + this.urlHash(this._url); return true; } return false; } @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.fieldKey] = new WebField(new URL(this._url = history.pop()!)); this._annotationKey = "annotations-" + this.urlHash(this._url); return true; } return false; } urlHash(s: string) { return s.split('').reduce((a: any, b: any) => { a = ((a << 5) - a) + b.charCodeAt(0); return a & a; }, 0); } @action submitURL = (newUrl: string) => { if (!newUrl.startsWith("http")) newUrl = "http://" + newUrl; try { const future = Cast(this.dataDoc[this.fieldKey + "-future"], listSpec("string"), null); const history = Cast(this.dataDoc[this.fieldKey + "-history"], listSpec("string"), null); 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); } this.layoutDoc._scrollTop = 0; future && (future.length = 0); } this._url = newUrl; this._annotationKey = "annotations-" + this.urlHash(this._url); this.dataDoc[this.fieldKey] = new WebField(new URL(newUrl)); } catch (e) { console.log("WebBox URL error:" + this._url); } return true; } menuControls = () => this.urlEditor; onWebUrlDrop = (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 || ""; const newurl = url.startsWith(window.location.origin) ? url.replace(window.location.origin, this._url?.match(/http[s]?:\/\/[^\/]*/)?.[0] || "") : url; this.submitURL(newurl); e.stopPropagation(); } onWebUrlValueKeyDown = (e: React.KeyboardEvent) => { e.key === "Enter" && this.submitURL(this._keyInput.current!.value); e.stopPropagation(); } @computed get urlEditor() { return (
e.preventDefault()} > e.preventDefault()} onKeyDown={this.onWebUrlValueKeyDown} onClick={(e) => { this._keyInput.current!.select(); e.stopPropagation(); }} ref={this._keyInput} />
); } 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" }); funcs.push({ description: (this.layoutDoc[this.fieldKey + "-contentWidth"] ? "Unfreeze" : "Freeze") + " Content Width", event: () => this.layoutDoc[this.fieldKey + "-contentWidth"] = this.layoutDoc[this.fieldKey + "-contentWidth"] ? undefined : Doc.NativeWidth(this.layoutDoc), icon: "snowflake" }); funcs.push({ description: "Toggle Annotation View ", event: () => this.Document._showSidebar = !this.Document._showSidebar, icon: "expand-arrows-alt" }); cm.addItem({ description: "Options...", subitems: funcs, icon: "asterisk" }); } @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 =