diff options
Diffstat (limited to 'src/client/views/nodes/WebBox.tsx')
-rw-r--r-- | src/client/views/nodes/WebBox.tsx | 273 |
1 files changed, 178 insertions, 95 deletions
diff --git a/src/client/views/nodes/WebBox.tsx b/src/client/views/nodes/WebBox.tsx index 3c4696df3..0f0008700 100644 --- a/src/client/views/nodes/WebBox.tsx +++ b/src/client/views/nodes/WebBox.tsx @@ -7,7 +7,7 @@ import * as React from 'react'; import axios from 'axios'; 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 { Doc, DocListCast, Field, FieldType, Opt, StrListCast } from '../../../fields/Doc'; import { Id } from '../../../fields/FieldSymbols'; import { HtmlField } from '../../../fields/HtmlField'; import { InkTool } from '../../../fields/InkField'; @@ -55,7 +55,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { } public static openSidebarWidth = 250; public static sidebarResizerWidth = 5; - static webStyleSheet = addStyleSheet(); + static webStyleSheet = addStyleSheet().sheet; private _setPreviewCursor: undefined | ((x: number, y: number, drag: boolean, hide: boolean, doc: Opt<Doc>) => void); private _mainCont: React.RefObject<HTMLDivElement> = React.createRef(); private _outerRef: React.RefObject<HTMLDivElement> = React.createRef(); @@ -454,9 +454,27 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { } }; @action - iframeDown = () => { - // This is an empty replacement to avoid linter errors - // The original functionality is no longer needed + iframeDown = (e: PointerEvent) => { + this._textAnnotationCreator = undefined; + const sel = this._url ? this._iframe?.contentDocument?.getSelection() : window.document.getSelection(); + if (sel?.empty && !(e.target as any).textContent) + 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 target = e.target as HTMLElement; + const word = target && getWordAtPoint(target, e.clientX, e.clientY); + if (!word && !target?.className?.includes('rangeslider') && !target?.onclick && !target?.parentElement?.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; @@ -482,6 +500,122 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { _iframetimeout: NodeJS.Timeout | undefined = 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<string>; + try { + href = iframe?.contentWindow?.location.href; + } catch { + // 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', + undoable( + action((e: MouseEvent) => { + let eleHref = (e.target as any)?.outerHTML?.split('"="')[1]?.split('"')[0]; + for (let ele = e.target as HTMLElement | Element | null; ele; ele = ele.parentElement) { + if ('href' in ele) { + eleHref = (typeof ele.href === 'string' ? ele.href : eleHref) || (ele.parentElement && 'href' in ele.parentElement ? (ele.parentElement.href as string) : eleHref); + } + } + const origin = this.webField?.origin; + if (eleHref && origin) { + const batch = UndoManager.StartBatch('webclick'); + e.stopPropagation(); + setTimeout(() => { + const url = eleHref.replace(ClientUtils.prepend(''), origin); + this.setData(url); + batch.end(); + }); + if (this._outerRef.current) { + this._outerRef.current.scrollTop = NumCast(this.layoutDoc._layout_scrollTop); + this._outerRef.current.scrollLeft = 0; + } + } + }), + 'follow web link' + ) + ); + 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) { @@ -515,8 +649,8 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { }; forward = (checkAvailable?: boolean) => { - const future = Cast(this.dataDoc[this.fieldKey + '_future'], listSpec('string'), []); - const history = Cast(this.dataDoc[this.fieldKey + '_history'], listSpec('string'), []); + const future = StrListCast(this.dataDoc[this.fieldKey + '_future']); + const history = StrListCast(this.dataDoc[this.fieldKey + '_history']); if (checkAvailable) return future.length; runInAction(() => { if (future.length) { @@ -550,13 +684,13 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { }; back = (checkAvailable?: boolean) => { - const future = Cast(this.dataDoc[this.fieldKey + '_future'], listSpec('string')); - const history = Cast(this.dataDoc[this.fieldKey + '_history'], listSpec('string'), []); + const future = StrListCast(this.dataDoc[this.fieldKey + '_future']); + const history = StrListCast(this.dataDoc[this.fieldKey + '_history']); if (checkAvailable) return history.length; runInAction(() => { if (history.length) { const curUrl = this._url; - if (future === undefined) this.dataDoc[this.fieldKey + '_future'] = new List<string>([this._url]); + if (!future.length) this.dataDoc[this.fieldKey + '_future'] = new List<string>([this._url]); else this.dataDoc[this.fieldKey + '_future'] = new List<string>([...future, this._url]); this.dataDoc[this.fieldKey] = new WebField(new URL(history.pop()!)); this._scrollHeight = 0; @@ -568,12 +702,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { if (this._webUrl === this._url) { this._webUrl = curUrl; - setTimeout( - action(() => { - this._webUrl = this._url; - this.captureWebScreenshot(); // Capture screenshot for new URL - }) - ); + setTimeout(action(() => (this._webUrl = this._url))); } else { this._webUrl = this._url; this.captureWebScreenshot(); // Capture screenshot for new URL @@ -851,80 +980,36 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { // Handle WebField (screenshot of webpage) if (field instanceof WebField) { - // Show loading state with spinner - if (this._isLoadingScreenshot) { - return ( - <div className="webBox-loading"> - <div className="webBox-loading-message">{this._loadingFromCache ? 'Loading cached webpage preview...' : 'Loading webpage preview...'}</div> - <div className="webBox-loading-spinner"> - <FontAwesomeIcon className="documentdecorations-icon" icon="spinner" spin /> - </div> - </div> - ); - } - - // Show error state with retry button - if (this._screenshotError) { - return ( - <div className="webBox-error"> - <div className="webBox-error-icon"> - <FontAwesomeIcon icon="exclamation-triangle" size="2x" /> - </div> - <div className="webBox-error-message">{this._screenshotError}</div> - <div className="webBox-error-actions"> - <button onClick={() => this.captureWebScreenshot()} className="webBox-retry-button"> - <FontAwesomeIcon icon="sync" style={{ marginRight: '5px' }} /> - Retry - </button> - </div> - </div> - ); - } - - // Show screenshot in scrollable container - if (this._screenshotUrl) { - return ( - <div className="webBox-screenshot-container"> - <img - src={this._screenshotUrl} - alt="Webpage screenshot" - className="webBox-screenshot" - style={{ - width: '100%', - height: 'auto', - display: 'block', - }} - onError={action((e: React.SyntheticEvent<HTMLImageElement>) => { - console.error('Error loading screenshot:', e); - this._screenshotError = 'Failed to load screenshot image'; - this._isLoadingScreenshot = false; - this.dataDoc[this.fieldKey + '_screenshotUrl'] = undefined; - this.dataDoc[this.fieldKey + '_screenshotHeight'] = undefined; - })} - onLoad={() => { - this._scrollHeight = this._fullHeight; - if (this._initialScroll !== undefined) { - this.setScrollPos(this._initialScroll); - } - }} - /> - </div> - ); - } - - // Fall back to a placeholder if no screenshot yet + const url = this.layoutDoc[this.fieldKey + '_useCors'] ? '/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 ( - <div className="webBox-placeholder"> - <div>Preparing webpage preview...</div> - </div> + <iframe + title="web iframe" + key={this._warning} + className="webBox-iframe" + ref={action((r: HTMLIFrameElement | null) => { + this._iframe = r; + })} + style={{ pointerEvents: SnappingManager.IsResizing ? 'none' : undefined }} + src={url} + onLoad={this.iframeLoaded} + scrolling="no" // ugh.. on windows, I get an inner scroll bar for the iframe's body even though the scrollHeight should be set to the full height of the document. + // the 'allow-top-navigation' and 'allow-top-navigation-by-user-activation' attributes are left out to prevent iframes from redirecting the top-level Dash page + // sandbox={"allow-forms allow-modals allow-orientation-lock allow-pointer-lock allow-popups allow-popups-to-escape-sandbox allow-presentation allow-same-origin allow-scripts"} />; + sandbox={`${scripts ? 'allow-scripts' : ''} allow-forms allow-modals allow-orientation-lock allow-pointer-lock allow-popups allow-popups-to-escape-sandbox allow-presentation allow-same-origin`} + /> ); } - - // Default placeholder return ( - <div className="webBox-placeholder"> - <div>No content to display</div> - </div> + <iframe + title="web frame" + className="webBox-iframe" + ref={action((r: HTMLIFrameElement | null) => { + this._iframe = r; + })} + src="https://crossorigin.me/https://cs.brown.edu" + /> ); } @@ -1111,18 +1196,15 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { childPointerEvents = () => (this._props.isContentActive() ? 'all' : undefined); @computed get webpage() { TraceMobx(); - const containerWidth = NumCast(this.layoutDoc._width) || this._props.PanelWidth(); + // const previewScale = this._previewNativeWidth ? 1 - this.sidebarWidth() / this._previewNativeWidth : 1; const pointerEvents = this.layoutDoc._lockedPosition ? 'none' : (this._props.pointerEvents?.() as Property.PointerEvents | undefined); - + // const scale = previewScale * (this._props.NativeDimScaling?.() || 1); return ( <div className="webBox-outerContent" ref={this._outerRef} style={{ - width: '100%', - height: `${containerWidth}px`, - overflowY: 'auto', - overflowX: 'hidden', + height: '100%', //`${100 / scale}%`, pointerEvents, }} onWheel={this.onZoomWheel} @@ -1234,8 +1316,9 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { <div className="webBox-container" style={{ - width: `calc(100% - ${this.SidebarShown ? this.sidebarWidth() : 0}px)`, - height: '100%', + width: `calc(${100 / scale}% - ${!this.SidebarShown ? 0 : ((this.sidebarWidth() - WebBox.sidebarResizerWidth) / scale) * (this._previewWidth ? scale : 1)}px)`, + height: `${100 / scale}%`, + transform: `scale(${scale})`, pointerEvents, }} onContextMenu={this.specificContextMenu}> @@ -1276,7 +1359,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { {...this._props} whenChildContentsActiveChanged={this.whenChildContentsActiveChanged} fieldKey={this.fieldKey + '_' + this._urlHash} - Document={this.Document} + Doc={this.Document} layoutDoc={this.layoutDoc} dataDoc={this.dataDoc} setHeight={emptyFunction} |