aboutsummaryrefslogtreecommitdiff
path: root/src/client/views/pdf
diff options
context:
space:
mode:
authorbobzel <zzzman@gmail.com>2023-06-23 21:44:01 -0400
committerbobzel <zzzman@gmail.com>2023-06-23 21:44:01 -0400
commit85c017527f209c9d007d67ac70958843ab45e729 (patch)
treee2649860002e0c60e98d84439a67235002ddd9a4 /src/client/views/pdf
parente9d5dbeef2bf1dab9dfb863d970b70b3074e3d0a (diff)
parent1429ab79eac9aa316082f52c14c576f6b3a97111 (diff)
Merge branch 'master' into heartbeat
Diffstat (limited to 'src/client/views/pdf')
-rw-r--r--src/client/views/pdf/AnchorMenu.tsx338
-rw-r--r--src/client/views/pdf/Annotation.tsx35
-rw-r--r--src/client/views/pdf/GPTPopup/GPTPopup.scss132
-rw-r--r--src/client/views/pdf/GPTPopup/GPTPopup.tsx188
-rw-r--r--src/client/views/pdf/PDFViewer.tsx169
5 files changed, 692 insertions, 170 deletions
diff --git a/src/client/views/pdf/AnchorMenu.tsx b/src/client/views/pdf/AnchorMenu.tsx
index ee2ae10a7..5480600b0 100644
--- a/src/client/views/pdf/AnchorMenu.tsx
+++ b/src/client/views/pdf/AnchorMenu.tsx
@@ -10,6 +10,10 @@ import { SelectionManager } from '../../util/SelectionManager';
import { AntimodeMenu, AntimodeMenuProps } from '../AntimodeMenu';
import { LinkPopup } from '../linking/LinkPopup';
import { ButtonDropdown } from '../nodes/formattedText/RichTextMenu';
+import { gptAPICall, GPTCallType } from '../../apis/gpt/GPT';
+import { GPTPopup, GPTPopupMode } from './GPTPopup/GPTPopup';
+import { LightboxView } from '../LightboxView';
+import { EditorView } from 'prosemirror-view';
import './AnchorMenu.scss';
@observer
@@ -17,6 +21,7 @@ export class AnchorMenu extends AntimodeMenu<AntimodeMenuProps> {
static Instance: AnchorMenu;
private _disposer: IReactionDisposer | undefined;
+ private _disposer2: IReactionDisposer | undefined;
private _commentCont = React.createRef<HTMLButtonElement>();
private _palette = [
'rgba(208, 2, 27, 0.8)',
@@ -36,15 +41,61 @@ export class AnchorMenu extends AntimodeMenu<AntimodeMenuProps> {
'rgba(0, 0, 0, 0.8)',
];
- @observable private _keyValue: string = '';
- @observable private _valueValue: string = '';
- @observable private _added: boolean = false;
@observable private highlightColor: string = 'rgba(245, 230, 95, 0.616)';
@observable private _showLinkPopup: boolean = false;
- @observable public Highlighting: boolean = false;
@observable public Status: 'marquee' | 'annotation' | '' = '';
+ // GPT additions
+ @observable private GPTpopupText: string = '';
+ @observable private loadingGPT: boolean = false;
+ @observable private showGPTPopup: boolean = false;
+ @observable private GPTMode: GPTPopupMode = GPTPopupMode.SUMMARY;
+ @observable private selectedText: string = '';
+ @observable private editorView?: EditorView;
+ @observable private textDoc?: Doc;
+ @observable private highlightRange: number[] | undefined;
+ private selectionRange: number[] | undefined;
+
+ @action
+ setGPTPopupVis = (vis: boolean) => {
+ this.showGPTPopup = vis;
+ };
+ @action
+ setGPTMode = (mode: GPTPopupMode) => {
+ this.GPTMode = mode;
+ };
+
+ @action
+ setGPTPopupText = (txt: string) => {
+ this.GPTpopupText = txt;
+ };
+
+ @action
+ setLoading = (loading: boolean) => {
+ this.loadingGPT = loading;
+ };
+
+ @action
+ setHighlightRange(r: number[] | undefined) {
+ this.highlightRange = r;
+ }
+
+ @action
+ public setSelectedText = (txt: string) => {
+ this.selectedText = txt;
+ };
+
+ @action
+ public setEditorView = (editor: EditorView) => {
+ this.editorView = editor;
+ };
+
+ @action
+ public setTextDoc = (textDoc: Doc) => {
+ this.textDoc = textDoc;
+ };
+
public onMakeAnchor: () => Opt<Doc> = () => undefined; // Method to get anchor from text search
public OnCrop: (e: PointerEvent) => void = unimplementedFunction;
@@ -52,13 +103,13 @@ 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, isPushpin: boolean) => Opt<Doc> = (color: string, isPushpin: boolean) => undefined;
- public GetAnchor: (savedAnnotations?: ObservableMap<number, HTMLDivElement[]>) => Opt<Doc> = () => undefined;
+ public Highlight: (color: string, isTargetToggler: boolean, savedAnnotations?: ObservableMap<number, HTMLDivElement[]>, addAsAnnotation?: boolean) => Opt<Doc> = (color: string, isTargetToggler: boolean) => undefined;
+ public GetAnchor: (savedAnnotations: Opt<ObservableMap<number, HTMLDivElement[]>>, addAsAnnotation: boolean) => Opt<Doc> = (savedAnnotations: Opt<ObservableMap<number, HTMLDivElement[]>>, addAsAnnotation: boolean) => undefined;
public Delete: () => void = unimplementedFunction;
- public AddTag: (key: string, value: string) => boolean = returnFalse;
public PinToPres: () => void = unimplementedFunction;
- public MakePushpin: () => void = unimplementedFunction;
- public IsPushpin: () => boolean = returnFalse;
+ public MakeTargetToggle: () => void = unimplementedFunction;
+ public ShowTargetTrail: () => void = unimplementedFunction;
+ public IsTargetToggler: () => boolean = returnFalse;
public get Active() {
return this._left > 0;
}
@@ -70,16 +121,102 @@ export class AnchorMenu extends AntimodeMenu<AntimodeMenuProps> {
AnchorMenu.Instance._canFade = false;
}
+ componentWillUnmount() {
+ this._disposer?.();
+ this._disposer2?.();
+ }
+
componentDidMount() {
+ this._disposer2 = reaction(
+ () => this._opacity,
+ opacity => {
+ if (!opacity) {
+ this._showLinkPopup = false;
+ this.setGPTPopupVis(false);
+ this.setGPTPopupText('');
+ }
+ },
+ { fireImmediately: true }
+ );
this._disposer = reaction(
- () => SelectionManager.Views(),
+ () => SelectionManager.Views().slice(),
selected => {
this._showLinkPopup = false;
+ this.setGPTPopupVis(false);
+ this.setGPTPopupText('');
AnchorMenu.Instance.fadeOut(true);
}
);
}
+ /**
+ * Invokes the API with the selected text and stores it in the summarized text.
+ * @param e pointer down event
+ */
+ gptSummarize = async (e: React.PointerEvent) => {
+ this.setHighlightRange(undefined);
+ this.setGPTPopupVis(true);
+ this.setGPTMode(GPTPopupMode.SUMMARY);
+ this.setLoading(true);
+
+ try {
+ const res = await gptAPICall(this.selectedText, GPTCallType.SUMMARY);
+ if (res) {
+ this.setGPTPopupText(res);
+ } else {
+ this.setGPTPopupText('Something went wrong.');
+ }
+ } catch (err) {
+ console.error(err);
+ }
+
+ this.setLoading(false);
+ };
+
+ /**
+ * Makes a GPT call to edit selected text.
+ * @returns nothing
+ */
+ gptEdit = async () => {
+ if (!this.editorView) return;
+ this.setHighlightRange(undefined);
+ const state = this.editorView.state;
+ const sel = state.selection;
+ const fullText = state.doc.textBetween(0, this.editorView.state.doc.content.size, ' \n');
+ const selectedText = state.doc.textBetween(sel.from, sel.to);
+
+ this.setGPTPopupVis(true);
+ this.setGPTMode(GPTPopupMode.EDIT);
+ this.setLoading(true);
+
+ try {
+ let res = await gptAPICall(selectedText, GPTCallType.EDIT);
+ // let res = await this.mockGPTCall();
+ if (!res) return;
+ res = res.trim();
+ const resultText = fullText.slice(0, sel.from - 1) + res + fullText.slice(sel.to - 1);
+
+ if (res) {
+ this.setGPTPopupText(resultText);
+ this.setHighlightRange([sel.from - 1, sel.from - 1 + res.length]);
+ } else {
+ this.setGPTPopupText('Something went wrong.');
+ }
+ } catch (err) {
+ console.error(err);
+ }
+
+ this.setLoading(false);
+ };
+
+ /**
+ * Replaces text suggestions from GPT.
+ */
+ replaceText = (replacement: string) => {
+ if (!this.editorView || !this.textDoc) return;
+ this.textDoc.text = replacement;
+ };
+
pointerDown = (e: React.PointerEvent) => {
setupMoveUpEvents(
this,
@@ -112,9 +249,7 @@ export class AnchorMenu extends AntimodeMenu<AntimodeMenuProps> {
@action
highlightClicked = (e: React.MouseEvent) => {
- if (!this.Highlight(this.highlightColor, false) && this.Pinned) {
- this.Highlighting = !this.Highlighting;
- }
+ this.Highlight(this.highlightColor, false, undefined, true);
AnchorMenu.Instance.fadeOut(true);
};
@@ -129,7 +264,7 @@ export class AnchorMenu extends AntimodeMenu<AntimodeMenuProps> {
const button = (
<button className="antimodeMenu-button anchor-color-preview-button" title="" key="highlighter-button" onClick={this.highlightClicked}>
<div className="anchor-color-preview">
- <FontAwesomeIcon icon="highlighter" size="lg" style={{ transition: 'transform 0.1s', transform: this.Highlighting ? '' : 'rotate(-45deg)' }} />
+ <FontAwesomeIcon icon="highlighter" size="lg" style={{ transition: 'transform 0.1s', transform: 'rotate(-45deg)' }} />
<div className="color-preview" style={{ backgroundColor: this.highlightColor }}></div>
</div>
</button>
@@ -174,83 +309,116 @@ export class AnchorMenu extends AntimodeMenu<AntimodeMenuProps> {
this.highlightColor = Utils.colorString(col);
};
- @action keyChanged = (e: React.ChangeEvent<HTMLInputElement>) => {
- this._keyValue = e.currentTarget.value;
- };
- @action valueChanged = (e: React.ChangeEvent<HTMLInputElement>) => {
- this._valueValue = e.currentTarget.value;
+ /**
+ * Returns whether the selected text can be summarized. The goal is to have
+ * 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 = (): boolean => {
+ const docs = SelectionManager.Docs();
+ if (docs.length > 0) {
+ return docs.some(doc => doc.type === 'pdf' || doc.type === 'web');
+ }
+ return false;
};
- @action addTag = (e: React.PointerEvent) => {
- if (this._keyValue.length > 0 && this._valueValue.length > 0) {
- this._added = this.AddTag(this._keyValue, this._valueValue);
- setTimeout(
- action(() => (this._added = false)),
- 1000
- );
+
+ /**
+ * Returns whether the selected text can be edited.
+ * @returns Whether the GPT icon for summarization should appear
+ */
+ canEdit = (): boolean => {
+ const docs = SelectionManager.Docs();
+ if (docs.length > 0) {
+ return docs.some(doc => doc.type === 'rtf');
}
+ return false;
};
render() {
const buttons =
- this.Status === 'marquee'
- ? [
- this.highlighter,
-
- <Tooltip key="annotate" title={<div className="dash-tooltip">{'Drag to Place Annotation'}</div>}>
- <button className="antimodeMenu-button annotate" ref={this._commentCont} onPointerDown={this.pointerDown} style={{ cursor: 'grab' }}>
- <FontAwesomeIcon icon="comment-alt" size="lg" />
- </button>
- </Tooltip>,
- AnchorMenu.Instance.OnAudio === unimplementedFunction ? (
- <></>
- ) : (
- <Tooltip key="annoaudiotate" title={<div className="dash-tooltip">{'Click to Record Annotation'}</div>}>
- <button className="antimodeMenu-button annotate" onPointerDown={this.audioDown} style={{ cursor: 'grab' }}>
- <FontAwesomeIcon icon="microphone" size="lg" />
- </button>
- </Tooltip>
- ),
- //NOTE: link popup is currently in progress
- <Tooltip key="link" title={<div className="dash-tooltip">{'Find document to link to selected text'}</div>}>
- <button className="antimodeMenu-button link" onPointerDown={this.toggleLinkPopup} style={{}}>
- <FontAwesomeIcon style={{ position: 'absolute', transform: 'scale(1.5)' }} icon={'search'} size="lg" />
- <FontAwesomeIcon style={{ position: 'absolute', transform: 'scale(0.5)', transformOrigin: 'top left', top: 12, left: 12 }} icon={'link'} size="lg" />
- </button>
- </Tooltip>,
- <LinkPopup key="popup" showPopup={this._showLinkPopup} linkFrom={this.onMakeAnchor} />,
- AnchorMenu.Instance.StartCropDrag === unimplementedFunction ? (
- <></>
- ) : (
- <Tooltip key="crop" title={<div className="dash-tooltip">{'Click/Drag to create cropped image'}</div>}>
- <button className="antimodeMenu-button annotate" onPointerDown={this.cropDown} style={{ cursor: 'grab' }}>
- <FontAwesomeIcon icon="image" size="lg" />
- </button>
- </Tooltip>
- ),
- ]
- : [
- <Tooltip key="trash" title={<div className="dash-tooltip">{'Remove Link Anchor'}</div>}>
- <button className="antimodeMenu-button" onPointerDown={this.Delete}>
- <FontAwesomeIcon icon="trash-alt" size="lg" />
- </button>
- </Tooltip>,
- <Tooltip key="Pin" title={<div className="dash-tooltip">{'Pin to Presentation'}</div>}>
- <button className="antimodeMenu-button" onPointerDown={this.PinToPres}>
- <FontAwesomeIcon icon="map-pin" size="lg" />
- </button>
- </Tooltip>,
- <Tooltip key="pushpin" title={<div className="dash-tooltip">{'toggle pushpin behavior'}</div>}>
- <button className="antimodeMenu-button" style={{ color: this.IsPushpin() ? 'black' : 'white', backgroundColor: this.IsPushpin() ? 'white' : 'black' }} onPointerDown={this.MakePushpin}>
- <FontAwesomeIcon icon="thumbtack" size="lg" />
- </button>
- </Tooltip>,
- // <div key="7" className="anchorMenu-addTag" >
- // <input onChange={this.keyChanged} placeholder="Key" style={{ gridColumn: 1 }} />
- // <input onChange={this.valueChanged} placeholder="Value" style={{ gridColumn: 3 }} />
- // </div>,
- // <button key="8" className="antimodeMenu-button" title={`Add tag: ${this._keyValue} with value: ${this._valueValue}`} onPointerDown={this.addTag}>
- // <FontAwesomeIcon style={{ transition: "all .2s" }} color={this._added ? "#42f560" : "white"} icon="check" size="lg" /></button>,
- ];
+ this.Status === 'marquee' ? (
+ <>
+ {this.highlighter}
+ <Tooltip key="annotate" title={<div className="dash-tooltip">Drag to Place Annotation</div>}>
+ <button className="antimodeMenu-button annotate" ref={this._commentCont} onPointerDown={this.pointerDown} style={{ cursor: 'grab' }}>
+ <FontAwesomeIcon icon="comment-alt" size="lg" />
+ </button>
+ </Tooltip>
+ {/* GPT Summarize icon only shows up when text is highlighted, not on marquee selection*/}
+ {AnchorMenu.Instance.StartCropDrag === unimplementedFunction && this.canSummarize() && (
+ <Tooltip key="gpt" title={<div className="dash-tooltip">Summarize with AI</div>}>
+ <button className="antimodeMenu-button annotate" onPointerDown={this.gptSummarize} style={{ cursor: 'grab' }}>
+ <FontAwesomeIcon icon="comment-dots" size="lg" />
+ </button>
+ </Tooltip>
+ )}
+ <GPTPopup
+ key="gptpopup"
+ visible={this.showGPTPopup}
+ text={this.GPTpopupText}
+ highlightRange={this.highlightRange}
+ loading={this.loadingGPT}
+ callSummaryApi={this.gptSummarize}
+ callEditApi={this.gptEdit}
+ replaceText={this.replaceText}
+ mode={this.GPTMode}
+ />
+ {AnchorMenu.Instance.OnAudio === unimplementedFunction ? null : (
+ <Tooltip key="annoaudiotate" title={<div className="dash-tooltip">Click to Record Annotation</div>}>
+ <button className="antimodeMenu-button annotate" onPointerDown={this.audioDown} style={{ cursor: 'grab' }}>
+ <FontAwesomeIcon icon="microphone" size="lg" />
+ </button>
+ </Tooltip>
+ )}
+ {this.canEdit() && (
+ <Tooltip key="gpttextedit" title={<div className="dash-tooltip">AI edit suggestions</div>}>
+ <button className="antimodeMenu-button annotate" onPointerDown={this.gptEdit} style={{ cursor: 'grab' }}>
+ <FontAwesomeIcon icon="pencil-alt" size="lg" />
+ </button>
+ </Tooltip>
+ )}
+ <Tooltip key="link" title={<div className="dash-tooltip">Find document to link to selected text</div>}>
+ <button className="antimodeMenu-button link" onPointerDown={this.toggleLinkPopup}>
+ <FontAwesomeIcon style={{ position: 'absolute', transform: 'scale(1.5)' }} icon={'search'} size="lg" />
+ <FontAwesomeIcon style={{ position: 'absolute', transform: 'scale(0.5)', transformOrigin: 'top left', top: 12, left: 12 }} icon={'link'} size="lg" />
+ </button>
+ </Tooltip>
+ <LinkPopup key="popup" showPopup={this._showLinkPopup} linkCreateAnchor={this.onMakeAnchor} />,
+ {AnchorMenu.Instance.StartCropDrag === unimplementedFunction ? null : (
+ <Tooltip key="crop" title={<div className="dash-tooltip">Click/Drag to create cropped image</div>}>
+ <button className="antimodeMenu-button annotate" onPointerDown={this.cropDown} style={{ cursor: 'grab' }}>
+ <FontAwesomeIcon icon="image" size="lg" />
+ </button>
+ </Tooltip>
+ )}
+ </>
+ ) : (
+ <>
+ <Tooltip key="trash" title={<div className="dash-tooltip">Remove Link Anchor</div>}>
+ <button className="antimodeMenu-button" style={{ display: this.Delete === returnFalse ? 'none' : undefined }} onPointerDown={this.Delete}>
+ <FontAwesomeIcon icon="trash-alt" size="lg" />
+ </button>
+ </Tooltip>
+ <Tooltip key="Pin" title={<div className="dash-tooltip">Pin to Presentation</div>}>
+ <button className="antimodeMenu-button" style={{ display: this.PinToPres === returnFalse ? 'none' : undefined }} onPointerDown={this.PinToPres}>
+ <FontAwesomeIcon icon="map-pin" size="lg" />
+ </button>
+ </Tooltip>
+ <Tooltip key="trail" title={<div className="dash-tooltip">Show Linked Trail</div>}>
+ <button className="antimodeMenu-button" style={{ display: this.ShowTargetTrail === returnFalse ? 'none' : undefined }} onPointerDown={this.ShowTargetTrail}>
+ <FontAwesomeIcon icon="taxi" size="lg" />
+ </button>
+ </Tooltip>
+ <Tooltip key="toggle" title={<div className="dash-tooltip">make target visibility toggle on click</div>}>
+ <button
+ className="antimodeMenu-button"
+ style={{ display: this.IsTargetToggler === returnFalse ? 'none' : undefined, color: this.IsTargetToggler() ? 'black' : 'white', backgroundColor: this.IsTargetToggler() ? 'white' : 'black' }}
+ onPointerDown={this.MakeTargetToggle}>
+ <FontAwesomeIcon icon="thumbtack" size="lg" />
+ </button>
+ </Tooltip>
+ </>
+ );
return this.getElement(buttons);
}
diff --git a/src/client/views/pdf/Annotation.tsx b/src/client/views/pdf/Annotation.tsx
index ee418a02f..db6b1f011 100644
--- a/src/client/views/pdf/Annotation.tsx
+++ b/src/client/views/pdf/Annotation.tsx
@@ -1,12 +1,13 @@
import React = require('react');
-import { action, computed, IReactionDisposer, observable, reaction, runInAction } from 'mobx';
+import { action, computed } from 'mobx';
import { observer } from 'mobx-react';
import { Doc, DocListCast, Opt } from '../../../fields/Doc';
import { Id } from '../../../fields/FieldSymbols';
import { List } from '../../../fields/List';
-import { BoolCast, Cast, NumCast, StrCast } from '../../../fields/Types';
+import { BoolCast, Cast, DocCast, NumCast, StrCast } from '../../../fields/Types';
import { LinkFollower } from '../../util/LinkFollower';
import { undoBatch } from '../../util/UndoManager';
+import { OpenWhere } from '../nodes/DocumentView';
import { FieldViewProps } from '../nodes/FieldView';
import { AnchorMenu } from './AnchorMenu';
import './Annotation.scss';
@@ -22,7 +23,7 @@ interface IAnnotationProps extends FieldViewProps {
export class Annotation extends React.Component<IAnnotationProps> {
render() {
return (
- <div>
+ <div style={{ display: this.props.anno.textCopied && !Doc.isBrushedHighlightedDegree(this.props.anno) ? 'none' : undefined }}>
{DocListCast(this.props.anno.textInlineAnnotations).map(a => (
<RegionAnnotation pointerEvents={this.props.pointerEvents} {...this.props} document={a} key={a[Id]} />
))}
@@ -52,12 +53,20 @@ class RegionAnnotation extends React.Component<IRegionAnnotationProps> {
};
@undoBatch
- pinToPres = () => this.props.pinToPres(this.annoTextRegion);
+ pinToPres = () => this.props.pinToPres(this.annoTextRegion, {});
@undoBatch
- makePushpin = () => (this.annoTextRegion.isPushpin = !this.annoTextRegion.isPushpin);
+ makeTargretToggle = () => (this.annoTextRegion.followLinkToggle = !this.annoTextRegion.followLinkToggle);
- isPushpin = () => BoolCast(this.annoTextRegion.isPushpin);
+ isTargetToggler = () => BoolCast(this.annoTextRegion.followLinkToggle);
+ @undoBatch
+ showTargetTrail = (anchor: Doc) => {
+ const trail = DocCast(anchor.presTrail);
+ if (trail) {
+ Doc.ActivePresentation = trail;
+ this.props.addDocTab(trail, OpenWhere.replaceRight);
+ }
+ };
@action
onPointerDown = (e: React.PointerEvent) => {
@@ -65,24 +74,18 @@ class RegionAnnotation extends React.Component<IRegionAnnotationProps> {
AnchorMenu.Instance.Status = 'annotation';
AnchorMenu.Instance.Delete = this.deleteAnnotation.bind(this);
AnchorMenu.Instance.Pinned = false;
- AnchorMenu.Instance.AddTag = this.addTag.bind(this);
AnchorMenu.Instance.PinToPres = this.pinToPres;
- AnchorMenu.Instance.MakePushpin = this.makePushpin;
- AnchorMenu.Instance.IsPushpin = this.isPushpin;
+ AnchorMenu.Instance.MakeTargetToggle = this.makeTargretToggle;
+ AnchorMenu.Instance.IsTargetToggler = this.isTargetToggler;
+ AnchorMenu.Instance.ShowTargetTrail = () => this.showTargetTrail(this.annoTextRegion);
AnchorMenu.Instance.jumpTo(e.clientX, e.clientY, true);
e.stopPropagation();
} else if (e.button === 0) {
e.stopPropagation();
- LinkFollower.FollowLink(undefined, this.annoTextRegion, this.props, false);
+ LinkFollower.FollowLink(undefined, this.annoTextRegion, false);
}
};
- addTag = (key: string, value: string): boolean => {
- const valNum = parseInt(value);
- this.annoTextRegion[key] = isNaN(valNum) ? value : valNum;
- return true;
- };
-
render() {
const brushed = this.annoTextRegion && Doc.isBrushedHighlightedDegree(this.annoTextRegion);
return (
diff --git a/src/client/views/pdf/GPTPopup/GPTPopup.scss b/src/client/views/pdf/GPTPopup/GPTPopup.scss
new file mode 100644
index 000000000..44413ede7
--- /dev/null
+++ b/src/client/views/pdf/GPTPopup/GPTPopup.scss
@@ -0,0 +1,132 @@
+$textgrey: #707070;
+$lighttextgrey: #a3a3a3;
+$greyborder: #d3d3d3;
+$lightgrey: #ececec;
+$button: #5b97ff;
+$highlightedText: #82e0ff;
+
+.summary-box {
+ display: flex;
+ flex-direction: column;
+ justify-content: space-between;
+ background-color: #ffffff;
+ box-shadow: 0 2px 5px #7474748d;
+ color: $textgrey;
+ position: fixed;
+ bottom: 10px;
+ right: 10px;
+ width: 250px;
+ min-height: 200px;
+ border-radius: 15px;
+ padding: 15px;
+ padding-bottom: 0;
+ z-index: 999;
+
+ .summary-heading {
+ display: flex;
+ align-items: center;
+ border-bottom: 1px solid $greyborder;
+ padding-bottom: 5px;
+
+ .summary-text {
+ font-size: 12px;
+ font-weight: 500;
+ }
+ }
+
+ label {
+ color: $textgrey;
+ font-size: 12px;
+ font-weight: 400;
+ letter-spacing: 1px;
+ margin: 0;
+ padding-right: 5px;
+ }
+
+ a {
+ cursor: pointer;
+ }
+
+ .content-wrapper {
+ padding-top: 10px;
+ min-height: 50px;
+ max-height: 150px;
+ overflow-y: auto;
+ }
+
+ .btns-wrapper {
+ height: 50px;
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+
+ .summarizing {
+ display: flex;
+ align-items: center;
+ }
+ }
+
+ button {
+ font-size: 9px;
+ padding: 10px;
+ color: #ffffff;
+ background-color: $button;
+ border-radius: 5px;
+ }
+
+ .text-btn {
+ &:hover {
+ background-color: $button;
+ }
+ }
+
+ .btn-secondary {
+ font-size: 8px;
+ padding: 10px 5px;
+ background-color: $lightgrey;
+ color: $textgrey;
+ &:hover {
+ background-color: $lightgrey;
+ }
+ }
+
+ .icon-btn {
+ background-color: #ffffff;
+ padding: 10px;
+ border-radius: 50%;
+ color: $button;
+ border: 1px solid $button;
+ }
+
+ .ai-warning {
+ padding: 10px 0;
+ font-size: 10px;
+ color: $lighttextgrey;
+ border-top: 1px solid $greyborder;
+ }
+
+ .highlighted-text {
+ background-color: $highlightedText;
+ }
+}
+
+// Typist CSS
+.Typist .Cursor {
+ display: inline-block;
+}
+.Typist .Cursor--blinking {
+ opacity: 1;
+ animation: blink 1s linear infinite;
+}
+
+@keyframes blink {
+ 0% {
+ opacity: 1;
+ }
+ 50% {
+ opacity: 0;
+ }
+ 100% {
+ opacity: 1;
+ }
+}
diff --git a/src/client/views/pdf/GPTPopup/GPTPopup.tsx b/src/client/views/pdf/GPTPopup/GPTPopup.tsx
new file mode 100644
index 000000000..8bd060d4f
--- /dev/null
+++ b/src/client/views/pdf/GPTPopup/GPTPopup.tsx
@@ -0,0 +1,188 @@
+import React = require('react');
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+import { action, observable } from 'mobx';
+import { observer } from 'mobx-react';
+import ReactLoading from 'react-loading';
+import Typist from 'react-typist';
+import { Doc } from '../../../../fields/Doc';
+import { Docs } from '../../../documents/Documents';
+import './GPTPopup.scss';
+
+export enum GPTPopupMode {
+ SUMMARY,
+ EDIT,
+}
+
+interface GPTPopupProps {
+ visible: boolean;
+ text: string;
+ loading: boolean;
+ mode: GPTPopupMode;
+ callSummaryApi: (e: React.PointerEvent) => Promise<void>;
+ callEditApi: (e: React.PointerEvent) => Promise<void>;
+ replaceText: (replacement: string) => void;
+ highlightRange?: number[];
+}
+
+@observer
+export class GPTPopup extends React.Component<GPTPopupProps> {
+ static Instance: GPTPopup;
+
+ @observable
+ private done: boolean = false;
+ @observable
+ private sidebarId: string = '';
+
+ @action
+ public setDone = (done: boolean) => {
+ this.done = done;
+ };
+ @action
+ public setSidebarId = (id: string) => {
+ this.sidebarId = id;
+ };
+
+ public addDoc: (doc: Doc | Doc[], sidebarKey?: string | undefined) => boolean = () => false;
+
+ /**
+ * Transfers the summarization text to a sidebar annotation text document.
+ */
+ private transferToText = () => {
+ const newDoc = Docs.Create.TextDocument(this.props.text.trim(), {
+ _width: 200,
+ _height: 50,
+ _layout_fitWidth: true,
+ _layout_autoHeight: true,
+ });
+ this.addDoc(newDoc, this.sidebarId);
+ };
+
+ constructor(props: GPTPopupProps) {
+ super(props);
+ GPTPopup.Instance = this;
+ }
+
+ componentDidUpdate = () => {
+ if (this.props.loading) {
+ this.setDone(false);
+ }
+ };
+
+ summaryBox = () => (
+ <>
+ <div>
+ {this.heading('SUMMARY')}
+ <div className="content-wrapper">
+ {!this.props.loading &&
+ (!this.done ? (
+ <Typist
+ key={this.props.text}
+ avgTypingDelay={15}
+ cursor={{ hideWhenDone: true }}
+ onTypingDone={() => {
+ setTimeout(() => {
+ this.setDone(true);
+ }, 500);
+ }}>
+ {this.props.text}
+ </Typist>
+ ) : (
+ this.props.text
+ ))}
+ </div>
+ </div>
+ {!this.props.loading && (
+ <div className="btns-wrapper">
+ {this.done ? (
+ <>
+ <button className="icon-btn" onPointerDown={e => this.props.callSummaryApi(e)}>
+ <FontAwesomeIcon icon="redo-alt" size="lg" />
+ </button>
+ <button
+ className="text-btn"
+ onClick={e => {
+ this.transferToText();
+ }}>
+ Transfer to Text
+ </button>
+ </>
+ ) : (
+ <div className="summarizing">
+ <span>Summarizing</span>
+ <ReactLoading type="bubbles" color="#bcbcbc" width={20} height={20} />
+ <button
+ className="btn-secondary"
+ onClick={e => {
+ this.setDone(true);
+ }}>
+ Stop Animation
+ </button>
+ </div>
+ )}
+ </div>
+ )}
+ </>
+ );
+
+ editBox = () => {
+ const hr = this.props.highlightRange;
+ return (
+ <>
+ <div>
+ {this.heading('TEXT EDIT SUGGESTIONS')}
+ <div className="content-wrapper">
+ {hr && (
+ <div>
+ {this.props.text.slice(0, hr[0])} <span className="highlighted-text">{this.props.text.slice(hr[0], hr[1])}</span> {this.props.text.slice(hr[1])}
+ </div>
+ )}
+ </div>
+ </div>
+ {hr && !this.props.loading && (
+ <>
+ <div className="btns-wrapper">
+ <>
+ <button className="icon-btn" onPointerDown={e => this.props.callEditApi(e)}>
+ <FontAwesomeIcon icon="redo-alt" size="lg" />
+ </button>
+ <button
+ className="text-btn"
+ onClick={e => {
+ this.props.replaceText(this.props.text);
+ }}>
+ Replace Text
+ </button>
+ </>
+ </div>
+ {this.aiWarning()}
+ </>
+ )}
+ </>
+ );
+ };
+
+ aiWarning = () =>
+ this.done ? (
+ <div className="ai-warning">
+ <FontAwesomeIcon icon="exclamation-circle" size="sm" style={{ paddingRight: '5px' }} />
+ AI generated responses can contain inaccurate or misleading content.
+ </div>
+ ) : (
+ <></>
+ );
+
+ heading = (headingText: string) => (
+ <div className="summary-heading">
+ <label className="summary-text">{headingText}</label>
+ {this.props.loading && <ReactLoading type="spin" color="#bcbcbc" width={14} height={14} />}
+ </div>
+ );
+
+ render() {
+ return (
+ <div className="summary-box" style={{ display: this.props.visible ? 'flex' : 'none' }}>
+ {this.props.mode === GPTPopupMode.SUMMARY ? this.summaryBox() : this.editBox()}
+ </div>
+ );
+ }
+}
diff --git a/src/client/views/pdf/PDFViewer.tsx b/src/client/views/pdf/PDFViewer.tsx
index abc7336bd..8f6b8cd41 100644
--- a/src/client/views/pdf/PDFViewer.tsx
+++ b/src/client/views/pdf/PDFViewer.tsx
@@ -2,12 +2,13 @@ import { action, computed, IReactionDisposer, observable, ObservableMap, reactio
import { observer } from 'mobx-react';
import * as Pdfjs from 'pdfjs-dist';
import 'pdfjs-dist/web/pdf_viewer.css';
-import { Doc, DocListCast, Field, HeightSym, Opt } from '../../../fields/Doc';
+import { Doc, DocListCast, Field, Opt } from '../../../fields/Doc';
+import { 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, OmitKeys, smoothScroll, Utils } from '../../../Utils';
+import { addStyleSheet, addStyleSheetRule, clearStyleSheetRules, emptyFunction, returnAll, returnFalse, returnNone, returnTrue, returnZero, smoothScroll, Utils } from '../../../Utils';
import { DocUtils } from '../../documents/Documents';
import { DragManager } from '../../util/DragManager';
import { SelectionManager } from '../../util/SelectionManager';
@@ -16,7 +17,7 @@ import { SnappingManager } from '../../util/SnappingManager';
import { MarqueeOptionsMenu } from '../collections/collectionFreeForm';
import { CollectionFreeFormView } from '../collections/collectionFreeForm/CollectionFreeFormView';
import { MarqueeAnnotator } from '../MarqueeAnnotator';
-import { DocumentViewProps } from '../nodes/DocumentView';
+import { DocFocusOptions, DocumentViewProps } from '../nodes/DocumentView';
import { FieldViewProps } from '../nodes/FieldView';
import { LinkDocPreview } from '../nodes/LinkDocPreview';
import { StyleProp } from '../StyleProvider';
@@ -24,13 +25,14 @@ import { AnchorMenu } from './AnchorMenu';
import { Annotation } from './Annotation';
import './PDFViewer.scss';
import React = require('react');
+import { GPTPopup } from './GPTPopup/GPTPopup';
const PDFJSViewer = require('pdfjs-dist/web/pdf_viewer');
const pdfjsLib = require('pdfjs-dist');
const _global = (window /* browser */ || global) /* node */ as any;
//pdfjsLib.GlobalWorkerOptions.workerSrc = `/assets/pdf.worker.js`;
// The workerSrc property shall be specified.
-pdfjsLib.GlobalWorkerOptions.workerSrc = 'https://unpkg.com/pdfjs-dist@2.14.305/build/pdf.worker.js';
+pdfjsLib.GlobalWorkerOptions.workerSrc = 'https://unpkg.com/pdfjs-dist@2.16.105/build/pdf.worker.js';
interface IViewerProps extends FieldViewProps {
Document: Doc;
@@ -40,9 +42,10 @@ interface IViewerProps extends FieldViewProps {
fieldKey: string;
pdf: Pdfjs.PDFDocumentProxy;
url: string;
+ sidebarAddDoc: (doc: Doc | Doc[], sidebarKey?: string | undefined) => boolean;
loaded?: (nw: number, nh: number, np: number) => void;
setPdfViewer: (view: PDFViewer) => void;
- anchorMenuClick?: () => undefined | ((anchor: Doc) => void);
+ anchorMenuClick?: () => undefined | ((anchor: Doc, summarize?: boolean) => void);
crop: (region: Doc | undefined, addCrop?: boolean) => Doc | undefined;
}
@@ -68,23 +71,27 @@ export class PDFViewer extends React.Component<IViewerProps> {
private _annotationLayer: React.RefObject<HTMLDivElement> = React.createRef();
private _disposers: { [name: string]: IReactionDisposer } = {};
private _viewer: React.RefObject<HTMLDivElement> = React.createRef();
- public _getAnchor: (savedAnnotations?: ObservableMap<number, HTMLDivElement[]>) => Opt<Doc> = () => undefined;
_mainCont: React.RefObject<HTMLDivElement> = React.createRef();
private _selectionText: string = '';
+ private _selectionContent: DocumentFragment | undefined;
private _downX: number = 0;
private _downY: number = 0;
private _lastSearch = false;
private _viewerIsSetup = false;
private _ignoreScroll = false;
- private _initialScroll: Opt<number>;
+ private _initialScroll: { loc: Opt<number>; easeFunc: 'linear' | 'ease' | undefined } | undefined;
private _forcedScroll = true;
+ get _getAnchor() {
+ return AnchorMenu.Instance?.GetAnchor;
+ }
selectionText = () => this._selectionText;
+ selectionContent = () => this._selectionContent;
@observable isAnnotating = false;
// key where data is stored
@computed get allAnnotations() {
- return DocUtils.FilterDocs(DocListCast(this.props.dataDoc[this.props.fieldKey + '-annotations']), this.props.docFilters(), this.props.docRangeFilters(), undefined);
+ return DocUtils.FilterDocs(DocListCast(this.props.dataDoc[this.props.fieldKey + '_annotations']), this.props.childFilters(), this.props.childFiltersByRanges());
}
@computed get inlineTextAnnotations() {
return this.allAnnotations.filter(a => a.textInlineAnnotations);
@@ -95,12 +102,12 @@ export class PDFViewer extends React.Component<IViewerProps> {
this.setupPdfJsViewer();
this._mainCont.current?.addEventListener('scroll', e => ((e.target as any).scrollLeft = 0));
- this._disposers.autoHeight = reaction(
- () => this.props.layoutDoc._autoHeight,
- autoHeight => {
- if (autoHeight) {
- 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));
+ this._disposers.layout_autoHeight = reaction(
+ () => this.props.layoutDoc._layout_autoHeight,
+ layout_autoHeight => {
+ if (layout_autoHeight) {
+ 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));
}
}
);
@@ -117,7 +124,7 @@ export class PDFViewer extends React.Component<IViewerProps> {
{ fireImmediately: true }
);
this._disposers.curPage = reaction(
- () => Cast(this.props.Document._curPage, 'number', null),
+ () => Cast(this.props.Document._layout_curPage, 'number', null),
page => page !== undefined && page !== this._pdfViewer?.currentPageNumber && this.gotoPage(page),
{ fireImmediately: true }
);
@@ -131,10 +138,17 @@ export class PDFViewer extends React.Component<IViewerProps> {
copy = (e: ClipboardEvent) => {
if (this.props.isContentActive() && e.clipboardData) {
e.clipboardData.setData('text/plain', this._selectionText);
+ const anchor = this._getAnchor(undefined, false);
+ if (anchor) {
+ anchor.textCopied = true;
+ e.clipboardData.setData('dash/pdfAnchor', anchor[Id]);
+ }
e.preventDefault();
}
};
+ @observable _scrollHeight = 0;
+
@action
initialLoad = async () => {
if (this._pageSizes.length === 0) {
@@ -155,25 +169,27 @@ export class PDFViewer extends React.Component<IViewerProps> {
)
)
);
- this.props.Document.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);
+
// scrolls to focus on a nested annotation document. if this is part a link preview then it will jump to the scroll location,
// otherwise it will scroll smoothly.
- scrollFocus = (doc: Doc, smooth: boolean) => {
+ scrollFocus = (doc: Doc, scrollTop: number, options: DocFocusOptions) => {
const mainCont = this._mainCont.current;
let focusSpeed: Opt<number>;
if (doc !== this.props.rootDoc && mainCont) {
const windowHeight = this.props.PanelHeight() / (this.props.NativeDimScaling?.() || 1);
- const scrollTo = doc.unrendered ? NumCast(doc.y) : Utils.scrollIntoView(NumCast(doc.y), doc[HeightSym](), NumCast(this.props.layoutDoc._scrollTop), windowHeight, 0.1 * windowHeight, NumCast(this.props.Document.scrollHeight));
- if (scrollTo !== undefined && scrollTo !== this.props.layoutDoc._scrollTop) {
- if (!this._pdfViewer) this._initialScroll = scrollTo;
- else if (smooth) smoothScroll((focusSpeed = NumCast(doc.focusSpeed, 500)), mainCont, scrollTo);
+ const scrollTo = Utils.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);
else this._mainCont.current?.scrollTo({ top: Math.abs(scrollTo || 0) });
}
} else {
- this._initialScroll = NumCast(this.props.layoutDoc._scrollTop);
+ this._initialScroll = { loc: NumCast(this.props.layoutDoc._layout_scrollTop), easeFunc: options.easeFunc };
}
return focusSpeed;
};
@@ -193,17 +209,22 @@ export class PDFViewer extends React.Component<IViewerProps> {
pagesinit = () => {
if (this._pdfViewer._setDocumentViewerElement?.offsetParent) {
- runInAction(() => (this._pdfViewer.currentScaleValue = this.props.layoutDoc._viewScale = 1));
- this.gotoPage(NumCast(this.props.Document._curPage, 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: string | undefined = this._initialScroll ? this._initialScroll.toString() : '';
+ var 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),
+ { fireImmediately: true }
+ );
this._disposers.scroll = reaction(
- () => Math.abs(NumCast(this.props.Document._scrollTop)),
+ () => Math.abs(NumCast(this.props.Document._layout_scrollTop)),
pos => {
if (!this._ignoreScroll) {
this._showWaiting && this.setupPdfJsViewer();
- const viewTrans = quickScroll ?? StrCast(this.props.Document._viewTransition);
+ const viewTrans = quickScroll?.loc ?? StrCast(this.props.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;
@@ -211,7 +232,7 @@ export class PDFViewer extends React.Component<IViewerProps> {
if (duration) {
setTimeout(
() => {
- this._mainCont.current && smoothScroll(duration, this._mainCont.current, pos);
+ this._mainCont.current && (this._scrollStopper = smoothScroll(duration, this._mainCont.current, pos, this._initialScroll?.easeFunc ?? 'ease', this._scrollStopper));
setTimeout(() => (this._forcedScroll = false), duration);
},
this._mainCont.current ? 0 : 250
@@ -226,7 +247,7 @@ export class PDFViewer extends React.Component<IViewerProps> {
);
quickScroll = undefined;
if (this._initialScroll !== undefined && this._mainCont.current) {
- this._mainCont.current?.scrollTo({ top: Math.abs(this._initialScroll || 0) });
+ this._mainCont.current?.scrollTo({ top: Math.abs(this._initialScroll?.loc || 0) });
this._initialScroll = undefined;
}
};
@@ -285,7 +306,7 @@ export class PDFViewer extends React.Component<IViewerProps> {
@action
scrollToAnnotation = (scrollToAnnotation: Doc) => {
if (scrollToAnnotation) {
- this.scrollFocus(scrollToAnnotation, true);
+ this.scrollFocus(scrollToAnnotation, NumCast(scrollToAnnotation.y), { zoomTime: 500 });
Doc.linkFollowHighlight(scrollToAnnotation);
}
};
@@ -296,12 +317,12 @@ export class PDFViewer extends React.Component<IViewerProps> {
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 (!LinkDocPreview.LinkInfo) {
- this.props.layoutDoc._scrollTop = this._mainCont.current.scrollTop;
+ this.props.layoutDoc._layout_scrollTop = this._mainCont.current.scrollTop;
}
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.DocumentView?.().ComponentView?.getAnchor!()!, false);
+ DocUtils.MakeLinkToActiveAudio(() => this.props.DocumentView?.().ComponentView?.getAnchor!(true)!, false);
this._scrollTimer = undefined;
}, 200);
}
@@ -351,7 +372,7 @@ export class PDFViewer extends React.Component<IViewerProps> {
// if alt+left click, drag and annotate
this._downX = e.clientX;
this._downY = e.clientY;
- if ((this.props.Document._viewScale || 1) !== 1) return;
+ if ((this.props.Document._freeform_scale || 1) !== 1) return;
if ((e.button !== 0 || e.altKey) && this.props.isContentActive(true)) {
this._setPreviewCursor?.(e.clientX, e.clientY, true, false);
}
@@ -363,7 +384,6 @@ export class PDFViewer extends React.Component<IViewerProps> {
const target = e.target as any;
if (e.target && (target.className.includes('endOfContent') || (target.parentElement.className !== 'textLayer' && target.parentElement.parentElement?.className !== 'textLayer'))) {
this._textSelecting = false;
- document.addEventListener('pointermove', this.onSelectMove); // need this to prevent document from being dragged if stopPropagation doesn't get called
} else {
// if textLayer is hit, then we select text instead of using a marquee so clear out the marquee.
setTimeout(
@@ -373,35 +393,37 @@ export class PDFViewer extends React.Component<IViewerProps> {
this._styleRule = addStyleSheetRule(PDFViewer._annotationStyle, 'htmlAnnotation', { 'pointer-events': 'none' });
document.addEventListener('pointerup', this.onSelectEnd);
- document.addEventListener('pointermove', this.onSelectMove);
}
}
};
@action
finishMarquee = (x?: number, y?: number) => {
- this._getAnchor = AnchorMenu.Instance?.GetAnchor;
this.isAnnotating = false;
this._marqueeing = undefined;
this._textSelecting = true;
- document.removeEventListener('pointermove', this.onSelectMove);
};
- onSelectMove = (e: PointerEvent) => e.stopPropagation();
-
@action
onSelectEnd = (e: PointerEvent): void => {
this.isAnnotating = false;
clearStyleSheetRules(PDFViewer._annotationStyle);
this.props.select(false);
- document.removeEventListener('pointermove', this.onSelectMove);
document.removeEventListener('pointerup', this.onSelectEnd);
const sel = window.getSelection();
+ if (sel) {
+ AnchorMenu.Instance.setSelectedText(sel.toString());
+ }
+
if (sel?.type === 'Range') {
this.createTextAnnotation(sel, sel.getRangeAt(0));
AnchorMenu.Instance.jumpTo(e.clientX, e.clientY);
}
+
+ // Changing which document to add the annotation to (the currently selected PDF)
+ GPTPopup.Instance.setSidebarId('data_sidebar');
+ GPTPopup.Instance.addDoc = this.props.sidebarAddDoc;
};
@action
@@ -411,9 +433,9 @@ export class PDFViewer extends React.Component<IViewerProps> {
const clientRects = selRange.getClientRects();
for (let i = 0; i < clientRects.length; i++) {
const rect = clientRects.item(i);
- if (rect && rect.width !== this._mainCont.current.clientWidth && rect.width) {
+ if (rect?.width && rect.width < this._mainCont.current.clientWidth / this.props.ScreenToLocalTransform().Scale) {
const scaleX = this._mainCont.current.offsetWidth / boundingRect.width;
- const pdfScale = NumCast(this.props.layoutDoc._viewScale, 1);
+ const pdfScale = NumCast(this.props.layoutDoc._freeform_scale, 1);
const annoBox = document.createElement('div');
annoBox.className = 'marqueeAnnotator-annotationBox';
// transforms the positions from screen onto the pdf div
@@ -425,7 +447,8 @@ export class PDFViewer extends React.Component<IViewerProps> {
}
}
}
- this._selectionText = selRange.cloneContents().textContent || '';
+ this._selectionContent = selRange.cloneContents();
+ this._selectionText = this._selectionContent?.textContent || '';
// clear selection
if (sel.empty) {
@@ -437,11 +460,8 @@ export class PDFViewer extends React.Component<IViewerProps> {
}
};
- scrollXf = () => {
- return this._mainCont.current ? this.props.ScreenToLocalTransform().translate(0, NumCast(this.props.layoutDoc._scrollTop)) : this.props.ScreenToLocalTransform();
- };
-
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) {
this._setPreviewCursor(e.clientX, e.clientY, false, false);
}
@@ -458,19 +478,23 @@ export class PDFViewer extends React.Component<IViewerProps> {
if (e.ctrlKey) {
const curScale = Number(this._pdfViewer.currentScaleValue);
this._pdfViewer.currentScaleValue = Math.max(1, Math.min(10, curScale - (curScale * e.deltaY) / 1000));
- this.props.layoutDoc._viewScale = Number(this._pdfViewer.currentScaleValue);
+ this.props.layoutDoc._freeform_scale = Number(this._pdfViewer.currentScaleValue);
}
}
};
- pointerEvents = () => (this.props.isContentActive() && this.props.pointerEvents?.() !== 'none' && !MarqueeOptionsMenu.Instance.isShown() ? 'all' : SnappingManager.GetIsDragging() ? undefined : 'none');
+ pointerEvents = () =>
+ this.props.isContentActive() && !MarqueeOptionsMenu.Instance.isShown()
+ ? 'all' //
+ : 'none';
@computed get annotationLayer() {
return (
- <div className="pdfViewerDash-annotationLayer" style={{ height: Doc.NativeHeight(this.props.Document), transform: `scale(${NumCast(this.props.layoutDoc._viewScale, 1)})` }} ref={this._annotationLayer}>
+ <div className="pdfViewerDash-annotationLayer" style={{ height: Doc.NativeHeight(this.props.Document), transform: `scale(${NumCast(this.props.layoutDoc._freeform_scale, 1)})` }} ref={this._annotationLayer}>
{this.inlineTextAnnotations
.sort((a, b) => NumCast(a.y) - NumCast(b.y))
+ .filter(anno => !anno.hidden)
.map(anno => (
- <Annotation {...this.props} fieldKey={this.props.fieldKey + '-annotations'} pointerEvents={this.pointerEvents} showInfo={this.showInfo} dataDoc={this.props.dataDoc} anno={anno} key={`${anno[Id]}-annotation`} />
+ <Annotation {...this.props} fieldKey={this.props.fieldKey + '_annotations'} pointerEvents={this.pointerEvents} showInfo={this.showInfo} dataDoc={this.props.dataDoc} anno={anno} key={`${anno[Id]}-annotation`} />
))}
</div>
);
@@ -480,48 +504,55 @@ export class PDFViewer extends React.Component<IViewerProps> {
return !this._overlayAnnoInfo ? null : (
<div className="pdfViewerDash-overlayAnno" style={{ top: NumCast(this._overlayAnnoInfo.y), left: NumCast(this._overlayAnnoInfo.x) }}>
<div className="pdfViewerDash-overlayAnno" style={{ right: -50, background: SharingManager.Instance.users.find(users => users.user.email === this._overlayAnnoInfo!.author)?.userColor }}>
- {this._overlayAnnoInfo.author + ' ' + Field.toString(this._overlayAnnoInfo.creationDate as Field)}
+ {this._overlayAnnoInfo.author + ' ' + Field.toString(this._overlayAnnoInfo.author_date as Field)}
</div>
</div>
);
}
+ getScrollHeight = () => this._scrollHeight;
showInfo = action((anno: Opt<Doc>) => (this._overlayAnnoInfo = anno));
- overlayTransform = () => this.scrollXf().scale(1 / NumCast(this.props.layoutDoc._viewScale, 1));
- panelWidth = () => this.props.PanelWidth() / (this.props.NativeDimScaling?.() || 1); // (this.Document.scrollHeight || Doc.NativeHeight(this.Document) || 0);
- panelHeight = () => this.props.PanelHeight() / (this.props.NativeDimScaling?.() || 1); // () => this._pageSizes.length && this._pageSizes[0] ? this._pageSizes[0].width : Doc.NativeWidth(this.Document);
- transparentFilter = () => [...this.props.docFilters(), Utils.IsTransparentFilter()];
- opaqueFilter = () => [...this.props.docFilters(), Utils.noDragsDocFilter, ...(DragManager.docsBeingDragged.length ? [] : [Utils.IsOpaqueFilter()])];
+ scrollXf = () => (this._mainCont.current ? this.props.ScreenToLocalTransform().translate(0, NumCast(this.props.layoutDoc._layout_scrollTop)) : this.props.ScreenToLocalTransform());
+ 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.IsTransparentFilter()];
+ opaqueFilter = () => [...this.props.childFilters(), Utils.noDragsDocFilter, ...(DragManager.docsBeingDragged.length && this.props.isContentActive() ? [] : [Utils.IsOpaqueFilter()])];
childStyleProvider = (doc: Doc | undefined, props: Opt<DocumentViewProps>, property: string): any => {
if (doc instanceof Doc && property === StyleProp.PointerEvents) {
- if (doc.textInlineAnnotations) return 'none';
+ if (this.inlineTextAnnotations.includes(doc) || this.props.isContentActive() === false) return 'none';
return 'all';
}
return this.props.styleProvider?.(doc, props, property);
};
- renderAnnotations = (docFilters?: () => string[], mixBlendMode?: any, display?: string) => (
+ renderAnnotations = (childFilters: () => string[], mixBlendMode?: any, display?: string) => (
<div
className="pdfViewerDash-overlay"
style={{
mixBlendMode: mixBlendMode,
display: display,
- transform: `scale(${NumCast(this.props.layoutDoc._viewScale, 1)})`,
pointerEvents: Doc.ActiveTool !== InkTool.None ? 'all' : undefined,
}}>
<CollectionFreeFormView
- {...OmitKeys(this.props, ['NativeWidth', 'NativeHeight', 'setContentView']).omit}
+ {...this.props}
+ NativeWidth={returnZero}
+ NativeHeight={returnZero}
+ setContentView={emptyFunction} // override setContentView to do nothing
+ pointerEvents={SnappingManager.GetIsDragging() && this.props.isContentActive() ? returnAll : returnNone} // freeform view doesn't get events unless something is being dragged onto it.
+ childPointerEvents={this.props.isContentActive() !== false ? 'all' : 'none'} // but freeform children need to get events to allow text editing, etc
renderDepth={this.props.renderDepth + 1}
isAnnotationOverlay={true}
- fieldKey={this.props.fieldKey + '-annotations'}
- CollectionView={undefined}
+ fieldKey={this.props.fieldKey + '_annotations'}
+ getScrollHeight={this.getScrollHeight}
setPreviewCursor={this.setPreviewCursor}
setBrushViewer={this.setBrushViewer}
PanelHeight={this.panelHeight}
PanelWidth={this.panelWidth}
ScreenToLocalTransform={this.overlayTransform}
- dropAction={'alias'}
- docFilters={docFilters}
+ isAnyChildContentActive={returnFalse}
+ isAnnotationOverlayScrollable={true}
+ childFilters={childFilters}
select={emptyFunction}
bringToFront={emptyFunction}
styleProvider={this.childStyleProvider}
@@ -529,14 +560,14 @@ export class PDFViewer extends React.Component<IViewerProps> {
</div>
);
@computed get overlayTransparentAnnotations() {
- return this.renderAnnotations(this.transparentFilter, 'multiply', DragManager.docsBeingDragged.length ? 'none' : undefined);
+ return this.renderAnnotations(this.transparentFilter, 'multiply', DragManager.docsBeingDragged.length && this.props.isContentActive() ? 'none' : undefined);
}
@computed get overlayOpaqueAnnotations() {
return this.renderAnnotations(this.opaqueFilter, this.allAnnotations.some(anno => anno.mixBlendMode) ? 'hard-light' : undefined);
}
@computed get overlayLayer() {
return (
- <div style={{ pointerEvents: SnappingManager.GetIsDragging() ? 'all' : 'none' }}>
+ <div style={{ pointerEvents: this.props.isContentActive() && SnappingManager.GetIsDragging() ? 'all' : 'none' }}>
{this.overlayTransparentAnnotations}
{this.overlayOpaqueAnnotations}
</div>
@@ -558,8 +589,8 @@ export class PDFViewer extends React.Component<IViewerProps> {
onPointerDown={this.onPointerDown}
onClick={this.onClick}
style={{
- overflowX: NumCast(this.props.layoutDoc._viewScale, 1) !== 1 ? 'scroll' : undefined,
- height: !this.props.Document._fitWidth && window.screen.width > 600 ? Doc.NativeHeight(this.props.Document) : `100%`,
+ overflowX: NumCast(this.props.layoutDoc._freeform_scale, 1) !== 1 ? 'scroll' : undefined,
+ height: !this.props.Document._layout_fitWidth && window.screen.width > 600 ? Doc.NativeHeight(this.props.Document) : `100%`,
}}>
{this.pdfViewerDiv}
{this.annotationLayer}