import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { action, computed, IReactionDisposer, observable, ObservableMap, reaction, runInAction, trace } from 'mobx'; import { observer } from 'mobx-react'; import * as WebRequest from 'web-request'; import { Doc, DocListCast, Field, Opt } from '../../../fields/Doc'; 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, NumCast, StrCast, WebCast } from '../../../fields/Types'; import { ImageField, WebField } from '../../../fields/URLField'; import { TraceMobx } from '../../../fields/util'; import { addStyleSheet, addStyleSheetRule, clearStyleSheetRules, emptyFunction, getWordAtPoint, lightOrDark, returnFalse, returnOne, returnZero, setupMoveUpEvents, smoothScroll, Utils } from '../../../Utils'; import { Docs, DocUtils } from '../../documents/Documents'; import { DocumentManager } from '../../util/DocumentManager'; 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, doc: Opt) => void); private _mainCont: React.RefObject = React.createRef(); private _outerRef: React.RefObject = React.createRef(); private _marqueeref = 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 _getAnchor: (savedAnnotations: Opt>, addAsAnnotation: boolean) => Opt = () => undefined; @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 _marqueeing: number[] | undefined; get marqueeing() { return this._marqueeing; } set marqueeing(val) { val && this._marqueeref.current?.onInitiateSelection(val); !val && this._marqueeref.current?.onTerminateSelection(); this._marqueeing = val; } @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(NumCast(this.layoutDoc._height), this._scrollHeight); } @computed get allAnnotations() { return DocListCast(this.dataDoc[this.annotationKey]); } @computed get inlineTextAnnotations() { return this.allAnnotations.filter(a => a.text_inlineAnnotations); } @computed get webField() { return Cast(this.Document[this.props.fieldKey], WebField)?.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); }); } try { if (clear) { this._iframe?.contentWindow?.getSelection()?.empty(); } if (searchString) { (this._iframe?.contentWindow as any)?.find(searchString, false, bwd, true); } } catch (e) { console.log('WebBox search error', e); } 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.Document.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.Document.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 ? this._urlHash + '_' : '') + 'annotations'; // 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) if (this._url) { const reqdFuncs: { [key: string]: string } = {}; 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.dataDoc.data), url => this.submitURL(false, false) ); this._disposers.titling = reaction( () => StrCast(this.Document.title), url => { url.startsWith('www') && this.setData('http://' + url); url.startsWith('http') && this.setData(url); } ); 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 = NumCast(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) { if (this.dataDoc[this.props.fieldKey] instanceof HtmlField) this._mainCont.current.style.transform = `rotate(${NumCast(this.props.DocumentView!().screenToLocalTransform().RotateDeg)}deg)`; const clientRects = selRange.getClientRects(); for (let i = 0; i < clientRects.length; i++) { const rect = clientRects.item(i); const mainrect = this._url ? { translateX: 0, translateY: 0, scale: 1 } : Utils.GetScreenTransform(this._mainCont.current); if (rect && rect.width !== this._mainCont.current.clientWidth) { const annoBox = document.createElement('div'); annoBox.className = 'marqueeAnnotator-annotationBox'; const scale = this._url ? 1 : this.props.ScreenToLocalTransform().Scale; // transforms the positions from screen onto the pdf div annoBox.style.top = ((rect.top - mainrect.translateY) * scale + (this._url ? this._mainCont.current.scrollTop : NumCast(this.layoutDoc.layout_scrollTop))).toString(); annoBox.style.left = ((rect.left - mainrect.translateX) * scale).toString(); annoBox.style.width = (rect.width * scale).toString(); annoBox.style.height = (rect.height * scale).toString(); this._annotationLayer.current && MarqueeAnnotator.previewNewAnnotation(this._savedAnnotations, this._annotationLayer.current, annoBox, 1); } } this._mainCont.current.style.transform = ''; } this._selectionContent = selRange?.cloneContents(); this._selectionText = this._selectionContent?.textContent || ''; // clear selection this._textAnnotationCreator = undefined; if (sel.empty) sel.empty(); // Chrome else if (sel.removeAllRanges) sel.removeAllRanges(); // Firefox return this._savedAnnotations; }; focus = (anchor: Doc, options: DocFocusOptions) => { if (anchor !== this.Document && this._outerRef.current) { const windowHeight = this.props.PanelHeight() / (this.props.NativeDimScaling?.() || 1); const scrollTo = Utils.scrollIntoView(NumCast(anchor.y), NumCast(anchor._height), NumCast(this.layoutDoc._layout_scrollTop), windowHeight, windowHeight * 0.1, Math.max(NumCast(anchor.y) + NumCast(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.Document)) return new Promise>(res => res(this.props.DocumentView?.())); if (this.Document.layout_fieldKey === 'layout_icon') this.props.DocumentView?.().iconify(); const webUrl = WebCast(doc.config_data)?.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 visibleAnchor = this._getAnchor(this._savedAnnotations, true); const anchor = visibleAnchor ?? Docs.Create.ConfigDocument({ title: StrCast(this.Document.title + ' ' + this.layoutDoc._layout_scrollTop), y: NumCast(this.layoutDoc._layout_scrollTop), annotationOn: this.Document, }); PresBox.pinDocView(anchor, { pinDocLayout: pinProps?.pinDocLayout, pinData: { ...(pinProps?.pinData ?? {}), scrollable: pinProps?.pinData ? true : false, pannable: true } }, this.Document); anchor.text = ele?.textContent ?? ''; anchor.text_html = ele?.innerHTML ?? this._selectionText; addAsAnnotation && this.addDocumentWrapper(anchor); return anchor; }; _textAnnotationCreator: (() => ObservableMap) | undefined; savedAnnotationsCreator: () => ObservableMap = () => this._textAnnotationCreator?.() || this._savedAnnotations; @action iframeUp = (e: PointerEvent) => { this._getAnchor = AnchorMenu.Instance?.GetAnchor; // need to save AnchorMenu's getAnchor since a subsequent selection on another doc will overwrite this value 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 ? this._urlHash + '_' : ''}sidebar`); GPTPopup.Instance.addDoc = this.sidebarAddDocument; } } }; @action webClipDown = (e: React.PointerEvent) => { e.stopPropagation(); const sel = window.getSelection(); this._textAnnotationCreator = undefined; if (sel?.empty) sel.empty(); // Chrome else if (sel?.removeAllRanges) sel.removeAllRanges(); // Firefox // bcz: NEED TO unrotate e.clientX and e.clientY const word = getWordAtPoint(e.target, e.clientX, e.clientY); this._setPreviewCursor?.(e.clientX, e.clientY, false, true, this.Document); MarqueeAnnotator.clearAnnotations(this._savedAnnotations); if (!word && !(e.target as any)?.className?.includes('rangeslider') && !(e.target as any)?.onclick && !(e.target as any)?.parentNode?.onclick) { if (e.button !== 2) this.marqueeing = [e.clientX, e.clientY]; e.preventDefault(); } document.addEventListener('pointerup', this.webClipUp); }; @action webClipUp = (e: PointerEvent) => { if (window.getSelection()?.isCollapsed && this._marqueeref.current?.isEmpty) { this.marqueeing = undefined; } document.removeEventListener('pointerup', this.webClipUp); this._getAnchor = AnchorMenu.Instance?.GetAnchor; // need to save AnchorMenu's getAnchor since a subsequent selection on another doc will overwrite this value const sel = window.getSelection(); if (sel && !sel.isCollapsed) { const selRange = sel.getRangeAt(0); this._selectionText = sel.toString(); AnchorMenu.Instance.setSelectedText(sel.toString()); this._textAnnotationCreator = () => this.createTextAnnotation(sel, selRange); (!sel.isCollapsed || this.marqueeing) && AnchorMenu.Instance.jumpTo(e.clientX, e.clientY); // Changing which document to add the annotation to (the currently selected WebBox) GPTPopup.Instance.setSidebarId(`${this.props.fieldKey}_${this._urlHash ? this._urlHash + '_' : ''}sidebar`); GPTPopup.Instance.addDoc = this.sidebarAddDocument; } }; @action iframeDown = (e: PointerEvent) => { this.props.select(false); const locpt = { x: (e.clientX / NumCast(this.Document.nativeWidth)) * this.props.PanelWidth(), y: ((e.clientY - NumCast(this.layoutDoc.layout_scrollTop))/ NumCast(this.Document.nativeHeight)) * this.props.PanelHeight() }; // prettier-ignore const scrclick = this.props.DocumentView?.().props.ScreenToLocalTransform().inverse().transformPoint(locpt.x, locpt.y)!; const scrcent = this.props .DocumentView?.() .props.ScreenToLocalTransform() .inverse() .transformPoint(NumCast(this.Document.width) / 2, NumCast(this.Document.height) / 2)!; const theclickoff = Utils.rotPt(scrclick[0] - scrcent[0], scrclick[1] - scrcent[1], -this.props.ScreenToLocalTransform().Rotate); const theclick = [theclickoff.x + scrcent[0], theclickoff.y + scrcent[1]]; MarqueeAnnotator.clearAnnotations(this._savedAnnotations); const word = getWordAtPoint(e.target, e.clientX, e.clientY); if (!word && !(e.target as any)?.className?.includes('rangeslider') && !(e.target as any)?.onclick && !(e.target as any)?.parentNode?.onclick) { this.marqueeing = theclick; e.preventDefault(); } }; isFirefox = () => 'InstallTrigger' in window; // navigator.userAgent.indexOf("Chrome") !== -1; 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; @observable _warning = 0; @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) { runInAction(() => this._warning++); 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.scrollHeight || 0); if (this._scrollHeight) { this.Document.nativeHeight = Math.min(NumCast(this.Document.nativeHeight), this._scrollHeight); this.layoutDoc.height = Math.min(NumCast(this.layoutDoc._height), (NumCast(this.layoutDoc._width) * NumCast(this.dataDoc.nativeHeight)) / NumCast(this.dataDoc.nativeWidth)); } }; const swidth = Math.max(NumCast(this.layoutDoc.nativeWidth), iframeContent.body.scrollWidth || 0); if (swidth) { const aspectResize = swidth / NumCast(this.Document.nativeWidth); this.Document.nativeWidth = swidth; this.Document.nativeHeight = NumCast(this.Document.nativeHeight) * aspectResize; this.layoutDoc.height = NumCast(this.layoutDoc._height) * aspectResize; } 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) { 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.dataDoc[this.fieldKey + '_future'], listSpec('string'), []); const history = Cast(this.dataDoc[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.dataDoc[this.fieldKey + '_future'], listSpec('string')); const history = Cast(this.dataDoc[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 = (preview?: boolean, dontUpdateIframe?: boolean) => { 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.dataDoc[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.dataDoc[this.fieldKey + '_allowScripts'] ? 'Prevent' : 'Allow') + ' Scripts', event: () => { this.dataDoc[this.fieldKey + '_allowScripts'] = !this.dataDoc[this.fieldKey + '_allowScripts']; if (this._iframe) { runInAction(() => (this._hackHide = true)); setTimeout(action(() => (this._hackHide = false))); } }, icon: 'snowflake', }); funcs.push({ description: (!this.layoutDoc.layout_reflowHorizontal ? 'Force' : 'Prevent') + ' Reflow', event: () => { const nw = !this.layoutDoc.layout_reflowHorizontal ? undefined : Doc.NativeWidth(this.layoutDoc) - this.sidebarWidth() / (this.props.NativeDimScaling?.() || 1); this.layoutDoc.layout_reflowHorizontal = !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' }); } }; /** * This gets called when some other child of the webbox is selected and a pointer down occurs on the webbox. * it's also called for html clippings when you click outside the bounds of the clipping * @param e */ @action onMarqueeDown = (e: React.PointerEvent) => { const sel = this._url ? this._iframe?.contentDocument?.getSelection() : window.document.getSelection(); this._textAnnotationCreator = undefined; if (sel?.empty) sel.empty(); // Chrome else if (sel?.removeAllRanges) sel.removeAllRanges(); // Firefox this.marqueeing = [e.clientX, e.clientY]; 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); return true; }), returnFalse, action(() => { this.marqueeing = undefined; MarqueeAnnotator.clearAnnotations(this._savedAnnotations); }), false ); } else { this.marqueeing = undefined; } }; @action finishMarquee = (x?: number, y?: number, e?: PointerEvent) => { this._getAnchor = AnchorMenu.Instance?.GetAnchor; this.marqueeing = undefined; this._textAnnotationCreator = undefined; const sel = this._url ? this._iframe?.contentDocument?.getSelection() : window.document.getSelection(); if (sel?.empty) sel.empty(); // Chrome else if (sel?.removeAllRanges) sel.removeAllRanges(); // Firefox this._setPreviewCursor?.(x ?? 0, y ?? 0, false, !this._marqueeref.current?.isEmpty, this.Document); if (x !== undefined && y !== undefined) { ContextMenu.Instance.closeMenu(); ContextMenu.Instance.setIgnoreEvents(false); if (e?.button === 2 || e?.altKey) { e?.preventDefault(); e?.stopPropagation(); setTimeout(() => { // if menu comes up right away, the down event can still be active causing a menu item to be selected this.specificContextMenu(undefined as any); this.props.docViewPath().lastElement().docView?.onContextMenu(undefined, x, y); }); } } }; @observable lighttext = false; @computed get urlContent() { if (this.props.ScreenToLocalTransform().Scale > 25) return
; 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.dataDoc[this.props.fieldKey]; if (field instanceof HtmlField) { return ( { if (r) { this._scrollHeight = Number(getComputedStyle(r).height.replace('px', '')); this.lighttext = Array.from(r.children).some((c: any) => c instanceof HTMLElement && lightOrDark(getComputedStyle(c).color) !== Colors.WHITE); } })} contentEditable onPointerDown={this.webClipDown} dangerouslySetInnerHTML={{ __html: field.html }} /> ); } if (field instanceof WebField) { const url = this.layoutDoc[this.fieldKey + '_useCors'] ? Utils.CorsProxy(this._webUrl) : this._webUrl; const scripts = this.dataDoc[this.fieldKey + '_allowScripts'] || this._webUrl.includes('wikipedia.org') || this._webUrl.includes('google.com') || this._webUrl.startsWith('https://bing'); //if (!scripts) console.log('No scripts for: ' + url); return (