diff options
Diffstat (limited to 'src/client/views/pdf')
-rw-r--r-- | src/client/views/pdf/AnchorMenu.tsx | 48 | ||||
-rw-r--r-- | src/client/views/pdf/Annotation.tsx | 168 | ||||
-rw-r--r-- | src/client/views/pdf/GPTPopup/GPTPopup.tsx | 109 | ||||
-rw-r--r-- | src/client/views/pdf/PDFViewer.tsx | 92 |
4 files changed, 226 insertions, 191 deletions
diff --git a/src/client/views/pdf/AnchorMenu.tsx b/src/client/views/pdf/AnchorMenu.tsx index 0b3ba81d3..a837969aa 100644 --- a/src/client/views/pdf/AnchorMenu.tsx +++ b/src/client/views/pdf/AnchorMenu.tsx @@ -4,18 +4,20 @@ import { IReactionDisposer, ObservableMap, action, computed, makeObservable, obs import { observer } from 'mobx-react'; import * as React from 'react'; import { ColorResult } from 'react-color'; -import { Utils, returnFalse, setupMoveUpEvents, unimplementedFunction } from '../../../Utils'; +import { ClientUtils, returnFalse, setupMoveUpEvents } from '../../../ClientUtils'; +import { emptyFunction, unimplementedFunction } from '../../../Utils'; import { Doc, Opt } from '../../../fields/Doc'; import { DocumentType } from '../../documents/DocumentTypes'; -import { SelectionManager } from '../../util/SelectionManager'; import { SettingsManager } from '../../util/SettingsManager'; import { AntimodeMenu, AntimodeMenuProps } from '../AntimodeMenu'; import { LinkPopup } from '../linking/LinkPopup'; import './AnchorMenu.scss'; -import { GPTPopup, GPTPopupMode } from './GPTPopup/GPTPopup'; +import { GPTPopup } from './GPTPopup/GPTPopup'; +import { DocumentView } from '../nodes/DocumentView'; @observer export class AnchorMenu extends AntimodeMenu<AntimodeMenuProps> { + // eslint-disable-next-line no-use-before-define static Instance: AnchorMenu; private _disposer: IReactionDisposer | undefined; @@ -36,7 +38,9 @@ export class AnchorMenu extends AntimodeMenu<AntimodeMenuProps> { // GPT additions @observable private selectedText: string = ''; @action - public setSelectedText = (txt: string) => (this.selectedText = txt); + public setSelectedText = (txt: string) => { + this.selectedText = txt; + }; public onMakeAnchor: () => Opt<Doc> = () => undefined; // Method to get anchor from text search @@ -45,8 +49,8 @@ export class AnchorMenu extends AntimodeMenu<AntimodeMenuProps> { public OnAudio: (e: PointerEvent) => void = unimplementedFunction; public StartDrag: (e: PointerEvent, ele: HTMLElement) => void = unimplementedFunction; public StartCropDrag: (e: PointerEvent, ele: HTMLElement) => void = unimplementedFunction; - public Highlight: (color: string) => Opt<Doc> = (color: string) => undefined; - public GetAnchor: (savedAnnotations: Opt<ObservableMap<number, HTMLDivElement[]>>, addAsAnnotation: boolean) => Opt<Doc> = (savedAnnotations: Opt<ObservableMap<number, HTMLDivElement[]>>, addAsAnnotation: boolean) => undefined; + public Highlight: (color: string) => Opt<Doc> = (/* color: string */) => undefined; + public GetAnchor: (savedAnnotations: Opt<ObservableMap<number, HTMLDivElement[]>>, addAsAnnotation: boolean) => Opt<Doc> = emptyFunction; public Delete: () => void = unimplementedFunction; public PinToPres: () => void = unimplementedFunction; public MakeTargetToggle: () => void = unimplementedFunction; @@ -62,8 +66,8 @@ export class AnchorMenu extends AntimodeMenu<AntimodeMenuProps> { componentDidMount() { this._disposer = reaction( - () => SelectionManager.Views.slice(), - sel => AnchorMenu.Instance.fadeOut(true) + () => DocumentView.Selected().slice(), + () => AnchorMenu.Instance.fadeOut(true) ); } @@ -71,7 +75,7 @@ export class AnchorMenu extends AntimodeMenu<AntimodeMenuProps> { * Invokes the API with the selected text and stores it in the summarized text. * @param e pointer down event */ - gptSummarize = async (e: React.PointerEvent) => { + gptSummarize = async () => { GPTPopup.Instance?.setSelectedText(this.selectedText); GPTPopup.Instance.generateSummary(); }; @@ -80,29 +84,29 @@ export class AnchorMenu extends AntimodeMenu<AntimodeMenuProps> { setupMoveUpEvents( this, e, - (e: PointerEvent) => { - this.StartDrag(e, this._commentRef.current!); + (moveEv: PointerEvent) => { + this.StartDrag(moveEv, this._commentRef.current!); return true; }, returnFalse, - e => this.OnClick?.(e) + clickEv => this.OnClick?.(clickEv) ); }; audioDown = (e: React.PointerEvent) => { - setupMoveUpEvents(this, e, returnFalse, returnFalse, e => this.OnAudio?.(e)); + setupMoveUpEvents(this, e, returnFalse, returnFalse, clickEv => this.OnAudio?.(clickEv)); }; cropDown = (e: React.PointerEvent) => { setupMoveUpEvents( this, e, - (e: PointerEvent) => { - this.StartCropDrag(e, this._cropRef.current!); + (moveEv: PointerEvent) => { + this.StartCropDrag(moveEv, this._cropRef.current!); return true; }, returnFalse, - e => this.OnCrop?.(e) + clickev => this.OnCrop?.(clickev) ); }; @@ -117,7 +121,7 @@ export class AnchorMenu extends AntimodeMenu<AntimodeMenuProps> { <Group> <IconButton icon={<FontAwesomeIcon icon="highlighter" style={{ transition: 'transform 0.1s', transform: 'rotate(-45deg)' }} />} - tooltip={'Click to Highlight'} + tooltip="Click to Highlight" onClick={this.highlightClicked} colorPicker={this.highlightColor} color={SettingsManager.userColor} @@ -133,7 +137,7 @@ export class AnchorMenu extends AntimodeMenu<AntimodeMenuProps> { hsl: { a: 0, h: 0, s: 0, l: 0 }, rgb: { a: 0, r: 0, b: 0, g: 0 }, }; - this.highlightColor = Utils.colorString(col); + this.highlightColor = ClientUtils.colorString(col); }; /** @@ -141,7 +145,7 @@ export class AnchorMenu extends AntimodeMenu<AntimodeMenuProps> { * all selected text available to summarize but its only supported for pdf and web ATM. * @returns Whether the GPT icon for summarization should appear */ - canSummarize = () => SelectionManager.Docs.some(doc => [DocumentType.PDF, DocumentType.WEB].includes(doc.type as any)); + canSummarize = () => DocumentView.SelectedDocs().some(doc => [DocumentType.PDF, DocumentType.WEB].includes(doc.type as any)); render() { const buttons = @@ -156,7 +160,7 @@ export class AnchorMenu extends AntimodeMenu<AntimodeMenuProps> { color={SettingsManager.userColor} /> </div> - {/* GPT Summarize icon only shows up when text is highlighted, not on marquee selection*/} + {/* GPT Summarize icon only shows up when text is highlighted, not on marquee selection */} {AnchorMenu.Instance.StartCropDrag === unimplementedFunction && this.canSummarize() && ( <IconButton tooltip="Summarize with AI" // @@ -176,7 +180,7 @@ export class AnchorMenu extends AntimodeMenu<AntimodeMenuProps> { <Popup tooltip="Find document to link to selected text" // type={Type.PRIM} - icon={<FontAwesomeIcon icon={'search'} />} + icon={<FontAwesomeIcon icon="search" />} popup={<LinkPopup key="popup" linkCreateAnchor={this.onMakeAnchor} />} color={SettingsManager.userColor} /> @@ -219,7 +223,7 @@ export class AnchorMenu extends AntimodeMenu<AntimodeMenuProps> { )} {this.IsTargetToggler !== returnFalse && ( <Toggle - tooltip={'Make target visibility toggle on click'} + tooltip="Make target visibility toggle on click" type={Type.PRIM} toggleType={ToggleType.BUTTON} toggleStatus={this.IsTargetToggler()} diff --git a/src/client/views/pdf/Annotation.tsx b/src/client/views/pdf/Annotation.tsx index a1f5ce703..7dd4047c1 100644 --- a/src/client/views/pdf/Annotation.tsx +++ b/src/client/views/pdf/Annotation.tsx @@ -1,24 +1,50 @@ import { action, computed, makeObservable } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; -import { Doc, DocListCast, Opt } from '../../../fields/Doc'; -import { Id } from '../../../fields/FieldSymbols'; +import { Doc, DocListCast, Opt, StrListCast } from '../../../fields/Doc'; +import { Highlight } from '../../../fields/DocSymbols'; import { List } from '../../../fields/List'; -import { BoolCast, Cast, DocCast, NumCast, StrCast } from '../../../fields/Types'; -import { LinkFollower } from '../../util/LinkFollower'; +import { BoolCast, DocCast, NumCast, StrCast } from '../../../fields/Types'; import { LinkManager } from '../../util/LinkManager'; -import { undoBatch } from '../../util/UndoManager'; -import { OpenWhere } from '../nodes/DocumentView'; +import { undoable } from '../../util/UndoManager'; +import { ObservableReactComponent } from '../ObservableReactComponent'; +import { DocumentView } from '../nodes/DocumentView'; import { FieldViewProps } from '../nodes/FieldView'; +import { OpenWhere } from '../nodes/OpenWhere'; import { AnchorMenu } from './AnchorMenu'; import './Annotation.scss'; -import { ObservableReactComponent } from '../ObservableReactComponent'; + +interface IRegionAnnotationProps { + x: number; + y: number; + width: number; + height: number; + opacity: () => number; + background: () => string; + outline: () => string | undefined; +} + +const RegionAnnotation = function (props: IRegionAnnotationProps) { + return ( + <div + className="htmlAnnotation" + style={{ + left: NumCast(props.x), + top: NumCast(props.y), + width: NumCast(props.width), + height: NumCast(props.height), + opacity: props.opacity(), + outline: props.outline(), + backgroundColor: props.background(), + }} + /> + ); +}; interface IAnnotationProps extends FieldViewProps { - anno: Doc; - dataDoc: Doc; + annoDoc: Doc; + containerDataDoc: Doc; fieldKey: string; - showInfo?: (anno: Opt<Doc>) => void; pointerEvents?: () => Opt<string>; } @observer @@ -27,62 +53,45 @@ export class Annotation extends ObservableReactComponent<IAnnotationProps> { super(props); makeObservable(this); } - render() { - return ( - <div style={{ display: this._props.anno.textCopied && !Doc.GetBrushHighlightStatus(this._props.anno) ? 'none' : undefined }}> - {DocListCast(this._props.anno.text_inlineAnnotations).map(a => ( - <RegionAnnotation pointerEvents={this._props.pointerEvents} {...this._props} document={a} key={a[Id]} /> - ))} - </div> - ); - } -} -interface IRegionAnnotationProps extends IAnnotationProps { - document: Doc; - pointerEvents?: () => Opt<string>; -} -@observer -class RegionAnnotation extends ObservableReactComponent<IRegionAnnotationProps> { - private _mainCont: React.RefObject<HTMLDivElement> = React.createRef(); - - @computed get annoTextRegion() { - return Cast(this._props.document.annoTextRegion, Doc, null) || this._props.document; + @computed get linkHighlighted() { + const found = LinkManager.Instance.getAllDirectLinks(this._props.annoDoc).find(link => { + const a1 = Doc.getOppositeAnchor(link, this._props.annoDoc); + return a1 && Doc.GetBrushStatus(DocCast(a1.annotationOn, a1)); + }); + return found; } - @undoBatch - deleteAnnotation = () => { - const docAnnotations = DocListCast(this._props.dataDoc[this._props.fieldKey]); - this._props.dataDoc[this._props.fieldKey] = new List<Doc>(docAnnotations.filter(a => a !== this.annoTextRegion)); + deleteAnnotation = undoable(() => { + const docAnnotations = DocListCast(this._props.containerDataDoc[this._props.fieldKey]); + this._props.containerDataDoc[this._props.fieldKey] = new List<Doc>(docAnnotations.filter(a => a !== this._props.annoDoc)); AnchorMenu.Instance.fadeOut(true); this._props.select(false); - }; + }, 'delete annotation'); + + pinToPres = undoable(() => this._props.pinToPres(this._props.annoDoc, {}), 'pin to pres'); - @undoBatch - pinToPres = () => this._props.pinToPres(this.annoTextRegion, {}); + makeTargetToggle = undoable(() => { this._props.annoDoc.followLinkToggle = !this._props.annoDoc.followLinkToggle }, "set link toggle"); // prettier-ignore - @undoBatch - makeTargretToggle = () => (this.annoTextRegion.followLinkToggle = !this.annoTextRegion.followLinkToggle); + isTargetToggler = () => BoolCast(this._props.annoDoc.followLinkToggle); - isTargetToggler = () => BoolCast(this.annoTextRegion.followLinkToggle); - @undoBatch - showTargetTrail = (anchor: Doc) => { + showTargetTrail = undoable((anchor: Doc) => { const trail = DocCast(anchor.presentationTrail); if (trail) { Doc.ActivePresentation = trail; this._props.addDocTab(trail, OpenWhere.replaceRight); } - }; + }, 'show target trail'); @action onContextMenu = (e: React.MouseEvent) => { AnchorMenu.Instance.Status = 'annotation'; - AnchorMenu.Instance.Delete = this.deleteAnnotation.bind(this); + AnchorMenu.Instance.Delete = this.deleteAnnotation; AnchorMenu.Instance.Pinned = false; AnchorMenu.Instance.PinToPres = this.pinToPres; - AnchorMenu.Instance.MakeTargetToggle = this.makeTargretToggle; + AnchorMenu.Instance.MakeTargetToggle = this.makeTargetToggle; AnchorMenu.Instance.IsTargetToggler = this.isTargetToggler; - AnchorMenu.Instance.ShowTargetTrail = () => this.showTargetTrail(this.annoTextRegion); + AnchorMenu.Instance.ShowTargetTrail = () => this.showTargetTrail(this._props.annoDoc); AnchorMenu.Instance.jumpTo(e.clientX, e.clientY, true); e.stopPropagation(); e.preventDefault(); @@ -94,44 +103,43 @@ class RegionAnnotation extends ObservableReactComponent<IRegionAnnotationProps> e.preventDefault(); } else if (e.button === 0) { e.stopPropagation(); - LinkFollower.FollowLink(undefined, this.annoTextRegion, false); + DocumentView.FollowLink(undefined, this._props.annoDoc, false); } }; - - @computed get linkHighlighted() { - for (const link of LinkManager.Instance.getAllDirectLinks(this._props.document)) { - const a1 = LinkManager.getOppositeAnchor(link, this._props.document); - if (a1 && Doc.GetBrushStatus(DocCast(a1.annotationOn, this._props.document))) return true; - } - } - + brushed = () => this._props.annoDoc && Doc.GetBrushHighlightStatus(this._props.annoDoc); + opacity = () => (this.brushed() === Doc.DocBrushStatus.highlighted ? 0.5 : 1); + outline = () => (this.linkHighlighted ? 'solid 1px lightBlue' : undefined); + background = () => (this._props.annoDoc[Highlight] ? 'orange' : StrCast(this._props.annoDoc.backgroundColor)); render() { - const brushed = this.annoTextRegion && Doc.GetBrushHighlightStatus(this.annoTextRegion); return ( - <div - className="htmlAnnotation" - ref={this._mainCont} - onPointerEnter={action(() => { - Doc.BrushDoc(this._props.anno); - this._props.showInfo?.(this._props.anno); - })} - onPointerLeave={action(() => { - Doc.UnBrushDoc(this._props.anno); - this._props.showInfo?.(undefined); - })} - onPointerDown={this.onPointerDown} - onContextMenu={this.onContextMenu} - style={{ - left: NumCast(this._props.document.x), - top: NumCast(this._props.document.y), - width: NumCast(this._props.document._width), - height: NumCast(this._props.document._height), - opacity: brushed === Doc.DocBrushStatus.highlighted ? 0.5 : undefined, - pointerEvents: this._props.pointerEvents?.() as any, - outline: brushed === Doc.DocBrushStatus.unbrushed && this.linkHighlighted ? 'solid 1px lightBlue' : undefined, - backgroundColor: brushed === Doc.DocBrushStatus.highlighted ? 'orange' : StrCast(this._props.document.backgroundColor), - }} - /> + <div style={{ display: this._props.annoDoc.textCopied && !Doc.GetBrushHighlightStatus(this._props.annoDoc) ? 'none' : undefined }}> + {StrListCast(this._props.annoDoc.text_inlineAnnotations) + .map(a => a.split?.(':')) + .filter(fields => fields) + .map(([x, y, width, height]) => ( + <div + key={'' + x + y + width + height} + style={{ pointerEvents: this._props.pointerEvents?.() as any }} + onPointerDown={this.onPointerDown} + onContextMenu={this.onContextMenu} + onPointerEnter={() => { + Doc.BrushDoc(this._props.annoDoc); + }} + onPointerLeave={() => { + Doc.UnBrushDoc(this._props.annoDoc); + }}> + <RegionAnnotation // + x={Number(x)} + y={Number(y)} + width={Number(width)} + height={Number(height)} + outline={this.outline} + background={this.background} + opacity={this.opacity} + /> + </div> + ))} + </div> ); } } diff --git a/src/client/views/pdf/GPTPopup/GPTPopup.tsx b/src/client/views/pdf/GPTPopup/GPTPopup.tsx index 29b1ca365..c1bfdf176 100644 --- a/src/client/views/pdf/GPTPopup/GPTPopup.tsx +++ b/src/client/views/pdf/GPTPopup/GPTPopup.tsx @@ -1,17 +1,19 @@ +/* eslint-disable jsx-a11y/label-has-associated-control */ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { Button, EditableText, IconButton, Size, Type } from 'browndash-components'; +import { Button, IconButton, Type } from 'browndash-components'; import { action, makeObservable, observable } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; import { CgClose } from 'react-icons/cg'; import ReactLoading from 'react-loading'; import { TypeAnimation } from 'react-type-animation'; -import { Utils } from '../../../../Utils'; +import { ClientUtils } from '../../../../ClientUtils'; import { Doc } from '../../../../fields/Doc'; import { NumCast, StrCast } from '../../../../fields/Types'; import { Networking } from '../../../Network'; import { GPTCallType, gptAPICall, gptImageCall } from '../../../apis/gpt/GPT'; -import { DocUtils, Docs } from '../../../documents/Documents'; +import { Docs } from '../../../documents/Documents'; +import { DocUtils } from '../../../documents/DocUtils'; import { ObservableReactComponent } from '../../ObservableReactComponent'; import { AnchorMenu } from '../AnchorMenu'; import './GPTPopup.scss'; @@ -27,6 +29,7 @@ interface GPTPopupProps {} @observer export class GPTPopup extends ObservableReactComponent<GPTPopupProps> { + // eslint-disable-next-line no-use-before-define static Instance: GPTPopup; @observable private chatMode: boolean = false; @@ -59,7 +62,7 @@ export class GPTPopup extends ObservableReactComponent<GPTPopupProps> { public dataChatPrompt: string | null = null; @action public setDataJson = (text: string) => { - if (text=="") this.dataChatPrompt = ""; + if (text === '') this.dataChatPrompt = ''; this.dataJson = text; }; @@ -87,8 +90,6 @@ export class GPTPopup extends ObservableReactComponent<GPTPopupProps> { @observable public highlightRange: number[] = []; @action callSummaryApi = () => {}; - @action callEditApi = () => {}; - @action replaceText = (replacement: string) => {}; @observable private done: boolean = false; @@ -127,26 +128,27 @@ export class GPTPopup extends ObservableReactComponent<GPTPopupProps> { * Generates a Dalle image and uploads it to the server. */ generateImage = async () => { - if (this.imgDesc === '') return; + if (this.imgDesc === '') return undefined; this.setImgUrls([]); this.setMode(GPTPopupMode.IMAGE); this.setVisible(true); this.setLoading(true); try { - let image_urls = await gptImageCall(this.imgDesc); - console.log('Image urls: ', image_urls); - if (image_urls && image_urls[0]) { - const [result] = await Networking.PostToServer('/uploadRemoteImage', { sources: [image_urls[0]] }); + const imageUrls = await gptImageCall(this.imgDesc); + console.log('Image urls: ', imageUrls); + if (imageUrls && imageUrls[0]) { + const [result] = await Networking.PostToServer('/uploadRemoteImage', { sources: [imageUrls[0]] }); console.log('Upload result: ', result); - const source = Utils.prepend(result.accessPaths.agnostic.client); + const source = ClientUtils.prepend(result.accessPaths.agnostic.client); console.log('Upload source: ', source); - this.setImgUrls([[image_urls[0], source]]); + this.setImgUrls([[imageUrls[0], source]]); } } catch (err) { console.error(err); } this.setLoading(false); + return undefined; }; generateSummary = async () => { @@ -161,19 +163,19 @@ export class GPTPopup extends ObservableReactComponent<GPTPopupProps> { console.error(err); } GPTPopup.Instance.setLoading(false); - } + }; generateDataAnalysis = async () => { GPTPopup.Instance.setVisible(true); GPTPopup.Instance.setLoading(true); try { - let res = await gptAPICall(this.dataJson, GPTCallType.DATA, this.dataChatPrompt); + const res = await gptAPICall(this.dataJson, GPTCallType.DATA, this.dataChatPrompt); GPTPopup.Instance.setText(res || 'Something went wrong.'); } catch (err) { console.error(err); } GPTPopup.Instance.setLoading(false); - } + }; /** * Transfers the summarization text to a sidebar annotation text document. @@ -224,7 +226,7 @@ export class GPTPopup extends ObservableReactComponent<GPTPopupProps> { */ private chatWithAI = () => { this.chatMode = true; - } + }; dataPromptChanged = action((e: React.ChangeEvent<HTMLInputElement>) => { this.dataChatPrompt = e.target.value; }); @@ -243,30 +245,24 @@ export class GPTPopup extends ObservableReactComponent<GPTPopupProps> { } }; - imageBox = () => { - return ( - <div style={{ display: 'flex', flexDirection: 'column', gap: '1rem' }}> - {this.heading('GENERATED IMAGE')} - <div className="image-content-wrapper"> - {this.imgUrls.map(rawSrc => ( - <div className="img-wrapper"> - <div className="img-container"> - <img key={rawSrc[0]} src={rawSrc[0]} width={150} height={150} alt="dalle generation" /> - </div> - <div className="btn-container"> - <Button text="Save Image" onClick={() => this.transferToImage(rawSrc[1])} color={StrCast(Doc.UserDoc().userColor)} type={Type.TERT} /> - </div> + imageBox = () => ( + <div style={{ display: 'flex', flexDirection: 'column', gap: '1rem' }}> + {this.heading('GENERATED IMAGE')} + <div className="image-content-wrapper"> + {this.imgUrls.map(rawSrc => ( + <div className="img-wrapper"> + <div className="img-container"> + <img key={rawSrc[0]} src={rawSrc[0]} width={150} height={150} alt="dalle generation" /> </div> - ))} - </div> - {!this.loading && ( - <> - <IconButton tooltip="Generate Again" onClick={this.generateImage} icon={<FontAwesomeIcon icon="redo-alt" size="lg" />} color={StrCast(Doc.UserDoc().userVariantColor)} /> - </> - )} + <div className="btn-container"> + <Button text="Save Image" onClick={() => this.transferToImage(rawSrc[1])} color={StrCast(Doc.UserDoc().userColor)} type={Type.TERT} /> + </div> + </div> + ))} </div> - ); - }; + {!this.loading && <IconButton tooltip="Generate Again" onClick={this.generateImage} icon={<FontAwesomeIcon icon="redo-alt" size="lg" />} color={StrCast(Doc.UserDoc().userVariantColor)} />} + </div> + ); summaryBox = () => ( <> @@ -320,7 +316,7 @@ export class GPTPopup extends ObservableReactComponent<GPTPopupProps> { dataAnalysisBox = () => ( <> <div> - {this.heading("ANALYSIS")} + {this.heading('ANALYSIS')} <div className="content-wrapper"> {!this.loading && (!this.done ? ( @@ -342,8 +338,8 @@ export class GPTPopup extends ObservableReactComponent<GPTPopupProps> { </div> {!this.loading && ( <div className="btns-wrapper"> - {this.done? - this.chatMode?( + {this.done ? ( + this.chatMode ? ( <input defaultValue="" autoComplete="off" @@ -356,19 +352,26 @@ export class GPTPopup extends ObservableReactComponent<GPTPopupProps> { placeholder="Ask GPT a question about the data..." id="search-input" className="searchBox-input" - style={{width: "100%"}} + style={{ width: '100%' }} /> - ) - :( - <> - <Button tooltip="Transfer to text" text="Transfer To Text" onClick={this.transferToText} color={StrCast(Doc.UserDoc().userVariantColor)} type={Type.TERT} /> - <Button tooltip="Chat with AI" text="Chat with AI" onClick={this.chatWithAI} color={StrCast(Doc.UserDoc().userVariantColor)} type={Type.TERT} /> - </> + ) : ( + <> + <Button tooltip="Transfer to text" text="Transfer To Text" onClick={this.transferToText} color={StrCast(Doc.UserDoc().userVariantColor)} type={Type.TERT} /> + <Button tooltip="Chat with AI" text="Chat with AI" onClick={this.chatWithAI} color={StrCast(Doc.UserDoc().userVariantColor)} type={Type.TERT} /> + </> + ) ) : ( <div className="summarizing"> <span>Summarizing</span> <ReactLoading type="bubbles" color="#bcbcbc" width={20} height={20} /> - <Button text="Stop Animation" onClick={() => {this.setDone(true);}} color={StrCast(Doc.UserDoc().userVariantColor)} type={Type.TERT}/> + <Button + text="Stop Animation" + onClick={() => { + this.setDone(true); + }} + color={StrCast(Doc.UserDoc().userVariantColor)} + type={Type.TERT} + /> </div> )} </div> @@ -382,9 +385,7 @@ export class GPTPopup extends ObservableReactComponent<GPTPopupProps> { <FontAwesomeIcon icon="exclamation-circle" size="sm" style={{ paddingRight: '5px' }} /> AI generated responses can contain inaccurate or misleading content. </div> - ) : ( - <></> - ); + ) : null; heading = (headingText: string) => ( <div className="summary-heading"> @@ -396,7 +397,7 @@ export class GPTPopup extends ObservableReactComponent<GPTPopupProps> { render() { return ( <div className="summary-box" style={{ display: this.visible ? 'flex' : 'none' }}> - {this.mode === GPTPopupMode.SUMMARY? this.summaryBox() : this.mode === GPTPopupMode.DATA? this.dataAnalysisBox() : this.mode === GPTPopupMode.IMAGE ? this.imageBox() : <></>} + {this.mode === GPTPopupMode.SUMMARY ? this.summaryBox() : this.mode === GPTPopupMode.DATA ? this.dataAnalysisBox() : this.mode === GPTPopupMode.IMAGE ? this.imageBox() : null} </div> ); } diff --git a/src/client/views/pdf/PDFViewer.tsx b/src/client/views/pdf/PDFViewer.tsx index aaff2a342..45b1d727e 100644 --- a/src/client/views/pdf/PDFViewer.tsx +++ b/src/client/views/pdf/PDFViewer.tsx @@ -1,34 +1,37 @@ +/* eslint-disable jsx-a11y/no-static-element-interactions */ +/* eslint-disable jsx-a11y/click-events-have-key-events */ import { action, computed, IReactionDisposer, makeObservable, observable, ObservableMap, reaction, runInAction } from 'mobx'; import { observer } from 'mobx-react'; import * as Pdfjs from 'pdfjs-dist'; import 'pdfjs-dist/web/pdf_viewer.css'; import * as PDFJSViewer from 'pdfjs-dist/web/pdf_viewer.mjs'; import * as React from 'react'; -import { Doc, DocListCast, Opt } from '../../../fields/Doc'; +import { addStyleSheet, addStyleSheetRule, clearStyleSheetRules, ClientUtils, returnAll, returnFalse, returnNone, returnZero, smoothScroll } from '../../../ClientUtils'; +import { CreateLinkToActiveAudio, Doc, DocListCast, Opt } from '../../../fields/Doc'; import { DocData, Height } from '../../../fields/DocSymbols'; import { Id } from '../../../fields/FieldSymbols'; import { InkTool } from '../../../fields/InkField'; import { Cast, NumCast, StrCast } from '../../../fields/Types'; import { TraceMobx } from '../../../fields/util'; -import { addStyleSheet, addStyleSheetRule, clearStyleSheetRules, emptyFunction, returnAll, returnFalse, returnNone, returnZero, smoothScroll, Utils } from '../../../Utils'; -import { DocUtils } from '../../documents/Documents'; -import { SelectionManager } from '../../util/SelectionManager'; +import { emptyFunction } from '../../../Utils'; +import { DocUtils } from '../../documents/DocUtils'; import { SnappingManager } from '../../util/SnappingManager'; import { MarqueeOptionsMenu } from '../collections/collectionFreeForm'; import { CollectionFreeFormView } from '../collections/collectionFreeForm/CollectionFreeFormView'; import { MarqueeAnnotator } from '../MarqueeAnnotator'; -import { FocusViewOptions, FieldViewProps } from '../nodes/FieldView'; +import { DocumentView } from '../nodes/DocumentView'; +import { FieldViewProps } from '../nodes/FieldView'; +import { FocusViewOptions } from '../nodes/FocusViewOptions'; import { LinkInfo } from '../nodes/LinkDocPreview'; import { PDFBox } from '../nodes/PDFBox'; import { ObservableReactComponent } from '../ObservableReactComponent'; -import { StyleProp } from '../StyleProvider'; +import { StyleProp } from '../StyleProp'; import { AnchorMenu } from './AnchorMenu'; import { Annotation } from './Annotation'; import { GPTPopup } from './GPTPopup/GPTPopup'; import './PDFViewer.scss'; -const _global = (window /* browser */ || global) /* node */ as any; -//pdfjsLib.GlobalWorkerOptions.workerSrc = `/assets/pdf.worker.js`; +// pdfjsLib.GlobalWorkerOptions.workerSrc = `/assets/pdf.worker.js`; // The workerSrc property shall be specified. Pdfjs.GlobalWorkerOptions.workerSrc = 'https://unpkg.com/pdfjs-dist@4.1.392/build/pdf.worker.mjs'; @@ -42,6 +45,7 @@ interface IViewerProps extends FieldViewProps { url: string; sidebarAddDoc: (doc: Doc | Doc[], sidebarKey?: string | undefined) => boolean; loaded?: (nw: number, nh: number, np: number) => void; + // eslint-disable-next-line no-use-before-define setPdfViewer: (view: PDFViewer) => void; anchorMenuClick?: () => undefined | ((anchor: Doc) => void); crop: (region: Doc | undefined, addCrop?: boolean) => Doc | undefined; @@ -98,14 +102,18 @@ export class PDFViewer extends ObservableReactComponent<IViewerProps> { } componentDidMount() { - runInAction(() => (this._showWaiting = true)); + runInAction(() => { + this._showWaiting = true; + }); this.setupPdfJsViewer(); - this._mainCont.current?.addEventListener('scroll', e => ((e.target as any).scrollLeft = 0)); + this._mainCont.current?.addEventListener('scroll', e => { + (e.target as any).scrollLeft = 0; + }); this._disposers.layout_autoHeight = reaction( () => this._props.layoutDoc._layout_autoHeight, - layout_autoHeight => { - if (layout_autoHeight) { + layoutAutoHeight => { + if (layoutAutoHeight) { this._props.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)); } @@ -114,7 +122,7 @@ export class PDFViewer extends ObservableReactComponent<IViewerProps> { this._disposers.selected = reaction( () => this._props.isSelected(), - selected => SelectionManager.Views.length === 1 && this.setupPdfJsViewer(), + () => DocumentView.Selected().length === 1 && this.setupPdfJsViewer(), { fireImmediately: true } ); this._disposers.curPage = reaction( @@ -164,7 +172,9 @@ export class PDFViewer extends ObservableReactComponent<IViewerProps> { ) ); } - runInAction(() => (this._scrollHeight = (this._pageSizes.reduce((size, page) => size + page.height, 0) * 96) / 72)); + runInAction(() => { + this._scrollHeight = (this._pageSizes.reduce((size, page) => size + page.height, 0) * 96) / 72; + }); }; _scrollStopper: undefined | (() => void); @@ -176,7 +186,7 @@ export class PDFViewer extends ObservableReactComponent<IViewerProps> { let focusSpeed: Opt<number>; if (doc !== this._props.Document && mainCont) { const windowHeight = this._props.PanelHeight() / (this._props.NativeDimScaling?.() || 1); - const scrollTo = Utils.scrollIntoView(scrollTop, doc[Height](), NumCast(this._props.layoutDoc._layout_scrollTop), windowHeight, windowHeight * 0.1, this._scrollHeight); + const scrollTo = ClientUtils.scrollIntoView(scrollTop, doc[Height](), NumCast(this._props.layoutDoc._layout_scrollTop), windowHeight, windowHeight * 0.1, this._scrollHeight); if (scrollTo !== undefined && scrollTo !== this._props.layoutDoc._layout_scrollTop) { if (!this._pdfViewer) this._initialScroll = { loc: scrollTo, easeFunc: options.easeFunc }; else if (!options.instant) this._scrollStopper = smoothScroll((focusSpeed = options.zoomTime ?? 500), mainCont, scrollTo, options.easeFunc, this._scrollStopper); @@ -202,14 +212,18 @@ export class PDFViewer extends ObservableReactComponent<IViewerProps> { pagesinit = () => { if (this._pdfViewer._setDocumentViewerElement?.offsetParent) { - runInAction(() => (this._pdfViewer.currentScaleValue = this._props.layoutDoc._freeform_scale = 1)); + runInAction(() => { + this._pdfViewer.currentScaleValue = this._props.layoutDoc._freeform_scale = 1; + }); this.gotoPage(NumCast(this._props.Document._layout_curPage, 1)); } document.removeEventListener('pagesinit', this.pagesinit); - var quickScroll: { loc?: string; easeFunc?: 'ease' | 'linear' } | undefined = { loc: this._initialScroll ? this._initialScroll.loc?.toString() : '', easeFunc: this._initialScroll ? this._initialScroll.easeFunc : undefined }; + let quickScroll: { loc?: string; easeFunc?: 'ease' | 'linear' } | undefined = { loc: this._initialScroll ? this._initialScroll.loc?.toString() : '', easeFunc: this._initialScroll ? this._initialScroll.easeFunc : undefined }; this._disposers.scale = reaction( () => NumCast(this._props.layoutDoc._freeform_scale, 1), - scale => (this._pdfViewer.currentScaleValue = scale), + scale => { + this._pdfViewer.currentScaleValue = scale; + }, { fireImmediately: true } ); this._disposers.scroll = reaction( @@ -226,7 +240,9 @@ export class PDFViewer extends ObservableReactComponent<IViewerProps> { setTimeout( () => { this._mainCont.current && (this._scrollStopper = smoothScroll(duration, this._mainCont.current, pos, this._initialScroll?.easeFunc ?? 'ease', this._scrollStopper)); - setTimeout(() => (this._forcedScroll = false), duration); + setTimeout(() => { + this._forcedScroll = false; + }, duration); }, this._mainCont.current ? 0 : 250 ); // wait for mainCont and try again to scroll @@ -262,7 +278,9 @@ export class PDFViewer extends ObservableReactComponent<IViewerProps> { eventBus._on('pagesinit', this.pagesinit); eventBus._on( 'pagerendered', - action(() => (this._showWaiting = false)) + action(() => { + this._showWaiting = false; + }) ); const pdfLinkService = new PDFJSViewer.PDFLinkService({ eventBus }); const pdfFindController = new PDFJSViewer.PDFFindController({ linkService: pdfLinkService, eventBus }); @@ -305,7 +323,7 @@ export class PDFViewer extends ObservableReactComponent<IViewerProps> { @observable private _scrollTimer: any = undefined; - onScroll = (e: React.UIEvent<HTMLElement>) => { + onScroll = () => { if (this._mainCont.current && !this._forcedScroll) { this._ignoreScroll = true; // the pdf scrolled, so we need to tell the Doc to scroll but we don't want the doc to then try to set the PDF scroll pos (which would interfere with the smooth scroll animation) if (!LinkInfo.Instance?.LinkInfo) { @@ -314,7 +332,7 @@ export class PDFViewer extends ObservableReactComponent<IViewerProps> { this._ignoreScroll = false; if (this._scrollTimer) clearTimeout(this._scrollTimer); // wait until a scrolling pause, then create an anchor to audio this._scrollTimer = setTimeout(() => { - DocUtils.MakeLinkToActiveAudio(() => this._props.pdfBox.getAnchor(true)!, false); + CreateLinkToActiveAudio(() => this._props.pdfBox.getAnchor(true)!, false); this._scrollTimer = undefined; }, 200); } @@ -340,7 +358,7 @@ export class PDFViewer extends ObservableReactComponent<IViewerProps> { query: searchString, }; if (clear) { - this._pdfViewer?.eventBus.dispatch('reset', {}); + this._pdfViewer?.eventBus.dispatch('findbarclose', {}); } else if (!searchString) { bwd ? this.prevAnnotation() : this.nextAnnotation(); } else if (this._pdfViewer?.pageViewsReady) { @@ -371,7 +389,6 @@ export class PDFViewer extends ObservableReactComponent<IViewerProps> { if (!e.altKey && e.button === 0 && this._props.isContentActive() && ![InkTool.Highlighter, InkTool.Pen, InkTool.Write].includes(Doc.ActiveTool)) { this._props.select(false); MarqueeAnnotator.clearAnnotations(this._savedAnnotations); - this._marqueeref.current?.onInitiateSelection([e.clientX, e.clientY]); this.isAnnotating = true; const target = e.target as any; if (e.target && (target.className.includes('endOfContent') || (target.parentElement.className !== 'textLayer' && target.parentElement.parentElement?.className !== 'textLayer'))) { @@ -383,11 +400,12 @@ export class PDFViewer extends ObservableReactComponent<IViewerProps> { this._styleRule = addStyleSheetRule(PDFViewer._annotationStyle, 'htmlAnnotation', { 'pointer-events': 'none' }); document.addEventListener('pointerup', this.onSelectEnd); } + this._marqueeref.current?.onInitiateSelection([e.clientX, e.clientY]); } }; @action - finishMarquee = (x?: number, y?: number) => { + finishMarquee = (/* x?: number, y?: number */) => { this._getAnchor = AnchorMenu.Instance?.GetAnchor; this.isAnnotating = false; this._marqueeref.current?.onTerminateSelection(); @@ -456,13 +474,15 @@ export class PDFViewer extends ObservableReactComponent<IViewerProps> { onClick = (e: React.MouseEvent) => { this._scrollStopper?.(); - if (this._setPreviewCursor && e.button === 0 && Math.abs(e.clientX - this._downX) < Utils.DRAG_THRESHOLD && Math.abs(e.clientY - this._downY) < Utils.DRAG_THRESHOLD) { + if (this._setPreviewCursor && e.button === 0 && Math.abs(e.clientX - this._downX) < ClientUtils.DRAG_THRESHOLD && Math.abs(e.clientY - this._downY) < ClientUtils.DRAG_THRESHOLD) { this._setPreviewCursor(e.clientX, e.clientY, false, false, this._props.Document); } // e.stopPropagation(); // bcz: not sure why this was here. We need to allow the DocumentView to get clicks to process doubleClicks }; - setPreviewCursor = (func?: (x: number, y: number, drag: boolean, hide: boolean, doc: Opt<Doc>) => void) => (this._setPreviewCursor = func); + setPreviewCursor = (func?: (x: number, y: number, drag: boolean, hide: boolean, doc: Opt<Doc>) => void) => { + this._setPreviewCursor = func; + }; @action onZoomWheel = (e: React.WheelEvent) => { @@ -485,7 +505,8 @@ export class PDFViewer extends ObservableReactComponent<IViewerProps> { return ( <div className="pdfViewerDash-annotationLayer" style={{ height: Doc.NativeHeight(this._props.Document), transform: `scale(${NumCast(this._props.layoutDoc._freeform_scale, 1)})` }} ref={this._annotationLayer}> {inlineAnnos.map(anno => ( - <Annotation {...this._props} fieldKey={this._props.fieldKey + '_annotations'} pointerEvents={this.pointerEvents} dataDoc={this._props.dataDoc} anno={anno} key={`${anno[Id]}-annotation`} /> + // eslint-disable-next-line react/jsx-props-no-spreading + <Annotation {...this._props} fieldKey={this._props.fieldKey + '_annotations'} pointerEvents={this.pointerEvents} containerDataDoc={this._props.dataDoc} annoDoc={anno} key={`${anno[Id]}-annotation`} /> ))} </div> ); @@ -496,8 +517,8 @@ export class PDFViewer extends ObservableReactComponent<IViewerProps> { overlayTransform = () => this.scrollXf().scale(1 / NumCast(this._props.layoutDoc._freeform_scale, 1)); panelWidth = () => this._props.PanelWidth() / (this._props.NativeDimScaling?.() || 1); panelHeight = () => this._props.PanelHeight() / (this._props.NativeDimScaling?.() || 1); - transparentFilter = () => [...this._props.childFilters(), Utils.TransparentBackgroundFilter]; - opaqueFilter = () => [...this._props.childFilters(), Utils.noDragDocsFilter, ...(SnappingManager.CanEmbed && this._props.isContentActive() ? [] : [Utils.OpaqueBackgroundFilter])]; + transparentFilter = () => [...this._props.childFilters(), ClientUtils.TransparentBackgroundFilter]; + opaqueFilter = () => [...this._props.childFilters(), ClientUtils.noDragDocsFilter, ...(SnappingManager.CanEmbed && this._props.isContentActive() ? [] : [ClientUtils.OpaqueBackgroundFilter])]; childStyleProvider = (doc: Doc | undefined, props: Opt<FieldViewProps>, property: string): any => { if (doc instanceof Doc && property === StyleProp.PointerEvents) { if (this.inlineTextAnnotations.includes(doc) || this._props.isContentActive() === false) return 'none'; @@ -517,6 +538,7 @@ export class PDFViewer extends ObservableReactComponent<IViewerProps> { pointerEvents: Doc.ActiveTool !== InkTool.None ? 'all' : undefined, }}> <CollectionFreeFormView + // eslint-disable-next-line react/jsx-props-no-spreading {...this._props} NativeWidth={returnZero} NativeHeight={returnZero} @@ -524,7 +546,7 @@ export class PDFViewer extends ObservableReactComponent<IViewerProps> { pointerEvents={this._props.isContentActive() && (SnappingManager.IsDragging || Doc.ActiveTool !== InkTool.None) ? returnAll : returnNone} // freeform view doesn't get events unless something is being dragged onto it. childPointerEvents={this.childPointerEvents} // but freeform children need to get events to allow text editing, etc renderDepth={this._props.renderDepth + 1} - isAnnotationOverlay={true} + isAnnotationOverlay fieldKey={this._props.fieldKey + '_annotations'} getScrollHeight={this.getScrollHeight} setPreviewCursor={this.setPreviewCursor} @@ -532,7 +554,7 @@ export class PDFViewer extends ObservableReactComponent<IViewerProps> { PanelWidth={this.panelWidth} ScreenToLocalTransform={this.overlayTransform} isAnyChildContentActive={returnFalse} - isAnnotationOverlayScrollable={true} + isAnnotationOverlayScrollable childFilters={childFilters} select={emptyFunction} styleProvider={this.childStyleProvider} @@ -577,7 +599,7 @@ export class PDFViewer extends ObservableReactComponent<IViewerProps> { {this.pdfViewerDiv} {this.annotationLayer} {this.overlayLayer} - {this._showWaiting ? <img className="pdfViewerDash-waiting" src={'/assets/loading.gif'} /> : null} + {this._showWaiting ? <img alt="" className="pdfViewerDash-waiting" src="/assets/loading.gif" /> : null} {!this._mainCont.current || !this._annotationLayer.current ? null : ( <MarqueeAnnotator ref={this._marqueeref} @@ -585,7 +607,7 @@ export class PDFViewer extends ObservableReactComponent<IViewerProps> { getPageFromScroll={this.getPageFromScroll} anchorMenuClick={this._props.anchorMenuClick} scrollTop={0} - isNativeScaled={true} + isNativeScaled annotationLayerScrollTop={NumCast(this._props.Document._layout_scrollTop)} addDocument={this.addDocumentWrapper} docView={this._props.pdfBox.DocumentView!} @@ -594,7 +616,7 @@ export class PDFViewer extends ObservableReactComponent<IViewerProps> { selectionText={this.selectionText} annotationLayer={this._annotationLayer.current} marqueeContainer={this._mainCont.current} - anchorMenuCrop={this._textSelecting ? undefined : this.crop} + anchorMenuCrop={this.crop} /> )} </div> |