/* eslint-disable jsx-a11y/control-has-associated-label */ /* eslint-disable jsx-a11y/no-static-element-interactions */ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { htmlToText } from 'html-to-text'; import { action, computed, IReactionDisposer, makeObservable, observable, ObservableMap, reaction, runInAction } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; import * as WebRequest from 'web-request'; import { addStyleSheet, addStyleSheetRule, clearStyleSheetRules, ClientUtils, DivHeight, getWordAtPoint, lightOrDark, returnFalse, returnOne, returnZero, setupMoveUpEvents, smoothScroll } from '../../../ClientUtils'; import { Doc, DocListCast, Field, FieldType, 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, toList, WebCast } from '../../../fields/Types'; import { ImageField, WebField } from '../../../fields/URLField'; import { TraceMobx } from '../../../fields/util'; import { emptyFunction, stringHash } from '../../../Utils'; import { Docs } from '../../documents/Documents'; import { DocumentType } from '../../documents/DocumentTypes'; import { DocUtils } from '../../documents/DocUtils'; 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, ViewBoxInterface } 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 { PinDocView, PinProps } from '../PinFuncs'; import { SidebarAnnos } from '../SidebarAnnos'; import { StyleProp } from '../StyleProp'; import { DocumentView } from './DocumentView'; import { FieldView, FieldViewProps } from './FieldView'; import { FocusViewOptions } from './FocusViewOptions'; import { LinkInfo } from './LinkDocPreview'; import { OpenWhere } from './OpenWhere'; import './WebBox.scss'; const { CreateImage } = require('./WebBoxRenderer'); @observer export class WebBox extends ViewBoxAnnotatableComponent() implements ViewBoxInterface { 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 = 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 '' + (stringHash(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: FieldViewProps) { super(props); makeObservable(this); 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(); let htmlString = this._iframe.contentDocument && new XMLSerializer().serializeToString(this._iframe.contentDocument); if (!htmlString) { htmlString = await (await fetch(ClientUtils.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((dataUrl: any) => { if (dataUrl.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((error: any) => { console.error('oops, something went wrong!', error); }); }; componentDidMount() { this._props.setContentViewBox?.(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), () => 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, layoutAutoHeight => { if (layoutAutoHeight) { this.layoutDoc._nativeHeight = NumCast(this.Document[this._props.fieldKey + '_nativeHeight']); this._props.setHeight?.(NumCast(this.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) { WebRequest.get(ClientUtils.CorsProxy(this.webField.href)) // .then(result => { result && (this.dataDoc.text = htmlToText(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 } ); } 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.DocumentView!().screenToContentsTransform().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 } : ClientUtils.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.ScreenToLocalBoxXf().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: FocusViewOptions) => { if (anchor !== this.Document && this._outerRef.current) { const windowHeight = this._props.PanelHeight() / (this._props.NativeDimScaling?.() || 1); const scrollTo = ClientUtils.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; } this._initialScroll = scrollTo; } } return undefined; }; @action getView = (doc: Doc /* , options: FocusViewOptions */) => { if (Doc.AreProtosEqual(doc, this.Document)) return new Promise>(res => { res(this.DocumentView?.()); }); if (this.Document.layout_fieldKey === 'layout_icon') this.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 => { DocumentView.addViewRenderedCb(doc, dv => res(dv)); }); }; sidebarAddDocTab = (doc: Doc, where: OpenWhere) => { if (DocListCast(this.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; try { const contents = this._iframe?.contentWindow?.getSelection()?.getRangeAt(0).cloneContents(); if (contents) { ele = document.createElement('div'); ele.append(contents); } } catch (e) { /* empty */ } 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, }); PinDocView(anchor, { pinDocLayout: pinProps?.pinDocLayout, pinData: { ...(pinProps?.pinData ?? {}), scrollable: !!pinProps?.pinData, 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 iframeMove = (e: PointerEvent) => { const theclick = this.props .ScreenToLocalTransform() .inverse() .transformPoint(e.clientX, e.clientY - NumCast(this.layoutDoc.layout_scrollTop)); this._marqueeref.current?.onMove(theclick); }; @action iframeUp = (e: PointerEvent) => { this._iframe?.contentDocument?.removeEventListener('pointermove', this.iframeMove); this.marqueeing = undefined; 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.DocumentView?.()?.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 = ClientUtils.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; } } else { const theclick = this.props .ScreenToLocalTransform() .inverse() .transformPoint(e.clientX, e.clientY - NumCast(this.layoutDoc.layout_scrollTop)); if (!this._marqueeref.current?.isEmpty) this._marqueeref.current?.onEnd(theclick[0], theclick[1]); else { if (!(e.target as any)?.tagName?.includes('INPUT')) this.finishMarquee(theclick[0], theclick[1]); this._getAnchor = AnchorMenu.Instance?.GetAnchor; this.marqueeing = 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(); this.DocumentView?.().onContextMenu(undefined, theclick[0], theclick[1]); }); } } }; @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._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._props.select(false); const theclick = this.props .ScreenToLocalTransform() .inverse() .transformPoint(e.clientX, e.clientY - NumCast(this.layoutDoc.layout_scrollTop)); 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; this._marqueeref.current?.onInitiateSelection(this.marqueeing); this._iframe?.contentDocument?.addEventListener('pointermove', this.iframeMove); 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; } return undefined; } 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 = () => { 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(ClientUtils.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.Document.nativeHeight)) / NumCast(this.Document.nativeWidth)); } }; const swidth = Math.max(NumCast(this.Document.nativeWidth), iframeContent.body.scrollWidth || 0); if (swidth) { const aspectResize = swidth / NumCast(this.Document.nativeWidth, swidth); this.layoutDoc.height = NumCast(this.layoutDoc._height) * aspectResize; this.Document.nativeWidth = swidth; this.Document.nativeHeight = (swidth * NumCast(this.layoutDoc._height)) / NumCast(this.layoutDoc._width); } initHeights(); this._iframetimeout && clearTimeout(this._iframetimeout); this._iframetimeout = setTimeout( action(() => initHeights), 5000 ); iframeContent.addEventListener( 'click', undoBatch( action((e: MouseEvent) => { let eleHref = ''; for (let ele = e.target as any; ele; ele = ele.parentElement) { eleHref = (typeof ele.href === 'string' ? ele.href : ele.href?.baseVal) || ele.parentElement?.href || eleHref; } const origin = this.webField?.origin; if (eleHref && origin) { const batch = UndoManager.StartBatch('webclick'); e.stopPropagation(); setTimeout(() => { this.setData(eleHref.replace(ClientUtils.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 (!LinkInfo.Instance?.LinkInfo && this._outerRef.current && newScrollTop !== this.layoutDoc.thumbScrollTop && (!LightboxView.LightboxDoc || LightboxView.Contains(this.DocumentView?.()))) { 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 undefined; }); 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 undefined; }); return false; }; @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: FieldType | 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 = (): 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() && ![InkTool.Highlighter, InkTool.Pen, InkTool.Write].includes(Doc.ActiveTool)) { setupMoveUpEvents( this, e, action(() => { 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) => { this._getAnchor = AnchorMenu.Instance?.GetAnchor; this.marqueeing = undefined; this._setPreviewCursor?.(x ?? 0, y ?? 0, false, !this._marqueeref.current?.isEmpty, this.Document); }; @observable lighttext = false; @computed get urlContent() { if (this.ScreenToLocalBoxXf().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 = DivHeight(r); this.lighttext = Array.from(r.children).some((c: any) => c instanceof HTMLElement && lightOrDark(getComputedStyle(c).color) !== Colors.WHITE); } })} contentEditable onPointerDown={this.webClipDown} // eslint-disable-next-line react/no-danger dangerouslySetInnerHTML={{ __html: field.html }} /> ); } if (field instanceof WebField) { const url = this.layoutDoc[this.fieldKey + '_useCors'] ? ClientUtils.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 (