import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { action, computed, IReactionDisposer, observable, ObservableMap, reaction, runInAction } from 'mobx'; import { observer } from 'mobx-react'; import * as WebRequest from 'web-request'; import { Doc, DocListCast, Field, Opt } from '../../../fields/Doc'; import { Height, Width } from '../../../fields/DocSymbols'; import { Id } from '../../../fields/FieldSymbols'; import { HtmlField } from '../../../fields/HtmlField'; import { InkTool } from '../../../fields/InkField'; import { List } from '../../../fields/List'; import { RefField } from '../../../fields/RefField'; import { listSpec } from '../../../fields/Schema'; import { Cast, ImageCast, NumCast, StrCast, WebCast } from '../../../fields/Types'; import { ImageField, WebField } from '../../../fields/URLField'; import { TraceMobx } from '../../../fields/util'; import { addStyleSheet, addStyleSheetRule, clearStyleSheetRules, emptyFunction, getWordAtPoint, returnFalse, returnOne, returnZero, setupMoveUpEvents, smoothScroll, Utils } from '../../../Utils'; import { Docs, DocUtils } from '../../documents/Documents'; import { DocumentManager } from '../../util/DocumentManager'; import { DragManager } from '../../util/DragManager'; import { ScriptingGlobals } from '../../util/ScriptingGlobals'; import { SnappingManager } from '../../util/SnappingManager'; import { undoBatch, UndoManager } from '../../util/UndoManager'; import { MarqueeOptionsMenu } from '../collections/collectionFreeForm'; import { CollectionFreeFormView } from '../collections/collectionFreeForm/CollectionFreeFormView'; import { ContextMenu } from '../ContextMenu'; import { ContextMenuProps } from '../ContextMenuItem'; import { ViewBoxAnnotatableComponent, ViewBoxAnnotatableProps } from '../DocComponent'; import { Colors } from '../global/globalEnums'; import { LightboxView } from '../LightboxView'; import { MarqueeAnnotator } from '../MarqueeAnnotator'; import { AnchorMenu } from '../pdf/AnchorMenu'; import { Annotation } from '../pdf/Annotation'; import { GPTPopup } from '../pdf/GPTPopup/GPTPopup'; import { SidebarAnnos } from '../SidebarAnnos'; import { StyleProp } from '../StyleProvider'; import { DocComponentView, DocFocusOptions, DocumentView, DocumentViewProps, OpenWhere } from './DocumentView'; import { FieldView, FieldViewProps } from './FieldView'; import { LinkDocPreview } from './LinkDocPreview'; import { PinProps, PresBox } from './trails'; import './WebBox.scss'; import React = require('react'); const { CreateImage } = require('./WebBoxRenderer'); const _global = (window /* browser */ || global) /* node */ as any; const htmlToText = require('html-to-text'); @observer export class WebBox extends ViewBoxAnnotatableComponent() { public static LayoutString(fieldKey: string) { return FieldView.LayoutString(WebBox, fieldKey); } public static openSidebarWidth = 250; public static sidebarResizerWidth = 5; static webStyleSheet = addStyleSheet(); private _setPreviewCursor: undefined | ((x: number, y: number, drag: boolean, hide: boolean) => void); private _setBrushViewer: undefined | ((view: { width: number; height: number; panX: number; panY: number }) => void); 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(); private _initialScroll: Opt = NumCast(this.layoutDoc.thumbScrollTop, NumCast(this.layoutDoc.layout_scrollTop)); private _sidebarRef = React.createRef(); private _searchRef = React.createRef(); private _searchString = ''; private _scrollTimer: any; private get _getAnchor() { return AnchorMenu.Instance?.GetAnchor; } @observable private _webUrl = ''; // url of the src parameter of the embedded iframe but not necessarily the rendered page - eg, when following a link, the rendered page changes but we don't want the src parameter to also change as that would cause an unnecessary re-render. @observable private _hackHide = false; // apparently changing the value of the 'sandbox' prop doesn't necessarily apply it to the active iframe. so thisforces the ifrmae to be rebuilt when allowScripts is toggled @observable private _searching: boolean = false; @observable private _showSidebar = false; @observable private _webPageHasBeenRendered = false; @observable private _overlayAnnoInfo: Opt; @observable private _marqueeing: number[] | undefined; @observable private _isAnnotating = false; @observable private _iframeClick: HTMLIFrameElement | undefined = undefined; @observable private _iframe: HTMLIFrameElement | null = null; @observable private _savedAnnotations = new ObservableMap(); @observable private _scrollHeight = NumCast(this.layoutDoc.scrollHeight); @computed get _url() { return this.webField?.toString() || ''; } @computed get _urlHash() { return this._url ? WebBox.urlHash(this._url) + '' : ''; } @computed get scrollHeight() { return Math.max(this.layoutDoc[Height](), this._scrollHeight); } @computed get allAnnotations() { return DocListCast(this.dataDoc[this.annotationKey]); } @computed get inlineTextAnnotations() { return this.allAnnotations.filter(a => a.textInlineAnnotations); } @computed get webField() { return Cast(this.rootDoc[this.props.fieldKey], WebField)?.url; } @computed get webThumb() { return ( this.props.thumbShown?.() && ImageCast( this.layoutDoc['thumb-frozen'], ImageCast( this.layoutDoc.thumbScrollTop === this.layoutDoc._layout_scrollTop && this.layoutDoc.thumbNativeWidth === NumCast(this.layoutDoc.nativeWidth) && this.layoutDoc.thumbNativeHeight === NumCast(this.layoutDoc.nativeHeight) ? this.layoutDoc.thumb : undefined ) )?.url ); } constructor(props: any) { super(props); runInAction(() => (this._webUrl = this._url)); // setting the weburl will change the src parameter of the embedded iframe and force a navigation to it. } @action search = (searchString: string, bwd?: boolean, clear: boolean = false) => { if (!this._searching && !clear) { this._searching = true; setTimeout(() => { this._searchRef.current?.focus(); this._searchRef.current?.select(); this._searchRef.current?.setRangeText(searchString); }); } if (clear) { this._iframe?.contentWindow?.getSelection()?.empty(); } if (searchString) { (this._iframe?.contentWindow as any)?.find(searchString, false, bwd, true); } return true; }; @action setScrollPos = (pos: number) => { if (!this._outerRef.current || this._outerRef.current.scrollHeight < pos) { if (this._webPageHasBeenRendered) setTimeout(() => this.setScrollPos(pos), 250); } else { this._outerRef.current.scrollTop = pos; this._initialScroll = undefined; } }; updateThumb = async () => { if (!this._iframe) return; const scrollTop = NumCast(this.layoutDoc._layout_scrollTop); const nativeWidth = NumCast(this.layoutDoc.nativeWidth); const nativeHeight = (nativeWidth * this.props.PanelHeight()) / this.props.PanelWidth(); var htmlString = this._iframe.contentDocument && new XMLSerializer().serializeToString(this._iframe.contentDocument); if (!htmlString) { htmlString = await (await fetch(Utils.CorsProxy(this.webField!.href))).text(); } this.layoutDoc.thumb = undefined; this.rootDoc.thumbLockout = true; // lock to prevent multiple thumb updates. CreateImage(this._webUrl.endsWith('/') ? this._webUrl.substring(0, this._webUrl.length - 1) : this._webUrl, this._iframe.contentDocument?.styleSheets ?? [], htmlString, nativeWidth, nativeHeight, scrollTop) .then((data_url: any) => { if (data_url.includes(' setTimeout( action(() => { this.rootDoc.thumbLockout = false; this.layoutDoc.thumb = new ImageField(returnedfilename); this.layoutDoc.thumbScrollTop = scrollTop; this.layoutDoc.thumbNativeWidth = nativeWidth; this.layoutDoc.thumbNativeHeight = nativeHeight; }), 500 ) ); }) .catch(function (error: any) { console.error('oops, something went wrong!', error); }); }; async componentDidMount() { this.props.setContentView?.(this); // this tells the DocumentView that this WebBox is the "content" of the document. this allows the DocumentView to call WebBox relevant methods to configure the UI (eg, show back/forward buttons) runInAction(() => { this._annotationKeySuffix = () => this._urlHash + '_annotations'; const reqdFuncs: { [key: string]: string } = {}; // bcz: need to make sure that doc.data_annotations points to the currently active web page's annotations (this could/should be when the doc is created) reqdFuncs[this.fieldKey + '_annotations'] = `copyField(this["${this.fieldKey}_"+urlHash(this["${this.fieldKey}"]?.url?.toString())+"_annotations"])`; reqdFuncs[this.fieldKey + '_annotations-setter'] = `this["${this.fieldKey}_"+urlHash(this["${this.fieldKey}"]?.url?.toString())+"_annotations"] = value`; reqdFuncs[this.fieldKey + '_sidebar'] = `copyField(this["${this.fieldKey}_"+urlHash(this["${this.fieldKey}"]?.url?.toString())+"_sidebar"])`; DocUtils.AssignScripts(this.dataDoc, {}, reqdFuncs); }); this._disposers.urlchange = reaction( () => WebCast(this.rootDoc.data), url => { this.submitURL(url.url.href, false, false); } ); this._disposers.layout_autoHeight = reaction( () => this.layoutDoc._layout_autoHeight, layout_autoHeight => { if (layout_autoHeight) { this.layoutDoc._nativeHeight = NumCast(this.props.Document[this.props.fieldKey + '_nativeHeight']); this.props.setHeight?.(NumCast(this.props.Document[this.props.fieldKey + '_nativeHeight']) * (this.props.NativeDimScaling?.() || 1)); } } ); if (this.webField?.href.indexOf('youtube') !== -1) { const youtubeaspect = 400 / 315; const nativeWidth = Doc.NativeWidth(this.layoutDoc); const nativeHeight = Doc.NativeHeight(this.layoutDoc); if (this.webField) { 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[Width]() / youtubeaspect; } } // else it's an HTMLfield } else if (this.webField && !this.dataDoc.text) { const result = await WebRequest.get(Utils.CorsProxy(this.webField.href)); if (result) { this.dataDoc.text = htmlToText.fromString(result.content); } } this._disposers.scrollReaction = reaction( () => NumCast(this.layoutDoc._layout_scrollTop), scrollTop => { 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, 'ease'); }, { fireImmediately: true } ); } @action componentWillUnmount() { this._iframetimeout && clearTimeout(this._iframetimeout); this._iframetimeout = undefined; Object.values(this._disposers).forEach(disposer => disposer?.()); // this._iframe?.removeEventListener('wheel', this.iframeWheel, true); // this._iframe?.contentDocument?.removeEventListener("pointerup", this.iframeUp); } private _selectionText: string = ''; private _selectionContent: DocumentFragment | undefined; selectionText = () => this._selectionText; selectionContent = () => this._selectionContent; @action createTextAnnotation = (sel: Selection, selRange: Range | undefined) => { if (this._mainCont.current && selRange) { 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 || ""; this._selectionContent = selRange?.cloneContents(); this._selectionText = this._selectionContent?.textContent || ''; // clear selection if (sel.empty) sel.empty(); // Chrome else if (sel.removeAllRanges) sel.removeAllRanges(); // Firefox return this._savedAnnotations; }; setBrushViewer = (func?: (view: { width: number; height: number; panX: number; panY: number }) => void) => (this._setBrushViewer = func); brushView = (view: { width: number; height: number; panX: number; panY: number }) => this._setBrushViewer?.(view); focus = (anchor: Doc, options: DocFocusOptions) => { if (anchor !== this.rootDoc && this._outerRef.current) { const windowHeight = this.props.PanelHeight() / (this.props.NativeDimScaling?.() || 1); const scrollTo = Utils.scrollIntoView(NumCast(anchor.y), anchor[Height](), NumCast(this.layoutDoc._layout_scrollTop), windowHeight, windowHeight * 0.1, Math.max(NumCast(anchor.y) + anchor[Height](), this._scrollHeight)); if (scrollTo !== undefined) { if (this._initialScroll === undefined) { const focusTime = options.zoomTime ?? 500; this.goTo(scrollTo, focusTime, options.easeFunc); return focusTime; } else { this._initialScroll = scrollTo; } } } }; @action getView = (doc: Doc) => { if (Doc.AreProtosEqual(doc, this.rootDoc)) return new Promise>(res => res(this.props.DocumentView?.())); if (this.rootDoc.layout_fieldKey === 'layout_icon') this.props.DocumentView?.().iconify(); const webUrl = WebCast(doc.presData)?.url; if (this._url && webUrl && webUrl.href !== this._url) this.setData(webUrl.href); if (this._sidebarRef?.current?.makeDocUnfiltered(doc) && !this.SidebarShown) this.toggleSidebar(false); return new Promise>(res => DocumentManager.Instance.AddViewRenderedCb(doc, dv => res(dv))); }; sidebarAddDocTab = (doc: Doc, where: OpenWhere) => { if (DocListCast(this.props.Document[this.props.fieldKey + '_sidebar']).includes(doc) && !this.SidebarShown) { this.toggleSidebar(false); return true; } return this.props.addDocTab(doc, where); }; getAnchor = (addAsAnnotation: boolean, pinProps?: PinProps) => { let ele: Opt = undefined; try { const contents = this._iframe?.contentWindow?.getSelection()?.getRangeAt(0).cloneContents(); if (contents) { ele = document.createElement('div'); ele.append(contents); } } catch (e) {} const anchor = this._getAnchor(this._savedAnnotations, false) ?? Docs.Create.WebConfigDocument({ title: StrCast(this.rootDoc.title + ' ' + this.layoutDoc._layout_scrollTop), y: NumCast(this.layoutDoc._layout_scrollTop), annotationOn: this.rootDoc, }); PresBox.pinDocView(anchor, { pinDocLayout: pinProps?.pinDocLayout, pinData: { ...(pinProps?.pinData ?? {}), scrollable: pinProps?.pinData ? true : false, pannable: true } }, this.rootDoc); anchor.text = ele?.textContent ?? ''; anchor.textHtml = ele?.innerHTML; //addAsAnnotation && this.addDocumentWrapper(anchor); return anchor; }; _textAnnotationCreator: (() => ObservableMap) | undefined; savedAnnotationsCreator: () => ObservableMap = () => this._textAnnotationCreator?.() || this._savedAnnotations; @action iframeUp = (e: PointerEvent) => { this._textAnnotationCreator = undefined; this.props.docViewPath().lastElement()?.docView?.cleanupPointerEvents(); // pointerup events aren't generated on containing document view, so we have to invoke it here. if (this._iframe?.contentWindow && this._iframe.contentDocument && !this._iframe.contentWindow.getSelection()?.isCollapsed) { const mainContBounds = Utils.GetScreenTransform(this._mainCont.current!); const scale = (this.props.NativeDimScaling?.() || 1) * mainContBounds.scale; const sel = this._iframe.contentWindow.getSelection(); if (sel) { this._selectionText = sel.toString(); AnchorMenu.Instance.setSelectedText(sel.toString()); this._textAnnotationCreator = () => this.createTextAnnotation(sel, !sel.isCollapsed ? sel.getRangeAt(0) : undefined); AnchorMenu.Instance.jumpTo(e.clientX * scale + mainContBounds.translateX, e.clientY * scale + mainContBounds.translateY - NumCast(this.layoutDoc._layout_scrollTop) * scale); // Changing which document to add the annotation to (the currently selected WebBox) GPTPopup.Instance.setSidebarId(`${this.props.fieldKey}_${this._urlHash}_sidebar`); GPTPopup.Instance.addDoc = this.sidebarAddDocument; } } }; @action iframeDown = (e: PointerEvent) => { const sel = this._iframe?.contentWindow?.getSelection?.(); const mainContBounds = Utils.GetScreenTransform(this._mainCont.current!); const scale = (this.props.NativeDimScaling?.() || 1) * mainContBounds.scale; const word = getWordAtPoint(e.target, e.clientX, e.clientY); this._setPreviewCursor?.(e.clientX, e.clientY, false, true); MarqueeAnnotator.clearAnnotations(this._savedAnnotations); e.button !== 2 && (this._marqueeing = [e.clientX * scale + mainContBounds.translateX, e.clientY * scale + mainContBounds.translateY - NumCast(this.layoutDoc._layout_scrollTop) * scale]); if (word || ((e.target as any) || '').className.includes('rangeslider') || (e.target as any)?.onclick || (e.target as any)?.parentNode?.onclick) { 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._iframeClick = this._iframe ?? undefined; this._isAnnotating = true; this.props.select(false); e.stopPropagation(); e.preventDefault(); } // bcz: hack - iframe grabs all events which messes up how we handle contextMenus. So this super naively simulates the event stack to get the specific menu items and the doc view menu items. if (e.button === 2 || (e.button === 0 && e.altKey)) { e.preventDefault(); //e.stopPropagation(); ContextMenu.Instance.closeMenu(); ContextMenu.Instance.setIgnoreEvents(true); } }; isFirefox = () => { return 'InstallTrigger' in window; // navigator.userAgent.indexOf("Chrome") !== -1; }; iframeClick = () => this._iframeClick; iframeScaling = () => 1 / this.props.ScreenToLocalTransform().Scale; addWebStyleSheet(document: any, styleType: string = 'text/css') { if (document) { const style = document.createElement('style'); style.type = styleType; const sheets = document.head.appendChild(style); return (sheets as any).sheet; } } addWebStyleSheetRule(sheet: any, selector: any, css: any, selectorPrefix = '.') { const propText = typeof css === 'string' ? css : Object.keys(css) .map(p => p + ':' + (p === 'content' ? "'" + css[p] + "'" : css[p])) .join(';'); return sheet?.insertRule(selectorPrefix + selector + '{' + propText + '}', sheet.cssRules.length); } _iframetimeout: any = undefined; @action iframeLoaded = (e: any) => { const iframe = this._iframe; if (this._initialScroll !== undefined) { this.setScrollPos(this._initialScroll); } this._scrollHeight = this._iframe?.contentDocument?.body?.scrollHeight ?? 0; this.addWebStyleSheetRule(this.addWebStyleSheet(this._iframe?.contentDocument), '::selection', { color: 'white', background: 'orange' }, ''); let href: Opt; try { href = iframe?.contentWindow?.location.href; } catch (e) { href = undefined; } let requrlraw = decodeURIComponent(href?.replace(Utils.prepend('') + '/corsProxy/', '') ?? this._url.toString()); if (requrlraw !== this._url.toString()) { if (requrlraw.match(/q=.*&/)?.length && this._url.toString().match(/q=.*&/)?.length) { const matches = requrlraw.match(/[^a-zA-z]q=[^&]*/g); const newsearch = matches?.lastElement()!; if (matches) { requrlraw = requrlraw.substring(0, requrlraw.indexOf(newsearch)); for (let i = 1; i < Array.from(matches)?.length; i++) { requrlraw = requrlraw.replace(matches[i], ''); } } requrlraw = requrlraw .replace(/q=[^&]*/, newsearch.substring(1)) .replace('search&', 'search?') .replace('?gbv=1', ''); } this.setData(requrlraw); } const iframeContent = iframe?.contentDocument; if (iframeContent) { iframeContent.addEventListener('pointerup', this.iframeUp); iframeContent.addEventListener('pointerdown', this.iframeDown); // iframeContent.addEventListener( // 'wheel', // e => { // e.ctrlKey && e.preventDefault(); // }, // { passive: false } // ); const initHeights = () => { this._scrollHeight = Math.max(this._scrollHeight, (iframeContent.body.children[0] as any)?.scrollHeight || 0); if (this._scrollHeight) { this.rootDoc.nativeHeight = Math.min(NumCast(this.rootDoc.nativeHeight), this._scrollHeight); this.layoutDoc.height = Math.min(this.layoutDoc[Height](), (this.layoutDoc[Width]() * NumCast(this.rootDoc.nativeHeight)) / NumCast(this.rootDoc.nativeWidth)); } }; initHeights(); this._iframetimeout && clearTimeout(this._iframetimeout); this._iframetimeout = setTimeout( action(() => initHeights), 5000 ); iframeContent.addEventListener( 'click', undoBatch( action((e: MouseEvent) => { let href = ''; for (let ele = e.target as any; ele; ele = ele.parentElement) { href = (typeof ele.href === 'string' ? ele.href : ele.href?.baseVal) || ele.parentElement?.href || href; } const origin = this.webField?.origin; if (href && origin) { const batch = UndoManager.StartBatch('webclick'); e.stopPropagation(); setTimeout(() => { this.setData(href.replace(Utils.prepend(''), origin)); batch.end(); }); if (this._outerRef.current) { this._outerRef.current.scrollTop = NumCast(this.layoutDoc._layout_scrollTop); this._outerRef.current.scrollLeft = 0; } } }) ) ); iframe.contentDocument.addEventListener('wheel', this.iframeWheel, { passive: false }); } }; @action iframeWheel = (e: WheelEvent) => { if (!this._scrollTimer) { addStyleSheetRule(WebBox.webStyleSheet, 'webBox-iframe', { 'pointer-events': 'none' }); this._scrollTimer = setTimeout(() => { this._scrollTimer = undefined; clearStyleSheetRules(WebBox.webStyleSheet); }, 250); // this turns events off on the iframe which allows scrolling to change direction smoothly } if (e.ctrlKey) { if (this._innerCollectionView) { this._innerCollectionView.zoom(e.screenX, e.screenY, e.deltaY); const offset = e.clientY - NumCast(this.layoutDoc._layout_scrollTop); this.layoutDoc.freeform_panY = offset - offset / NumCast(this.layoutDoc._freeform_scale) + NumCast(this.layoutDoc._layout_scrollTop) - NumCast(this.layoutDoc._layout_scrollTop) / NumCast(this.layoutDoc._freeform_scale); } e.preventDefault(); } }; @action setDashScrollTop = (scrollTop: number, timeout: number = 250) => { const iframeHeight = Math.max(scrollTop, this._scrollHeight - this.panelHeight()); if (this._scrollTimer) { clearTimeout(this._scrollTimer); clearStyleSheetRules(WebBox.webStyleSheet); } addStyleSheetRule(WebBox.webStyleSheet, 'webBox-iframe', { 'pointer-events': 'none' }); this._scrollTimer = setTimeout(() => { clearStyleSheetRules(WebBox.webStyleSheet); this._scrollTimer = undefined; const newScrollTop = scrollTop > iframeHeight ? iframeHeight : scrollTop; if (!LinkDocPreview.LinkInfo && this._outerRef.current && newScrollTop !== this.layoutDoc.thumbScrollTop && (!LightboxView.LightboxDoc || LightboxView.IsLightboxDocView(this.props.docViewPath()))) { this.layoutDoc.thumb = undefined; this.layoutDoc.thumbScrollTop = undefined; this.layoutDoc.thumbNativeWidth = undefined; this.layoutDoc.thumbNativeHeight = undefined; this.layoutDoc.layout_scrollTop = this._outerRef.current.scrollTop = newScrollTop; } else if (this._outerRef.current) this._outerRef.current.scrollTop = newScrollTop; }, timeout); }; goTo = (scrollTop: number, duration: number, easeFunc: 'linear' | 'ease' | undefined) => { if (this._outerRef.current) { const iframeHeight = Math.max(scrollTop, this._scrollHeight - this.panelHeight()); if (duration) { smoothScroll(duration, [this._outerRef.current], scrollTop, easeFunc); this.setDashScrollTop(scrollTop, duration); } else { this.setDashScrollTop(scrollTop); } } else this._initialScroll = scrollTop; }; forward = (checkAvailable?: boolean) => { const future = Cast(this.rootDoc[this.fieldKey + '_future'], listSpec('string'), []); const history = Cast(this.rootDoc[this.fieldKey + '_history'], listSpec('string'), []); if (checkAvailable) return future.length; runInAction(() => { if (future.length) { const curUrl = this._url; this.dataDoc[this.fieldKey + '_history'] = new List([...history, this._url]); this.dataDoc[this.fieldKey] = new WebField(new URL(future.pop()!)); this._scrollHeight = 0; if (this._webUrl === this._url) { this._webUrl = curUrl; setTimeout(action(() => (this._webUrl = this._url))); } else { this._webUrl = this._url; } return true; } }); return false; }; back = (checkAvailable?: boolean) => { const future = Cast(this.rootDoc[this.fieldKey + '_future'], listSpec('string')); const history = Cast(this.rootDoc[this.fieldKey + '_history'], listSpec('string'), []); if (checkAvailable) return history.length; runInAction(() => { if (history.length) { const curUrl = this._url; if (future === undefined) this.dataDoc[this.fieldKey + '_future'] = new List([this._url]); else this.dataDoc[this.fieldKey + '_future'] = new List([...future, this._url]); this.dataDoc[this.fieldKey] = new WebField(new URL(history.pop()!)); this._scrollHeight = 0; if (this._webUrl === this._url) { this._webUrl = curUrl; setTimeout(action(() => (this._webUrl = this._url))); } else { this._webUrl = this._url; } return true; } }); return false; }; static urlHash = (s: string) => { const split = s.split(''); return Math.abs( split.reduce((a: any, b: any) => { a = (a << 5) - a + b.charCodeAt(0); return a & a; }, 0) ); }; @action submitURL = (newUrl?: string, preview?: boolean, dontUpdateIframe?: boolean) => { if (!newUrl) return; if (!newUrl.startsWith('http')) newUrl = 'http://' + newUrl; try { if (!preview) { if (this._webPageHasBeenRendered) { this.layoutDoc.thumb = undefined; this.layoutDoc.thumbScrollTop = undefined; this.layoutDoc.thumbNativeWidth = undefined; this.layoutDoc.thumbNativeHeight = undefined; } } if (!preview) { if (!dontUpdateIframe) { this._webUrl = this._url; } } } catch (e) { console.log('WebBox URL error:' + this._url); } return true; }; 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.setData(newurl); e.stopPropagation(); }; @action setData = (data: Field | Promise) => { if (!(typeof data === 'string') && !(data instanceof WebField)) return false; if (Field.toString(data) === this._url) return false; this._scrollHeight = 0; const oldUrl = this._url; const history = Cast(this.rootDoc[this.fieldKey + '_history'], listSpec('string'), []); const weburl = new WebField(Field.toString(data)); this.dataDoc[this.fieldKey + '_future'] = new List([]); this.dataDoc[this.fieldKey + '_history'] = new List([...(history || []), oldUrl]); this.dataDoc[this.fieldKey] = weburl; return true; }; onWebUrlValueKeyDown = (e: React.KeyboardEvent) => { if (e.key === 'Enter') this.setData(this._keyInput.current!.value); e.stopPropagation(); }; specificContextMenu = (e: React.MouseEvent | PointerEvent): void => { const cm = ContextMenu.Instance; const funcs: ContextMenuProps[] = []; if (!cm.findByDescription('Options...')) { !Doc.noviceMode && funcs.push({ description: (this.layoutDoc[this.fieldKey + '_useCors'] ? "Don't Use" : 'Use') + ' Cors', event: () => (this.layoutDoc[this.fieldKey + '_useCors'] = !this.layoutDoc[this.fieldKey + '_useCors']), icon: 'snowflake' }); funcs.push({ description: (this.layoutDoc.allowScripts ? 'Prevent' : 'Allow') + ' Scripts', event: () => { this.layoutDoc.allowScripts = !this.layoutDoc.allowScripts; if (this._iframe) { runInAction(() => (this._hackHide = true)); setTimeout(action(() => (this._hackHide = false))); } }, icon: 'snowflake', }); funcs.push({ description: (!this.layoutDoc.layout_forceReflow ? 'Force' : 'Prevent') + ' Reflow', event: () => { const nw = !this.layoutDoc.layout_forceReflow ? undefined : Doc.NativeWidth(this.layoutDoc) - this.sidebarWidth() / (this.props.NativeDimScaling?.() || 1); this.layoutDoc.layout_forceReflow = !nw; if (nw) { Doc.SetInPlace(this.layoutDoc, this.fieldKey + '_nativeWidth', nw, true); } }, icon: 'snowflake', }); funcs.push({ description: 'Create Thumbnail', event: () => this.updateThumb(), icon: 'portrait' }); cm.addItem({ description: 'Options...', subitems: funcs, icon: 'asterisk' }); } }; @action onMarqueeDown = (e: React.PointerEvent) => { if (!e.altKey && e.button === 0 && this.props.isContentActive(true) && ![InkTool.Highlighter, InkTool.Pen, InkTool.Write].includes(Doc.ActiveTool)) { setupMoveUpEvents( this, e, action(e => { MarqueeAnnotator.clearAnnotations(this._savedAnnotations); this._marqueeing = [e.clientX, e.clientY]; return true; }), returnFalse, () => MarqueeAnnotator.clearAnnotations(this._savedAnnotations), false ); } }; @action finishMarquee = (x?: number, y?: number, e?: PointerEvent) => { this._marqueeing = undefined; this._isAnnotating = false; this._iframeClick = undefined; const sel = this._iframe?.contentDocument?.getSelection(); if (sel?.empty) sel.empty(); // Chrome else if (sel?.removeAllRanges) sel.removeAllRanges(); // Firefox if (x !== undefined && y !== undefined) { this._setPreviewCursor?.(x, y, false, false); ContextMenu.Instance.closeMenu(); ContextMenu.Instance.setIgnoreEvents(false); if (e?.button === 2 || e?.altKey) { this.specificContextMenu(undefined as any); this.props.docViewPath().lastElement().docView?.onContextMenu(undefined, x, y); } } }; @computed get urlContent() { setTimeout( action(() => { if (this._initialScroll === undefined && !this._webPageHasBeenRendered) { this.setScrollPos(NumCast(this.layoutDoc.thumbScrollTop, NumCast(this.layoutDoc.layout_scrollTop))); } this._webPageHasBeenRendered = true; }) ); const field = this.rootDoc[this.props.fieldKey]; if (field instanceof HtmlField) { return e.stopPropagation()} dangerouslySetInnerHTML={{ __html: field.html }} />; } if (field instanceof WebField) { const url = this.layoutDoc[this.fieldKey + '_useCors'] ? Utils.CorsProxy(this._webUrl) : this._webUrl; return (