diff options
34 files changed, 562 insertions, 603 deletions
diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts index b32cbd3d0..df08345f9 100644 --- a/src/client/documents/Documents.ts +++ b/src/client/documents/Documents.ts @@ -280,7 +280,7 @@ export namespace Docs { }], [DocumentType.WEB, { layout: { view: WebBox, dataField: defaultDataKey }, - options: { _height: 300 } + options: { _height: 300, scrollHeight: 100000, _fitWidth: true } }], [DocumentType.COL, { layout: { view: CollectionView, dataField: defaultDataKey }, @@ -304,7 +304,7 @@ export namespace Docs { }], [DocumentType.PDF, { layout: { view: PDFBox, dataField: defaultDataKey }, - options: { _curPage: 1 } + options: { _curPage: 1, _fitWidth: true } }], [DocumentType.IMPORT, { layout: { view: DirectoryImportBox, dataField: defaultDataKey }, @@ -764,11 +764,16 @@ export namespace Docs { } export function PdfDocument(url: string, options: DocumentOptions = {}) { - return InstanceFromProto(Prototypes.get(DocumentType.PDF), new PdfField(new URL(url)), options); + const pdfProto = Prototypes.get(DocumentType.PDF); + pdfProto._fitWidth = true; // backward compatibility -- can be removed after db is reset + return InstanceFromProto(pdfProto, new PdfField(new URL(url)), options); } export function WebDocument(url: string, options: DocumentOptions = {}) { - return InstanceFromProto(Prototypes.get(DocumentType.WEB), url ? new WebField(new URL(url)) : undefined, { _fitWidth: true, _chromeStatus: url ? "disabled" : "enabled", isAnnotating: false, _lockedTransform: true, ...options }); + const webProto = Prototypes.get(DocumentType.WEB); + webProto.scrollHeight = 100000; // backward compatibility -- can be removed after db is reset + webProto._fitWidth = true; // backward compatibility -- can be removed after db is reset + return InstanceFromProto(webProto, url ? new WebField(new URL(url)) : undefined, { _chromeStatus: url ? "disabled" : "enabled", isAnnotating: false, _lockedTransform: true, ...options }); } export function HtmlDocument(html: string, options: DocumentOptions = {}) { @@ -1110,7 +1115,6 @@ export namespace DocUtils { } if (type.indexOf("pdf") !== -1) { ctor = Docs.Create.PdfDocument; - if (!options._fitWidth) options._fitWidth = true; if (!options._width) options._width = 400; if (!options._height) options._height = options._width * 1200 / 927; } @@ -1131,7 +1135,7 @@ export namespace DocUtils { }); } ctor = Docs.Create.WebDocument; - options = { ...options, _fitWidth: true, _width: 400, _height: 512, title: path, }; + options = { ...options, _width: 400, _height: 512, title: path, }; } return ctor ? ctor(path, options) : undefined; } diff --git a/src/client/util/CurrentUserUtils.ts b/src/client/util/CurrentUserUtils.ts index 382b225a4..f683ac2a0 100644 --- a/src/client/util/CurrentUserUtils.ts +++ b/src/client/util/CurrentUserUtils.ts @@ -444,7 +444,7 @@ export class CurrentUserUtils { { _width: 250, _height: 250, title: "container", system: true, cloneFieldFilter: new List<string>(["system"]) }); } if (doc.emptyWebpage === undefined) { - doc.emptyWebpage = Docs.Create.WebDocument("", { title: "webpage", _nativeWidth: 850, _fitWidth: true, isTemplateDoc: true, _height: 512, _width: 400, useCors: true, system: true, cloneFieldFilter: new List<string>(["system"]) }); + doc.emptyWebpage = Docs.Create.WebDocument("", { title: "webpage", _nativeWidth: 850, isTemplateDoc: true, _height: 512, _width: 400, useCors: true, system: true, cloneFieldFilter: new List<string>(["system"]) }); } if (doc.activeMobileMenu === undefined) { this.setupActiveMobileMenu(doc); diff --git a/src/client/util/DragManager.ts b/src/client/util/DragManager.ts index 52ccfda74..7b4d43793 100644 --- a/src/client/util/DragManager.ts +++ b/src/client/util/DragManager.ts @@ -105,14 +105,14 @@ export namespace DragManager { constructor(aborted: boolean, dragData: { [id: string]: any }) { this.aborted = aborted; this.docDragData = dragData instanceof DocumentDragData ? dragData : undefined; - this.annoDragData = dragData instanceof PdfAnnoDragData ? dragData : undefined; + this.annoDragData = dragData instanceof AnchorAnnoDragData ? dragData : undefined; this.linkDragData = dragData instanceof LinkDragData ? dragData : undefined; this.columnDragData = dragData instanceof ColumnDragData ? dragData : undefined; } linkDocument?: Doc; aborted: boolean; docDragData?: DocumentDragData; - annoDragData?: PdfAnnoDragData; + annoDragData?: AnchorAnnoDragData; linkDragData?: LinkDragData; columnDragData?: ColumnDragData; } @@ -152,19 +152,21 @@ export namespace DragManager { } colKey: SchemaHeaderField; } - // used by PDFs to conditionally (if the drop completes) create a text annotation when dragging from the PDF toolbar when a text region has been selected. + // used by PDFs,Text,Image,Video,Web to conditionally (if the drop completes) create a text annotation when dragging the annotate button from the AnchorMenu when a text/region selection has been made. // this is pretty clunky and should be rethought out using linkDrag or DocumentDrag - export class PdfAnnoDragData { - constructor(dragDoc: Doc, annotationDoc: Doc, dropDoc: Doc) { + export class AnchorAnnoDragData { + constructor(dragDoc: Doc, annotationDocCreator: () => Doc, dropDocCreator: () => Doc) { this.dragDocument = dragDoc; - this.dropDocument = dropDoc; - this.annotationDocument = annotationDoc; + this.dropDocCreator = dropDocCreator; + this.annotationDocCreator = annotationDocCreator; this.offset = [0, 0]; } targetContext: Doc | undefined; dragDocument: Doc; - annotationDocument: Doc; - dropDocument: Doc; + annotationDocCreator: () => Doc; + dropDocCreator: () => Doc; + dropDocument?: Doc; + annotationDocument?: Doc; offset: number[]; dropAction: dropActionType; userDropAction: dropActionType; @@ -250,7 +252,7 @@ export namespace DragManager { } // drag&drop the pdf annotation anchor which will create a text note on drop via a dropCompleted() DragOption - export function StartPdfAnnoDrag(eles: HTMLElement[], dragData: PdfAnnoDragData, downX: number, downY: number, options?: DragOptions) { + export function StartAnchorAnnoDrag(eles: HTMLElement[], dragData: AnchorAnnoDragData, downX: number, downY: number, options?: DragOptions) { StartDrag(eles, dragData, downX, downY, options); } @@ -353,7 +355,7 @@ export namespace DragManager { const xs: number[] = []; const ys: number[] = []; - docsBeingDragged = dragData instanceof DocumentDragData ? dragData.draggedDocuments : dragData instanceof PdfAnnoDragData ? [dragData.dragDocument] : []; + docsBeingDragged = dragData instanceof DocumentDragData ? dragData.draggedDocuments : dragData instanceof AnchorAnnoDragData ? [dragData.dragDocument] : []; const elesCont = { left: Number.MAX_SAFE_INTEGER, top: Number.MAX_SAFE_INTEGER, diff --git a/src/client/util/HypothesisUtils.ts b/src/client/util/HypothesisUtils.ts index 7a449b882..8ddfce772 100644 --- a/src/client/util/HypothesisUtils.ts +++ b/src/client/util/HypothesisUtils.ts @@ -21,7 +21,7 @@ export namespace Hypothesis { export const getSourceWebDoc = async (uri: string) => { const result = await findWebDoc(uri); console.log(result ? "existing doc found" : "existing doc NOT found"); - return result || Docs.Create.WebDocument(uri, { title: uri, _fitWidth: true, _nativeWidth: 850, _height: 512, _width: 400, useCors: true }); // create and return a new Web doc with given uri if no matching docs are found + return result || Docs.Create.WebDocument(uri, { title: uri, _nativeWidth: 850, _height: 512, _width: 400, useCors: true }); // create and return a new Web doc with given uri if no matching docs are found }; diff --git a/src/client/views/DocumentButtonBar.tsx b/src/client/views/DocumentButtonBar.tsx index a2bd0aad9..eeef94d74 100644 --- a/src/client/views/DocumentButtonBar.tsx +++ b/src/client/views/DocumentButtonBar.tsx @@ -155,7 +155,7 @@ export class DocumentButtonBar extends React.Component<{ views: () => (DocumentV e.preventDefault(); let googleDoc = await Cast(dataDoc.googleDoc, Doc); if (!googleDoc) { - const options = { _width: 600, _fitWidth: true, _nativeWidth: 960, _nativeHeight: 800, isAnnotating: false, useCors: false }; + const options = { _width: 600, _nativeWidth: 960, _nativeHeight: 800, isAnnotating: false, useCors: false }; googleDoc = Docs.Create.WebDocument(googleDocUrl, options); dataDoc.googleDoc = googleDoc; } diff --git a/src/client/views/GlobalKeyHandler.ts b/src/client/views/GlobalKeyHandler.ts index 8a07c5321..1c5277de0 100644 --- a/src/client/views/GlobalKeyHandler.ts +++ b/src/client/views/GlobalKeyHandler.ts @@ -22,7 +22,7 @@ import { DocumentDecorations } from "./DocumentDecorations"; import { InkStrokeProperties } from "./InkStrokeProperties"; import { MainView } from "./MainView"; import { DocumentLinksButton } from "./nodes/DocumentLinksButton"; -import { PDFMenu } from "./pdf/PDFMenu"; +import { AnchorMenu } from "./pdf/AnchorMenu"; import { SnappingManager } from "../util/SnappingManager"; import { SearchBox } from "./search/SearchBox"; import { random } from "lodash"; @@ -261,7 +261,7 @@ export class KeyManager { } break; case "c": - if (!PDFMenu.Instance.Active && DocumentDecorations.Instance.Bounds.r - DocumentDecorations.Instance.Bounds.x > 2) { + if (!AnchorMenu.Instance.Active && DocumentDecorations.Instance.Bounds.r - DocumentDecorations.Instance.Bounds.x > 2) { const bds = DocumentDecorations.Instance.Bounds; const pt = SelectionManager.Views()[0].props.ScreenToLocalTransform().transformPoint(bds.x + (bds.r - bds.x) / 2, bds.y + (bds.b - bds.y) / 2); const text = `__DashCloneId(${pt?.[0] || 0},${pt?.[1] || 0}):` + SelectionManager.Views().map(dv => dv.Document[Id]).join(":"); diff --git a/src/client/views/InkingStroke.tsx b/src/client/views/InkingStroke.tsx index 8df7f7eef..19b23af13 100644 --- a/src/client/views/InkingStroke.tsx +++ b/src/client/views/InkingStroke.tsx @@ -6,7 +6,7 @@ import { InkData, InkField, InkTool } from "../../fields/InkField"; import { makeInterface } from "../../fields/Schema"; import { Cast, StrCast } from "../../fields/Types"; import { TraceMobx } from "../../fields/util"; -import { setupMoveUpEvents } from "../../Utils"; +import { setupMoveUpEvents, emptyFunction, returnFalse } from "../../Utils"; import { CognitiveServices } from "../cognitive_services/CognitiveServices"; import { InteractionUtils } from "../util/InteractionUtils"; import { Scripting } from "../util/Scripting"; @@ -87,6 +87,11 @@ export class InkingStroke extends ViewBoxBaseComponent<FieldViewProps, InkDocume } } + onPointerDown = (e: React.PointerEvent) => { + this.props.isSelected(true) && setupMoveUpEvents(this, e, returnFalse, emptyFunction, action((e: PointerEvent, doubleTap: boolean | undefined) => { + doubleTap && InkStrokeProperties.Instance && (InkStrokeProperties.Instance._controlBtn = true); + })); + } public static MaskDim = 50000; render() { @@ -196,6 +201,7 @@ export class InkingStroke extends ViewBoxBaseComponent<FieldViewProps, InkDocume mixBlendMode: this.layoutDoc.tool === InkTool.Highlighter ? "multiply" : "unset", overflow: "visible", }} + onPointerDown={this.onPointerDown} onContextMenu={() => { const cm = ContextMenu.Instance; if (cm) { diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx index c1fafe3e6..bd5db62ef 100644 --- a/src/client/views/MainView.tsx +++ b/src/client/views/MainView.tsx @@ -55,7 +55,7 @@ import { RadialMenu } from './nodes/RadialMenu'; import { TaskCompletionBox } from './nodes/TaskCompletedBox'; import { WebBox } from './nodes/WebBox'; import { OverlayView } from './OverlayView'; -import { PDFMenu } from './pdf/PDFMenu'; +import { AnchorMenu } from './pdf/AnchorMenu'; import { PreviewCursor } from './PreviewCursor'; import { PropertiesView } from './PropertiesView'; import { SearchBox } from './search/SearchBox'; @@ -615,7 +615,7 @@ export class MainView extends React.Component { <TaskCompletionBox /> <ContextMenu /> <RadialMenu /> - <PDFMenu /> + <AnchorMenu /> <MarqueeOptionsMenu /> <OverlayView /> <TimelineMenu /> diff --git a/src/client/views/MarqueeAnnotator.scss b/src/client/views/MarqueeAnnotator.scss new file mode 100644 index 000000000..c90d48a65 --- /dev/null +++ b/src/client/views/MarqueeAnnotator.scss @@ -0,0 +1,12 @@ + +.marqueeAnnotator-annotationBox { + position: absolute; + background-color: rgba(245, 230, 95, 0.616); +} + + +.marqueeAnnotator-dragBox { + position:absolute; + background-color: transparent; + opacity: 0.1; +}
\ No newline at end of file diff --git a/src/client/views/MarqueeAnnotator.tsx b/src/client/views/MarqueeAnnotator.tsx new file mode 100644 index 000000000..0ab2d1ecf --- /dev/null +++ b/src/client/views/MarqueeAnnotator.tsx @@ -0,0 +1,215 @@ +import { action, observable, runInAction } from "mobx"; +import { observer } from "mobx-react"; +import { Dictionary } from "typescript-collections"; +import { AclAddonly, AclAdmin, AclEdit, DataSym, Doc, Opt } from "../../fields/Doc"; +import { Id } from "../../fields/FieldSymbols"; +import { GetEffectiveAcl } from "../../fields/util"; +import { DocUtils, Docs } from "../documents/Documents"; +import { CurrentUserUtils } from "../util/CurrentUserUtils"; +import { DragManager } from "../util/DragManager"; +import { FormattedTextBox } from "./nodes/formattedText/FormattedTextBox"; +import { AnchorMenu } from "./pdf/AnchorMenu"; +import "./MarqueeAnnotator.scss"; +import React = require("react"); +import { undoBatch } from "../util/UndoManager"; +import { NumCast } from "../../fields/Types"; +import { DocumentType } from "../documents/DocumentTypes"; +import { List } from "../../fields/List"; +const _global = (window /* browser */ || global /* node */) as any; + +export interface MarqueeAnnotatorProps { + rootDoc: Doc; + down: number[]; + scaling?: () => number; + mainCont: HTMLDivElement; + savedAnnotations: Dictionary<number, HTMLDivElement[]>; + annotationLayer: HTMLDivElement; + addDocument: (doc: Doc) => boolean; + getPageFromScroll?: (top: number) => number; + finishMarquee: () => void; +} +@observer +export class MarqueeAnnotator extends React.Component<MarqueeAnnotatorProps> { + private _startX: number = 0; + private _startY: number = 0; + @observable private _left: number = 0; + @observable private _top: number = 0; + @observable private _width: number = 0; + @observable private _height: number = 0; + + constructor(props: any) { + super(props); + runInAction(() => { + AnchorMenu.Instance.Status = "marquee"; + AnchorMenu.Instance.fadeOut(true); + // clear out old marquees and initialize menu for new selection + this.props.savedAnnotations.values().forEach(v => v.forEach(a => a.remove())); + this.props.savedAnnotations.clear(); + }); + } + + @action componentDidMount() { + // set marquee x and y positions to the spatially transformed position + const boundingRect = this.props.mainCont.getBoundingClientRect(); + this._startX = this._left = (this.props.down[0] - boundingRect.left) * (this.props.mainCont.offsetWidth / boundingRect.width); + this._startY = this._top = (this.props.down[1] - boundingRect.top) * (this.props.mainCont.offsetHeight / boundingRect.height) + this.props.mainCont.scrollTop; + this._height = this._width = 0; + document.addEventListener("pointermove", this.onSelectMove); + document.addEventListener("pointerup", this.onSelectEnd); + } + componentWillUnmount() { + document.removeEventListener("pointermove", this.onSelectMove); + document.removeEventListener("pointerup", this.onSelectEnd); + } + + @undoBatch + @action + makeAnnotationDocument = (color: string): Opt<Doc> => { + if (this.props.savedAnnotations.size() === 0) return undefined; + if ((this.props.savedAnnotations.values()[0][0] as any).marqueeing) { + const scale = this.props.scaling?.() || 1; + const anno = this.props.savedAnnotations.values()[0][0]; + const mainAnnoDoc = Docs.Create.FreeformDocument([], { backgroundColor: color, annotationOn: this.props.rootDoc, title: "Annotation on " + this.props.rootDoc.title }); + if (anno.style.left) mainAnnoDoc.x = parseInt(anno.style.left) / scale; + if (anno.style.top) mainAnnoDoc.y = (NumCast(this.props.rootDoc._scrollTop) + parseInt(anno.style.top)) / scale; + if (anno.style.height) mainAnnoDoc._height = parseInt(anno.style.height) / scale; + if (anno.style.width) mainAnnoDoc._width = parseInt(anno.style.width) / scale; + mainAnnoDoc.group = mainAnnoDoc; + anno.remove(); + this.props.savedAnnotations.clear(); + return mainAnnoDoc; + } else { + const mainAnnoDoc = Docs.Create.FreeformDocument([], { type: DocumentType.PDFANNO, annotationOn: this.props.rootDoc, title: "Selection on " + this.props.rootDoc.title, _width: 1, _height: 1 }); + const mainAnnoDocProto = Doc.GetProto(mainAnnoDoc); + + let maxX = -Number.MAX_VALUE; + let minY = Number.MAX_VALUE; + const annoDocs: Doc[] = []; + this.props.savedAnnotations.forEach((key: number, value: HTMLDivElement[]) => value.map(anno => { + const annoDoc = new Doc(); + if (anno.style.left) annoDoc.x = parseInt(anno.style.left); + if (anno.style.top) annoDoc.y = parseInt(anno.style.top); + if (anno.style.height) annoDoc._height = parseInt(anno.style.height); + if (anno.style.width) annoDoc._width = parseInt(anno.style.width); + annoDoc.group = mainAnnoDoc; + annoDoc.backgroundColor = color; + annoDocs.push(annoDoc); + anno.remove(); + (annoDoc.y !== undefined) && (minY = Math.min(NumCast(annoDoc.y), minY)); + (annoDoc.x !== undefined) && (maxX = Math.max(NumCast(annoDoc.x) + NumCast(annoDoc._width), maxX)); + })); + + mainAnnoDocProto.y = Math.max(minY, 0); + mainAnnoDocProto.x = Math.max(maxX, 0); + // mainAnnoDocProto.text = this._selectionText; + mainAnnoDocProto.annotations = new List<Doc>(annoDocs); + this.props.savedAnnotations.clear(); + return mainAnnoDoc; + } + } + @action + highlight = (color: string) => { + // creates annotation documents for current highlights + const effectiveAcl = GetEffectiveAcl(this.props.rootDoc[DataSym]); + const annotationDoc = [AclAddonly, AclEdit, AclAdmin].includes(effectiveAcl) && this.makeAnnotationDocument(color); + annotationDoc && this.props.addDocument(annotationDoc); + return annotationDoc as Doc ?? undefined; + } + + public static previewNewAnnotation = action((savedAnnotations: Dictionary<number, HTMLDivElement[]>, annotationLayer: HTMLDivElement, div: HTMLDivElement, page: number) => { + if (div.style.top) { + div.style.top = (parseInt(div.style.top)/*+ this.getScrollFromPage(page)*/).toString(); + } + annotationLayer.append(div); + div.style.backgroundColor = "#ACCEF7"; + div.style.opacity = "0.5"; + const savedPage = savedAnnotations.getValue(page); + if (savedPage) { + savedPage.push(div); + savedAnnotations.setValue(page, savedPage); + } + else { + savedAnnotations.setValue(page, [div]); + } + }); + + @action + onSelectMove = (e: PointerEvent) => { + // transform positions and find the width and height to set the marquee to + const boundingRect = this.props.mainCont.getBoundingClientRect(); + this._width = ((e.clientX - boundingRect.left) * (this.props.mainCont.offsetWidth / boundingRect.width)) - this._startX; + this._height = ((e.clientY - boundingRect.top) * (this.props.mainCont.offsetHeight / boundingRect.height)) - this._startY + this.props.mainCont.scrollTop; + this._left = Math.min(this._startX, this._startX + this._width); + this._top = Math.min(this._startY, this._startY + this._height); + this._width = Math.abs(this._width); + this._height = Math.abs(this._height); + e.stopPropagation(); + } + + onSelectEnd = (e: PointerEvent) => { + if (!e.ctrlKey) { + AnchorMenu.Instance.Marquee = { left: this._left, top: this._top, width: this._width, height: this._height }; + } + + AnchorMenu.Instance.Highlight = this.highlight; + /** + * This function is used by the AnchorMenu to create an anchor highlight and a new linked text annotation. + * It also initiates a Drag/Drop interaction to place the text annotation. + */ + AnchorMenu.Instance.StartDrag = action(async (e: PointerEvent, ele: HTMLElement) => { + e.preventDefault(); + e.stopPropagation(); + const targetCreator = () => { + const target = CurrentUserUtils.GetNewTextDoc("Note linked to " + this.props.rootDoc.title, 0, 0, 100, 100); + FormattedTextBox.SelectOnLoad = target[Id]; + return target; + } + const anchorCreator = () => { + const annoDoc = this.highlight("rgba(173, 216, 230, 0.75)"); // hyperlink color + annoDoc.isLinkButton = true; // prevents link button from showing up --- maybe not a good thing? + this.props.addDocument(annoDoc); + return annoDoc; + } + DragManager.StartAnchorAnnoDrag([ele], new DragManager.AnchorAnnoDragData(this.props.rootDoc, anchorCreator, targetCreator), e.pageX, e.pageY, { + dragComplete: e => { + if (!e.aborted && e.annoDragData && e.annoDragData.annotationDocument && e.annoDragData.dropDocument && !e.linkDocument) { + e.linkDocument = DocUtils.MakeLink({ doc: e.annoDragData.annotationDocument }, { doc: e.annoDragData.dropDocument }, "Annotation"); + e.annoDragData.annotationDocument.isPushpin = e.annoDragData?.dropDocument.annotationOn === this.props.rootDoc; + } + e.linkDocument && e.annoDragData?.linkDropCallback?.(e as { linkDocument: Doc });// bcz: typescript can't figure out that this is valid even though we tested e.linkDocument + } + }); + }); + + if (this._width > 10 || this._height > 10) { // configure and show the annotation/link menu if a the drag region is big enough + const marquees = this.props.mainCont.getElementsByClassName("marqueeAnnotator-dragBox"); + if (marquees?.length) { // copy the temporary marquee to allow for multiple selections (not currently available though). + const copy = document.createElement("div"); + ["left", "top", "width", "height", "border", "opacity"].forEach(prop => copy.style[prop as any] = (marquees[0] as HTMLDivElement).style[prop as any]); + copy.className = "marqueeAnnotator-annotationBox"; + (copy as any).marqueeing = true; + MarqueeAnnotator.previewNewAnnotation(this.props.savedAnnotations, this.props.annotationLayer, copy, this.props.getPageFromScroll?.(this._top) || 0); + } + + AnchorMenu.Instance.jumpTo(e.clientX, e.clientY); + + if (AnchorMenu.Instance.Highlighting) {// when highlighter has been toggled when menu is pinned, we auto-highlight immediately on mouse up + this.highlight("rgba(245, 230, 95, 0.75)"); // yellowish highlight color for highlighted text (should match AnchorMenu's highlight color) + } + } else { + runInAction(() => this._width = this._height = 0); + } + this.props.finishMarquee(); + } + + render() { + return <div className="marqueeAnnotator-dragBox" + style={{ + left: `${this._left}px`, top: `${this._top}px`, + width: `${this._width}px`, height: `${this._height}px`, + border: `${this._width === 0 ? "" : "2px dashed black"}`, + opacity: 0.2 + }}> + </div>; + } +} diff --git a/src/client/views/PreviewCursor.tsx b/src/client/views/PreviewCursor.tsx index ac61dd1d8..5b57ad19f 100644 --- a/src/client/views/PreviewCursor.tsx +++ b/src/client/views/PreviewCursor.tsx @@ -49,7 +49,7 @@ export class PreviewCursor extends React.Component<{}> { else if (re.test(plain)) { const url = plain; undoBatch(() => PreviewCursor._addDocument(Docs.Create.WebDocument(url, { - title: url, _fitWidth: true, _width: 500, _height: 300, useCors: true, x: newPoint[0], y: newPoint[1] + title: url, _width: 500, _height: 300, useCors: true, x: newPoint[0], y: newPoint[1] })))(); } else if (plain.startsWith("__DashDocId(") || plain.startsWith("__DashCloneId(")) { diff --git a/src/client/views/PropertiesButtons.tsx b/src/client/views/PropertiesButtons.tsx index 4413d28f5..9a836313c 100644 --- a/src/client/views/PropertiesButtons.tsx +++ b/src/client/views/PropertiesButtons.tsx @@ -153,7 +153,7 @@ export class PropertiesButtons extends React.Component<{}, {}> { e.preventDefault(); let googleDoc = await Cast(dataDoc.googleDoc, Doc); if (!googleDoc) { - const options = { _width: 600, _fitWidth: true, _nativeWidth: 960, _nativeHeight: 800, isAnnotating: false, useCors: false }; + const options = { _width: 600, _nativeWidth: 960, _nativeHeight: 800, isAnnotating: false, useCors: false }; googleDoc = Docs.Create.WebDocument(googleDocUrl, options); dataDoc.googleDoc = googleDoc; } diff --git a/src/client/views/StyleProvider.scss b/src/client/views/StyleProvider.scss index df63288f1..94001730c 100644 --- a/src/client/views/StyleProvider.scss +++ b/src/client/views/StyleProvider.scss @@ -3,7 +3,7 @@ width: 20; height: 20; position: absolute; - right: -5; + right: -25; top: -5; background: transparent; pointer-events: all; diff --git a/src/client/views/animationtimeline/Timeline.tsx b/src/client/views/animationtimeline/Timeline.tsx index 093310755..66afad0ac 100644 --- a/src/client/views/animationtimeline/Timeline.tsx +++ b/src/client/views/animationtimeline/Timeline.tsx @@ -12,6 +12,7 @@ import "./Timeline.scss"; import { TimelineOverview } from "./TimelineOverview"; import { Track } from "./Track"; import clamp from "../../util/clamp"; +import { DocumentType } from "../../documents/DocumentTypes"; /** * Timeline class controls most of timeline functions besides individual keyframe and track mechanism. Main functions are @@ -75,7 +76,7 @@ export class Timeline extends React.Component<FieldViewProps> { */ @computed private get children(): Doc[] { - const annotatedDoc = ["image", "video", "pdf"].includes(StrCast(this.props.Document.type)); + const annotatedDoc = [DocumentType.IMG, DocumentType.VID, DocumentType.PDF].includes(StrCast(this.props.Document.type) as any); if (annotatedDoc) { return DocListCast(this.props.Document[Doc.LayoutFieldKey(this.props.Document) + "-annotations"]); } diff --git a/src/client/views/collections/CollectionSubView.tsx b/src/client/views/collections/CollectionSubView.tsx index d7b9d9745..287bc56c2 100644 --- a/src/client/views/collections/CollectionSubView.tsx +++ b/src/client/views/collections/CollectionSubView.tsx @@ -224,7 +224,9 @@ export function CollectionSubView<T, X>(schemaCtor: (doc: Doc) => T, moreProps?: } else if (de.complete.annoDragData && (!this.props.isAnnotationOverlay || de.complete.annoDragData.dragDocument === this.props.Document)) { e.stopPropagation(); - return this.addDocument(de.complete.annoDragData.dropDocument); + de.complete.annoDragData.annotationDocument = de.complete.annoDragData.annotationDocCreator(); + de.complete.annoDragData.dropDocument = de.complete.annoDragData.dropDocCreator(); + return de.complete.annoDragData.dropDocument && this.addDocument(de.complete.annoDragData.dropDocument); } return false; } @@ -266,7 +268,7 @@ export function CollectionSubView<T, X>(schemaCtor: (doc: Doc) => T, moreProps?: } }); } else { - this.addDocument(Docs.Create.WebDocument(href, { ...options, _fitWidth: true, title: href })); + this.addDocument(Docs.Create.WebDocument(href, { ...options, title: href })); } } else if (text) { this.addDocument(Docs.Create.TextDocument(text, { ...options, _showTitle: StrCast(Doc.UserDoc().showTitle), _width: 100, _height: 25 })); @@ -383,7 +385,6 @@ export function CollectionSubView<T, X>(schemaCtor: (doc: Doc) => T, moreProps?: console.log("Adding ..."); const newDoc = Docs.Create.WebDocument(uriList.split("#annotations:")[0], {// clean hypothes.is URLs that reference a specific annotation (eg. https://en.wikipedia.org/wiki/Cartoon#annotations:t7qAeNbCEeqfG5972KR2Ig) ...options, - _fitWidth: true, title: uriList.split("#annotations:")[0], _width: 400, _height: 512, diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx index f934fcd92..2bc716928 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx @@ -43,7 +43,6 @@ import { CollectionViewType } from "../CollectionView"; import { computePivotLayout, computerPassLayout, computerStarburstLayout, computeTimelineLayout, PoolData, ViewDefBounds, ViewDefResult } from "./CollectionFreeFormLayoutEngines"; import { CollectionFreeFormRemoteCursors } from "./CollectionFreeFormRemoteCursors"; import "./CollectionFreeFormView.scss"; -import { MarqueeOptionsMenu } from "./MarqueeOptionsMenu"; import { MarqueeView } from "./MarqueeView"; import React = require("react"); import { CurrentUserUtils } from "../../../util/CurrentUserUtils"; @@ -267,8 +266,8 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P @undoBatch @action - internalPdfAnnoDrop(e: Event, annoDragData: DragManager.PdfAnnoDragData, xp: number, yp: number) { - const dragDoc = annoDragData.dropDocument; + internalPdfAnnoDrop(e: Event, annoDragData: DragManager.AnchorAnnoDragData, xp: number, yp: number) { + const dragDoc = annoDragData.dropDocument!; const dropPos = [NumCast(dragDoc.x), NumCast(dragDoc.y)]; dragDoc.x = xp - annoDragData.offset[0] + (NumCast(dragDoc.x) - dropPos[0]); dragDoc.y = yp - annoDragData.offset[1] + (NumCast(dragDoc.y) - dropPos[1]); @@ -299,7 +298,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P onInternalDrop = (e: Event, de: DragManager.DropEvent) => { const [xp, yp] = this.getTransform().transformPoint(de.x, de.y); if (this.isAnnotationOverlay !== true && de.complete.linkDragData) return this.internalLinkDrop(e, de, de.complete.linkDragData, xp, yp); - if (de.complete.annoDragData?.dropDocument && super.onInternalDrop(e, de)) return this.internalPdfAnnoDrop(e, de.complete.annoDragData, xp, yp); + if (de.complete.annoDragData?.dragDocument && super.onInternalDrop(e, de)) return this.internalPdfAnnoDrop(e, de.complete.annoDragData, xp, yp); if (de.complete.docDragData?.droppedDocuments.length) return this.internalDocDrop(e, de, de.complete.docDragData, xp, yp); return false; } @@ -813,7 +812,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P else if (this.props.active(true)) { e.stopPropagation(); if (!e.ctrlKey && MarqueeView.DragMarquee) this.setPan(this.panX() + e.deltaX, this.panY() + e.deltaY, "None", true); - else this.zoom(e.clientX, e.clientY, e.deltaY); + else if (!this.props.isAnnotationOverlay) this.zoom(e.clientX, e.clientY, e.deltaY); } this.props.Document.targetScale = NumCast(this.props.Document[this.scaleFieldKey]); } @@ -945,17 +944,18 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P const newAfterFocus = (didFocus: boolean) => { afterFocus && setTimeout(() => { // @ts-ignore - if (afterFocus?.(didFocus || (newPanX !== savedState.px || newPanY !== savedState.py))) { + if (afterFocus?.(!dontCenter && (didFocus || (newPanX !== savedState.px || newPanY !== savedState.py)))) { this.Document._panX = savedState.px; this.Document._panY = savedState.py; this.Document[this.scaleFieldKey] = savedState.s; this.Document._viewTransition = savedState.pt; } + doc.hidden && Doc.UnHighlightDoc(doc); }, newPanX !== savedState.px || newPanY !== savedState.py ? 500 : 0); return false; }; this.props.focus(this.props.Document, undefined, undefined, newAfterFocus, undefined, newDidFocus); - Doc.linkFollowHighlight(doc); + !doc.hidden && Doc.linkFollowHighlight(doc); } } diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx index d20d1abfc..9ef37ecc2 100644 --- a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx +++ b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx @@ -89,7 +89,7 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque const [x, y] = this.Transform.transformPoint(this._downX, this._downY); if (e.key === "?") { cm.setDefaultItem("?", (str: string) => this.props.addDocTab( - Docs.Create.WebDocument(`https://bing.com/search?q=${str}`, { _fitWidth: true, _width: 400, x, y, _height: 512, _nativeWidth: 850, isAnnotating: false, title: "bing", useCors: true }), "add:right")); + Docs.Create.WebDocument(`https://bing.com/search?q=${str}`, { _width: 400, x, y, _height: 512, _nativeWidth: 850, isAnnotating: false, title: "bing", useCors: true }), "add:right")); cm.displayMenu(this._downX, this._downY); e.stopPropagation(); @@ -217,6 +217,7 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque // bcz: do we need this? it kills the context menu on the main collection if !altKey // e.stopPropagation(); } + else PreviewCursor.Visible = false; } } diff --git a/src/client/views/linking/LinkMenuItem.tsx b/src/client/views/linking/LinkMenuItem.tsx index d6cefa5b0..19ef03a31 100644 --- a/src/client/views/linking/LinkMenuItem.tsx +++ b/src/client/views/linking/LinkMenuItem.tsx @@ -127,9 +127,10 @@ export class LinkMenuItem extends React.Component<LinkMenuItemProps> { @undoBatch @action - deleteLink = (): void => { + deleteLink = (e: React.PointerEvent): void => { this.props.linkDoc.linksToAnnotation && Hypothesis.deleteLink(this.props.linkDoc, this.props.sourceDoc, this.props.destinationDoc); LinkManager.Instance.deleteLink(this.props.linkDoc); + e.stopPropagation(); runInAction(() => { LinkDocPreview.LinkInfo = undefined; diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx index 6371ae5fb..6217f473f 100644 --- a/src/client/views/nodes/DocumentView.tsx +++ b/src/client/views/nodes/DocumentView.tsx @@ -386,9 +386,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps }, console.log); undoBatch(func)(); } else if (!Doc.IsSystem(this.props.Document)) { - if (this.props.Document.type === DocumentType.INK) { - InkStrokeProperties.Instance && (InkStrokeProperties.Instance._controlBtn = true); - } else if (this.props.Document.type !== DocumentType.LABEL) { + if (this.props.Document.type !== DocumentType.LABEL) { UndoManager.RunInBatch(() => { const fullScreenDoc = Cast(this.props.Document._fullScreenView, Doc, null) || this.props.Document; this.props.addDocTab(fullScreenDoc, "add"); @@ -550,10 +548,13 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps "linking to document tabs not yet supported. Drop link on document content."); return; } + if (de.complete.annoDragData) de.complete.annoDragData.annotationDocument = de.complete.annoDragData.annotationDocCreator(); const linkSource = de.complete.annoDragData ? de.complete.annoDragData.annotationDocument : de.complete.linkDragData ? de.complete.linkDragData.linkSourceDocument : undefined; if (linkSource && linkSource !== this.props.Document) { e.stopPropagation(); - de.complete.linkDocument = DocUtils.MakeLink({ doc: linkSource }, { doc: this._componentView?.getAnchor() || this.rootDoc }, "link", undefined, undefined, undefined, [de.x, de.y]); + const dropDoc = this._componentView?.getAnchor() || this.rootDoc; + if (de.complete.annoDragData) de.complete.annoDragData.dropDocument = dropDoc; + de.complete.linkDocument = DocUtils.MakeLink({ doc: linkSource }, { doc: dropDoc }, "link", undefined, undefined, undefined, [de.x, de.y]); } } @@ -873,10 +874,11 @@ export class DocumentView extends React.Component<DocumentViewProps> { @computed get panelWidth() { return this.nativeWidth ? this.nativeWidth * this.nativeScaling : this.props.PanelWidth(); } @computed get panelHeight() { if (this.nativeHeight) { - if (this.props.Document._fitWidth) { - return Math.min(this.props.PanelHeight(), NumCast(this.props.Document.scrollHeight, this.props.PanelHeight())); - } - return Math.min(this.props.PanelHeight(), this.nativeHeight * this.nativeScaling); + return Math.min(this.props.PanelHeight(), + this.props.Document._fitWidth ? + Math.max(NumCast(this.props.Document._height), NumCast(((this.props.Document.scrollHeight || 0) as number) * this.props.PanelWidth() / this.nativeWidth, this.props.PanelHeight())) : + this.nativeHeight * this.nativeScaling + ); } return this.props.PanelHeight(); } diff --git a/src/client/views/nodes/ImageBox.scss b/src/client/views/nodes/ImageBox.scss index c1b95b308..41055e2db 100644 --- a/src/client/views/nodes/ImageBox.scss +++ b/src/client/views/nodes/ImageBox.scss @@ -5,6 +5,16 @@ position: relative; transform-origin: top left; + + .imageBox-annotationLayer { + position: absolute; + transform-origin: left top; + top: 0; + width: 100%; + pointer-events: none; + mix-blend-mode: multiply; // bcz: makes text fuzzy! + } + .imageBox-fader { pointer-events: inherit; } diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx index b0e7f4ce5..649fe8f40 100644 --- a/src/client/views/nodes/ImageBox.tsx +++ b/src/client/views/nodes/ImageBox.tsx @@ -27,6 +27,10 @@ import { FieldView, FieldViewProps } from './FieldView'; import "./ImageBox.scss"; import React = require("react"); import { StyleProp } from '../StyleProvider'; +import { AnchorMenu } from '../pdf/AnchorMenu'; +import { Dictionary } from 'typescript-collections'; +import { MarqueeAnnotator } from '../MarqueeAnnotator'; +import { Annotation } from '../pdf/Annotation'; const path = require('path'); const { Howl } = require('howler'); @@ -63,7 +67,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps, ImageD public static LayoutString(fieldKey: string) { return FieldView.LayoutString(ImageBox, fieldKey); } private _imgRef: React.RefObject<HTMLImageElement> = React.createRef(); private _dropDisposer?: DragManager.DragDropDisposer; - private _pathDisposer?: IReactionDisposer; + private _disposers: { [name: string]: IReactionDisposer } = {}; @observable private _audioState = 0; @observable static _showControls: boolean; @observable uploadIcon = uploadIcons.idle; @@ -74,7 +78,15 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps, ImageD } componentDidMount() { - this._pathDisposer = reaction(() => ({ nativeSize: this.nativeSize, width: this.layoutDoc[WidthSym]() }), + this._disposers.selection = reaction(() => this.props.isSelected(), + selected => { + if (!selected) { + this._savedAnnotations.values().forEach(v => v.forEach(a => a.remove())); + this._savedAnnotations.clear(); + } + }, + { fireImmediately: true }); + this._disposers.path = reaction(() => ({ nativeSize: this.nativeSize, width: this.layoutDoc[WidthSym]() }), action(({ nativeSize, width }) => { if (!this.layoutDoc._height) { this.layoutDoc._height = width * nativeSize.nativeHeight / nativeSize.nativeWidth; @@ -84,7 +96,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps, ImageD } componentWillUnmount() { - this._pathDisposer?.(); + Object.values(this._disposers).forEach(disposer => disposer?.()); } @undoBatch @@ -356,7 +368,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps, ImageD transform = `translate(-100%, 0%) rotate(${rotation}deg) scale(${aspect})`; } - return <div className="imageBox-cont" key={this.layoutDoc[Id]} ref={this.createDropTarget}> + return <div className="imageBox-cont" key={this.layoutDoc[Id]} ref={this.createDropTarget} onPointerDown={this.marqueeDown}> <div className="imageBox-fader" > <img key={this._smallRetryCount + (this._mediumRetryCount << 4) + (this._largeRetryCount << 8)} // force cache to update on retrys src={srcpath} @@ -402,11 +414,28 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps, ImageD screenToLocalTransform = () => this.props.ScreenToLocalTransform().translate(0, -this.ycenter); contentFunc = () => [this.content]; + private _mainCont: React.RefObject<HTMLDivElement> = React.createRef(); + private _annotationLayer: React.RefObject<HTMLDivElement> = React.createRef(); + @observable _marqueeing: number[] | undefined; + @observable _savedAnnotations: Dictionary<number, HTMLDivElement[]> = new Dictionary<number, HTMLDivElement[]>(); + @computed get annotationLayer() { + return <div className="imageBox-annotationLayer" style={{ height: Doc.NativeHeight(this.Document) || undefined }} ref={this._annotationLayer} />; + } + @action + marqueeDown = (e: React.PointerEvent) => { + if (!e.altKey && e.button === 0 && this.active(true)) this._marqueeing = [e.clientX, e.clientY]; + } + @action + finishMarquee = () => { + this._marqueeing = undefined; + this.props.select(true); + } + render() { TraceMobx(); const borderRad = this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.BorderRounding); const borderRadius = borderRad?.includes("px") ? `${Number(borderRad.split("px")[0]) / (this.props.scaling?.() || 1)}px` : borderRad; - return (<div className={`imageBox`} onContextMenu={this.specificContextMenu} + return (<div className={`imageBox`} onContextMenu={this.specificContextMenu} ref={this._mainCont} style={{ width: this.props.PanelWidth() ? undefined : `100%`, height: this.props.PanelWidth() ? undefined : `100%`, @@ -437,6 +466,9 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps, ImageD whenActiveChanged={this.whenActiveChanged}> {this.contentFunc} </CollectionFreeFormView> + {this.annotationLayer} + {!this._marqueeing || !this._mainCont.current || !this._annotationLayer.current ? (null) : + <MarqueeAnnotator rootDoc={this.rootDoc} down={this._marqueeing} scaling={this.props.scaling} addDocument={this.addDocument} finishMarquee={this.finishMarquee} savedAnnotations={this._savedAnnotations} annotationLayer={this._annotationLayer.current} mainCont={this._mainCont.current} />} </div >); } } diff --git a/src/client/views/nodes/LinkDocPreview.tsx b/src/client/views/nodes/LinkDocPreview.tsx index 07b2b6338..7934dba8e 100644 --- a/src/client/views/nodes/LinkDocPreview.tsx +++ b/src/client/views/nodes/LinkDocPreview.tsx @@ -76,7 +76,7 @@ export class LinkDocPreview extends React.Component<Props> { if (this.props.linkDoc && this.props.linkSrc) { LinkManager.FollowLink(this.props.linkDoc, this.props.linkSrc, this.props.docprops, false); } else if (this.props.href) { - this.props.docprops?.addDocTab(Docs.Create.WebDocument(this.props.href, { _fitWidth: true, title: this.props.href, _width: 200, _height: 400, useCors: true }), "add:right"); + this.props.docprops?.addDocTab(Docs.Create.WebDocument(this.props.href, { title: this.props.href, _width: 200, _height: 400, useCors: true }), "add:right"); } } width = () => Math.min(225, NumCast(this._targetDoc?.[WidthSym](), 225)); diff --git a/src/client/views/nodes/VideoBox.tsx b/src/client/views/nodes/VideoBox.tsx index 67e8d74b3..2ac105545 100644 --- a/src/client/views/nodes/VideoBox.tsx +++ b/src/client/views/nodes/VideoBox.tsx @@ -225,6 +225,14 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps, VideoD } componentDidMount() { + this._disposers.selection = reaction(() => this.props.isSelected(), + selected => { + if (!selected) { + this._savedAnnotations.values().forEach(v => v.forEach(a => a.remove())); + this._savedAnnotations.clear(); + } + }, + { fireImmediately: true }); this._disposers.videoStart = reaction( () => this.Document._videoStart, (videoStart) => { @@ -743,17 +751,29 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps, VideoD screenToLocalTransform = () => this.props.ScreenToLocalTransform(); contentFunc = () => [this.youtubeVideoId ? this.youtubeContent : this.content]; + + @computed get annotationLayer() { + return <div className="imageBox-annotationLayer" style={{ height: Doc.NativeHeight(this.Document) || undefined }} ref={this._annotationLayer} />; + } + + marqueeDown = action((e: React.PointerEvent) => { + if (!e.altKey && e.button === 0 && this.active(true)) this._marqueeing = [e.clientX, e.clientY]; + }) + + finishMarquee = action(() => { + this._marqueeing = undefined; + this.props.select(true); + }) + render() { const borderRad = this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.BorderRounding); const borderRadius = borderRad?.includes("px") ? `${Number(borderRad.split("px")[0]) / (this.props.scaling?.() || 1)}px` : borderRad; - return (<div className="videoBox" onContextMenu={this.specificContextMenu} + return (<div className="videoBox" onContextMenu={this.specificContextMenu} ref={this._mainCont} style={{ - width: "100%", - height: "100%", pointerEvents: this.props.layerProvider?.(this.layoutDoc) === false ? "none" : undefined, borderRadius }} > - <div className="videoBox-viewer" > + <div className="videoBox-viewer" onPointerDown={this.marqueeDown}> <CollectionFreeFormView {...OmitKeys(this.props, ["NativeWidth", "NativeHeight", "setContentView"]).omit} forceScaling={true} fieldKey={this.annotationKey} @@ -771,6 +791,13 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps, VideoD {this.contentFunc} </CollectionFreeFormView> </div> +<<<<<<< HEAD +======= + {this.uIButtons} + {this.annotationLayer} + {!this._marqueeing || !this._mainCont.current || !this._annotationLayer.current ? (null) : + <MarqueeAnnotator rootDoc={this.rootDoc} down={this._marqueeing} scaling={this.props.scaling} addDocument={this.addDocumentWithTimestamp} finishMarquee={this.finishMarquee} savedAnnotations={this._savedAnnotations} annotationLayer={this._annotationLayer.current} mainCont={this._mainCont.current} />} +>>>>>>> 10b759d2bd09af3a8e8a4effbc8fd2312dd873d2 </div >); } } diff --git a/src/client/views/nodes/WebBox.tsx b/src/client/views/nodes/WebBox.tsx index 7fb14b0cc..69f797880 100644 --- a/src/client/views/nodes/WebBox.tsx +++ b/src/client/views/nodes/WebBox.tsx @@ -1,9 +1,10 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { Tooltip } from '@material-ui/core'; import { action, computed, IReactionDisposer, observable, reaction, runInAction } from "mobx"; import { observer } from "mobx-react"; import { Dictionary } from "typescript-collections"; import * as WebRequest from 'web-request'; -import { Doc, DocListCast, Opt, AclAddonly, AclEdit, AclAdmin, DataSym, HeightSym, WidthSym } from "../../../fields/Doc"; +import { Doc, DocListCast, HeightSym, Opt, WidthSym } from "../../../fields/Doc"; import { documentSchema } from "../../../fields/documentSchemas"; import { Id } from "../../../fields/FieldSymbols"; import { HtmlField } from "../../../fields/HtmlField"; @@ -12,9 +13,9 @@ import { List } from "../../../fields/List"; import { listSpec, makeInterface } from "../../../fields/Schema"; import { Cast, NumCast, StrCast } from "../../../fields/Types"; import { WebField } from "../../../fields/URLField"; -import { TraceMobx, GetEffectiveAcl } from "../../../fields/util"; -import { addStyleSheet, clearStyleSheetRules, emptyFunction, returnOne, returnZero, Utils, returnTrue, OmitKeys, smoothScroll } from "../../../Utils"; -import { Docs, DocUtils } from "../../documents/Documents"; +import { TraceMobx } from "../../../fields/util"; +import { emptyFunction, OmitKeys, returnOne, smoothScroll, Utils } from "../../../Utils"; +import { Docs } from "../../documents/Documents"; import { DragManager } from "../../util/DragManager"; import { ImageUtils } from "../../util/Import & Export/ImageUtils"; import { undoBatch } from "../../util/UndoManager"; @@ -24,15 +25,11 @@ import { ContextMenuProps } from "../ContextMenuItem"; import { ViewBoxAnnotatableComponent } from "../DocComponent"; import { DocumentDecorations } from "../DocumentDecorations"; import { Annotation } from "../pdf/Annotation"; -import { PDFMenu } from "../pdf/PDFMenu"; -import { PdfViewerMarquee } from "../pdf/PDFViewer"; +import { AnchorMenu } from "../pdf/AnchorMenu"; import { FieldView, FieldViewProps } from './FieldView'; import "./WebBox.scss"; -import "../pdf/PDFViewer.scss"; import React = require("react"); -import { Tooltip } from '@material-ui/core'; -import { CurrentUserUtils } from '../../util/CurrentUserUtils'; -import { FormattedTextBox } from './formattedText/FormattedTextBox'; +import { MarqueeAnnotator } from "../MarqueeAnnotator"; const htmlToText = require("html-to-text"); type WebDocument = makeInterface<[typeof documentSchema]>; @@ -40,32 +37,22 @@ const WebDocument = makeInterface(documentSchema); @observer export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps, WebDocument>(WebDocument) { - private _annotationLayer: React.RefObject<HTMLDivElement> = React.createRef(); - static _annotationStyle: any = addStyleSheet(); public static LayoutString(fieldKey: string) { return FieldView.LayoutString(WebBox, fieldKey); } private _mainCont: React.RefObject<HTMLDivElement> = React.createRef(); - private _startX: number = 0; - private _startY: number = 0; - @observable private _marqueeX: number = 0; - @observable private _marqueeY: number = 0; - @observable private _marqueeWidth: number = 0; - @observable private _marqueeHeight: number = 0; - @observable private _marqueeing: boolean = false; + private _setPreviewCursor: undefined | ((x: number, y: number, drag: boolean) => void); + private _disposers: { [name: string]: IReactionDisposer } = {}; + private _longPressSecondsHack?: NodeJS.Timeout; + private _outerRef = React.createRef<HTMLDivElement>(); + private _iframeIndicatorRef = React.createRef<HTMLDivElement>(); + private _iframeDragRef = React.createRef<HTMLDivElement>(); + @observable private _marqueeing: number[] | undefined; @observable private _url: string = "hello"; @observable private _pressX: number = 0; @observable private _pressY: number = 0; @observable private _iframe: HTMLIFrameElement | null = null; @observable private _savedAnnotations: Dictionary<number, HTMLDivElement[]> = new Dictionary<number, HTMLDivElement[]>(); - private _selectionReactionDisposer?: IReactionDisposer; - private _scrollReactionDisposer?: IReactionDisposer; - private _scrollTopReactionDisposer?: IReactionDisposer; - private _moveReactionDisposer?: IReactionDisposer; - private _longPressSecondsHack?: NodeJS.Timeout; - private _outerRef = React.createRef<HTMLDivElement>(); - private _iframeIndicatorRef = React.createRef<HTMLDivElement>(); - private _iframeDragRef = React.createRef<HTMLDivElement>(); - private _setPreviewCursor: undefined | ((x: number, y: number, drag: boolean) => void); + get scrollHeight() { return this.webpage?.scrollHeight || 1000; } get _collapsed() { return StrCast(this.layoutDoc._chromeStatus) !== "enabled"; } set _collapsed(value) { this.layoutDoc._chromeStatus = !value ? "enabled" : "disabled"; } @@ -99,8 +86,8 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps, WebDocum this.webpage.scrollLeft = NumCast(this.layoutDoc._scrollLeft); } } - this._scrollReactionDisposer?.(); - this._scrollReactionDisposer = reaction(() => ({ scrollY: this.layoutDoc._scrollY, scrollX: this.layoutDoc._scrollX }), + this._disposers.scrollReaction?.(); + this._disposers.scrollReaction = reaction(() => ({ scrollY: this.layoutDoc._scrollY, scrollX: this.layoutDoc._scrollX }), ({ scrollY, scrollX }) => { const delay = this._outerRef.current ? 0 : 250; // wait for mainCont and try again to scroll const durationStr = StrCast(this.Document._viewTransition).match(/([0-9]*)ms/); @@ -118,7 +105,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps, WebDocum }, { fireImmediately: true } ); - this._scrollTopReactionDisposer = reaction(() => this.layoutDoc._scrollTop, + this._disposers.scrollTop = reaction(() => this.layoutDoc._scrollTop, scrollTop => { const durationStr = StrCast(this.Document._viewTransition).match(/([0-9]*)ms/); const duration = durationStr ? Number(durationStr[1]) : 1000; @@ -166,15 +153,14 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps, WebDocum const urlField = Cast(this.dataDoc[this.props.fieldKey], WebField); runInAction(() => this._url = urlField?.url.toString() || ""); - this._moveReactionDisposer = reaction(() => this.layoutDoc.x || this.layoutDoc.y, + this._disposers.scrollMove = reaction(() => this.layoutDoc.x || this.layoutDoc.y, () => this.updateScroll(this.layoutDoc._scrollLeft, this.layoutDoc._scrollTop)); - this._selectionReactionDisposer = reaction(() => this.props.isSelected(), + this._disposers.selection = reaction(() => this.props.isSelected(), selected => { if (!selected) { this._savedAnnotations.values().forEach(v => v.forEach(a => a.remove())); - this._savedAnnotations.keys().forEach(k => this._savedAnnotations.setValue(k, [])); - PDFMenu.Instance.fadeOut(true); + this._savedAnnotations.clear(); } }, { fireImmediately: true }); @@ -202,18 +188,13 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps, WebDocum } componentWillUnmount() { - this._moveReactionDisposer?.(); - this._selectionReactionDisposer?.(); - this._scrollTopReactionDisposer?.(); - this._scrollReactionDisposer?.(); + Object.values(this._disposers).forEach(disposer => disposer?.()); document.removeEventListener("pointerup", this.onLongPressUp); document.removeEventListener("pointermove", this.onLongPressMove); this._iframe?.removeEventListener('wheel', this.iframeWheel); } - onUrlDragover = (e: React.DragEvent) => { - e.preventDefault(); - } + onUrlDragover = (e: React.DragEvent) => { e.preventDefault(); } @undoBatch @action @@ -256,6 +237,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps, WebDocum urlHash(s: string) { return s.split('').reduce((a: any, b: any) => { a = ((a << 5) - a) + b.charCodeAt(0); return a & a; }, 0); } + @action submitURL = () => { if (!this._url.startsWith("http")) this._url = "http://" + this._url; @@ -283,9 +265,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps, WebDocum } onValueKeyDown = async (e: React.KeyboardEvent) => { - if (e.key === "Enter") { - this.submitURL(); - } + if (e.key === "Enter") this.submitURL(); e.stopPropagation(); } @@ -300,22 +280,14 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps, WebDocum } _ignore = 0; - onPreWheel = (e: React.WheelEvent) => { - this._ignore = e.timeStamp; - } - onPrePointer = (e: React.PointerEvent) => { - this._ignore = e.timeStamp; - } + onPreWheel = (e: React.WheelEvent) => { this._ignore = e.timeStamp; } + onPrePointer = (e: React.PointerEvent) => { this._ignore = e.timeStamp; } onPostPointer = (e: React.PointerEvent) => { - if (this._ignore !== e.timeStamp) { - e.stopPropagation(); - } + if (this._ignore !== e.timeStamp) e.stopPropagation(); } onPostWheel = (e: React.WheelEvent) => { - if (this._ignore !== e.timeStamp) { - e.stopPropagation(); - } + if (this._ignore !== e.timeStamp) e.stopPropagation(); } onLongPressDown = (e: React.PointerEvent) => { @@ -431,7 +403,6 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps, WebDocum @computed get urlContent() { - const field = this.dataDoc[this.props.fieldKey]; let view; if (field instanceof HtmlField) { @@ -449,6 +420,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps, WebDocum } return view; } + @computed get content() { const view = this.urlContent; @@ -458,7 +430,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps, WebDocum return (<> <div className={"webBox-cont" + (this.props.isSelected() && Doc.GetSelectedTool() === InkTool.None && !DocumentDecorations.Instance?.Interacting ? "-interactive" : "")} style={{ - width: `${100 / scale}%`, + width: NumCast(this.layoutDoc[this.fieldKey + "-contentWidth"]) || `${100 / scale}%`, height: `${100 / scale}%`, transform: `scale(${scale})` }} @@ -476,25 +448,9 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps, WebDocum </>); } - - @computed get allAnnotations() { return DocListCast(this.dataDoc[this.props.fieldKey + "-annotations"]); } @computed get nonDocAnnotations() { return this.allAnnotations.filter(a => a.annotations); } - @undoBatch - @action - makeAnnotationDocument = (color: string): Opt<Doc> => { - if (this._savedAnnotations.size() === 0) return undefined; - const anno = this._savedAnnotations.values()[0][0]; - const annoDoc = Docs.Create.FreeformDocument([], { backgroundColor: color, annotationOn: this.props.Document, title: "Annotation on " + this.Document.title }); - if (anno.style.left) annoDoc.x = parseInt(anno.style.left); - if (anno.style.top) annoDoc.y = NumCast(this.layoutDoc._scrollTop) + parseInt(anno.style.top); - if (anno.style.height) annoDoc._height = parseInt(anno.style.height); - if (anno.style.width) annoDoc._width = parseInt(anno.style.width); - anno.remove(); - this._savedAnnotations.clear(); - return annoDoc; - } @computed get annotationLayer() { TraceMobx(); return <div className="webBox-annotationLayer" style={{ height: Doc.NativeHeight(this.Document) || undefined }} ref={this._annotationLayer}> @@ -503,153 +459,20 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps, WebDocum } </div>; } - @action - createAnnotation = (div: HTMLDivElement, page: number) => { - if (this._annotationLayer.current) { - if (div.style.top) { - div.style.top = (parseInt(div.style.top)).toString(); - } - this._annotationLayer.current.append(div); - div.style.backgroundColor = "#ACCEF7"; - div.style.opacity = "0.5"; - const savedPage = this._savedAnnotations.getValue(page); - if (savedPage) { - savedPage.push(div); - this._savedAnnotations.setValue(page, savedPage); - } - else { - this._savedAnnotations.setValue(page, [div]); - } - } - } @action - highlight = (color: string) => { - // creates annotation documents for current highlights - const effectiveAcl = GetEffectiveAcl(this.props.Document[DataSym]); - const annotationDoc = [AclAddonly, AclEdit, AclAdmin].includes(effectiveAcl) ? this.makeAnnotationDocument(color.replace(/[0-9.]*\)/, "0.3)")) : undefined; - annotationDoc && this.addDocument?.(annotationDoc); - return annotationDoc ?? undefined; - } - /** - * This is temporary for creating annotations from highlights. It will - * start a drag event and create or put the necessary info into the drag event. - */ - @action - startDrag = async (e: PointerEvent, ele: HTMLElement) => { - e.preventDefault(); - e.stopPropagation(); - - const targetDoc = CurrentUserUtils.GetNewTextDoc("Note linked to " + this.props.Document.title, 0, 0, 125, 125); - FormattedTextBox.SelectOnLoad = targetDoc[Id]; - const annotationDoc = this.highlight("rgba(173, 216, 230, 0.35)"); // hyperlink color - if (annotationDoc) { - DragManager.StartPdfAnnoDrag([ele], new DragManager.PdfAnnoDragData(this.props.Document, annotationDoc, targetDoc), e.pageX, e.pageY, { - dragComplete: e => { - if (!e.aborted && e.annoDragData && !e.linkDocument) { - e.linkDocument = DocUtils.MakeLink({ doc: annotationDoc }, { doc: e.annoDragData.dropDocument }, "Annotation"); - annotationDoc.isLinkButton = true; - annotationDoc.isPushpin = e.annoDragData?.dropDocument.annotationOn === this.props.Document; - } - } - }); - } - } - @action onMarqueeDown = (e: React.PointerEvent) => { - this._marqueeing = false; if (!e.altKey && e.button === 0 && this.active(true)) { - // clear out old marquees and initialize menu for new selection - PDFMenu.Instance.StartDrag = this.startDrag; - PDFMenu.Instance.Highlight = this.highlight; - PDFMenu.Instance.Status = "pdf"; - PDFMenu.Instance.fadeOut(true); - this._savedAnnotations.values().forEach(v => v.forEach(a => a.remove())); - this._savedAnnotations.keys().forEach(k => this._savedAnnotations.setValue(k, [])); - if ((e.target as any)?.parentElement.className === "textLayer") { - // start selecting text if mouse down on textLayer spans - } - else if (this._mainCont.current) { - // set marquee x and y positions to the spatially transformed position - const nheight = Doc.NativeHeight(this.Document) || 1; - const nwidth = Doc.NativeWidth(this.Document) || 1; - const boundingRect = this._mainCont.current.getBoundingClientRect(); - const boundingHeight = nheight / nwidth * boundingRect.width; - this._startX = (e.clientX - boundingRect.left) / boundingRect.width * nwidth; - this._startY = (e.clientY - boundingRect.top) / boundingHeight * nheight; - this._marqueeHeight = this._marqueeWidth = 0; - this._marqueeing = true; - } - document.removeEventListener("pointermove", this.onSelectMove); - document.addEventListener("pointermove", this.onSelectMove); - document.removeEventListener("pointerup", this.onSelectEnd); - document.addEventListener("pointerup", this.onSelectEnd); - } - } - @action - onSelectMove = (e: PointerEvent): void => { - if (this._marqueeing && this._mainCont.current) { - // transform positions and find the width and height to set the marquee to - const boundingRect = this._mainCont.current.getBoundingClientRect(); - const boundingHeight = (Doc.NativeHeight(this.Document) || 1) / (Doc.NativeWidth(this.Document) || 1) * boundingRect.width; - const curX = (e.clientX - boundingRect.left) / boundingRect.width * (Doc.NativeWidth(this.Document) || 1); - const curY = (e.clientY - boundingRect.top) / boundingHeight * (Doc.NativeHeight(this.Document) || 1); - this._marqueeWidth = curX - this._startX; - this._marqueeHeight = curY - this._startY; - this._marqueeX = Math.min(this._startX, this._startX + this._marqueeWidth); - this._marqueeY = Math.min(this._startY, this._startY + this._marqueeHeight); - this._marqueeWidth = Math.abs(this._marqueeWidth); - this._marqueeHeight = Math.abs(this._marqueeHeight); - e.stopPropagation(); - e.preventDefault(); - } - else if (e.target && (e.target as any).parentElement === this._mainCont.current) { - e.stopPropagation(); + this._marqueeing = [e.clientX, e.clientY]; } } @action - onSelectEnd = (e: PointerEvent): void => { - clearStyleSheetRules(WebBox._annotationStyle); - this._savedAnnotations.clear(); - if (this._marqueeWidth > 10 || this._marqueeHeight > 10) { - const marquees = this._mainCont.current!.getElementsByClassName("pdfViewerDash-dragAnnotationBox"); - if (marquees?.length) { // copy the marquee and convert it to a permanent annotation. - const style = (marquees[0] as HTMLDivElement).style; - const copy = document.createElement("div"); - copy.style.left = style.left; - copy.style.top = style.top; - copy.style.width = style.width; - copy.style.height = style.height; - copy.style.border = style.border; - copy.style.opacity = style.opacity; - (copy as any).marqueeing = true; - copy.className = "webBox-annotationBox"; - this.createAnnotation(copy, 0); - } - - if (!e.ctrlKey) { - PDFMenu.Instance.Marquee = { left: this._marqueeX, top: this._marqueeY, width: this._marqueeWidth, height: this._marqueeHeight }; - } - PDFMenu.Instance.jumpTo(e.clientX, e.clientY); - } - //this._marqueeing = false; - - if (PDFMenu.Instance.Highlighting) {// when highlighter has been toggled when menu is pinned, we auto-highlight immediately on mouse up - this.highlight("rgba(245, 230, 95, 0.616)"); // yellowish highlight color for highlighted text (should match PDFMenu's highlight color) - } - else { - PDFMenu.Instance.StartDrag = this.startDrag; - PDFMenu.Instance.Highlight = this.highlight; - } - document.removeEventListener("pointermove", this.onSelectMove); - document.removeEventListener("pointerup", this.onSelectEnd); + finishMarquee = () => { + this._marqueeing = undefined; + this.props.select(true); } - marqueeWidth = () => this._marqueeWidth; - marqueeHeight = () => this._marqueeHeight; - marqueeX = () => this._marqueeX; - marqueeY = () => this._marqueeY; - marqueeing = () => this._marqueeing; + scrollXf = () => this.props.ScreenToLocalTransform().translate(NumCast(this.layoutDoc._scrollLeft), NumCast(this.layoutDoc._scrollTop)); render() { const inactiveLayer = this.props.layerProvider?.(this.layoutDoc) === false; @@ -662,9 +485,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps, WebDocum {this.content} <div className={"webBox-outerContent"} ref={this._outerRef} style={{ - width: `${100 / scale}%`, - height: `${100 / scale}%`, - transform: `scale(${scale})`, + width: `${100 / scale}%`, height: `${100 / scale}%`, transform: `scale(${scale})`, pointerEvents: this.layoutDoc.isAnnotating && !inactiveLayer ? "all" : "none" }} onWheel={e => { @@ -707,9 +528,9 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps, WebDocum </div> </div> {this.annotationLayer} - <PdfViewerMarquee isMarqueeing={this.marqueeing} width={this.marqueeWidth} height={this.marqueeHeight} x={this.marqueeX} y={this.marqueeY} /> + {!this._marqueeing || !this._mainCont.current || !this._annotationLayer.current ? (null) : + <MarqueeAnnotator rootDoc={this.rootDoc} down={this._marqueeing} scaling={this.props.scaling} addDocument={this.addDocument} finishMarquee={this.finishMarquee} savedAnnotations={this._savedAnnotations} annotationLayer={this._annotationLayer.current} mainCont={this._mainCont.current} />} </div > - {this.props.isSelected() ? this.editToggleBtn() : null} </div>); } diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx index c129d0204..ac5ea66ff 100644 --- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx +++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx @@ -59,11 +59,13 @@ import { FormattedTextBoxComment, formattedTextBoxCommentPlugin, findLinkMark } import React = require("react"); import { LinkManager } from '../../../util/LinkManager'; import { CollectionStackingView } from '../../collections/CollectionStackingView'; -import { CollectionViewType, CollectionViewProps } from '../../collections/CollectionView'; +import { CollectionViewType } from '../../collections/CollectionView'; import { SnappingManager } from '../../../util/SnappingManager'; import { LinkDocPreview } from '../LinkDocPreview'; import { SubCollectionViewProps } from '../../collections/CollectionSubView'; import { StyleProp } from '../../StyleProvider'; +import { AnchorMenu } from '../../pdf/AnchorMenu'; +import { CurrentUserUtils } from '../../../util/CurrentUserUtils'; export interface FormattedTextBoxProps { makeLink?: () => Opt<Doc>; // bcz: hack: notifies the text document when the container has made a link. allows the text doc to react and setup a hyeprlink for any selected text @@ -83,6 +85,7 @@ type PullHandler = (exportState: Opt<GoogleApiClientUtils.Docs.ImportResult>, da export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProps & FormattedTextBoxProps), RichTextDocument>(RichTextDocument) { public static LayoutString(fieldStr: string) { return FieldView.LayoutString(FormattedTextBox, fieldStr); } public static blankState = () => EditorState.create(FormattedTextBox.Instance.config); + public static CanAnnotate = true; public static Instance: FormattedTextBox; public ProseRef?: HTMLDivElement; public get EditorView() { return this._editorView; } @@ -209,6 +212,42 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp this.linkOnDeselect.clear(); } + @action + setupAnchorMenu = () => { + AnchorMenu.Instance.Status = "marquee"; + AnchorMenu.Instance.Highlight = action((color: string) => { + this._editorView?.state && RichTextMenu.Instance.insertHighlight(color, this._editorView.state, this._editorView?.dispatch); + return undefined; + }); + /** + * This function is used by the PDFmenu to create an anchor highlight and a new linked text annotation. + * It also initiates a Drag/Drop interaction to place the text annotation. + */ + AnchorMenu.Instance.StartDrag = action(async (e: PointerEvent, ele: HTMLElement) => { + e.preventDefault(); + e.stopPropagation(); + const targetCreator = () => { + const target = CurrentUserUtils.GetNewTextDoc("Note linked to " + this.rootDoc.title, 0, 0, 100, 100); + FormattedTextBox.SelectOnLoad = target[Id]; + return target; + } + + DragManager.StartAnchorAnnoDrag([ele], new DragManager.AnchorAnnoDragData(this.rootDoc, () => this.rootDoc, targetCreator), e.pageX, e.pageY, { + dragComplete: e => { + if (!e.aborted && e.annoDragData && e.annoDragData.annotationDocument && e.annoDragData.dropDocument && !e.linkDocument) { + e.linkDocument = DocUtils.MakeLink({ doc: e.annoDragData.annotationDocument }, { doc: e.annoDragData.dropDocument }, "hyperlink", "link to note"); + e.annoDragData.annotationDocument.isPushpin = e.annoDragData?.dropDocument.annotationOn === this.rootDoc; + } + e.linkDocument && e.annoDragData?.dropDocument && this.makeLinkToSelection(e.linkDocument[Id], "a link", "add:right", e.annoDragData.dropDocument[Id]); + e.linkDocument && e.annoDragData?.linkDropCallback?.(e as { linkDocument: Doc });// bcz: typescript can't figure out that this is valid even though we tested e.linkDocument + } + }); + }); + const coordsT = this._editorView!.coordsAtPos(this._editorView!.state.selection.to); + const coordsB = this._editorView!.coordsAtPos(this._editorView!.state.selection.to); + this.props.isSelected(true) && AnchorMenu.Instance.jumpTo(Math.min(coordsT.left, coordsB.left), Math.max(coordsT.bottom, coordsB.bottom)); + } + dispatchTransaction = (tx: Transaction) => { let timeStamp; clearTimeout(timeStamp); @@ -252,6 +291,8 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp let unchanged = true; const effectiveAcl = GetEffectiveAcl(this.dataDoc); + if (!this._editorView.state.selection.empty && FormattedTextBox.CanAnnotate) this.setupAnchorMenu(); + const removeSelection = (json: string | undefined) => { return json?.indexOf("\"storedMarks\"") === -1 ? json?.replace(/"selection":.*/, "") : json?.replace(/"selection":"\"storedMarks\""/, "\"storedMarks\""); }; @@ -619,6 +660,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp const uicontrols: ContextMenuProps[] = []; + uicontrols.push({ description: `${FormattedTextBox.CanAnnotate ? "Hide" : "Show"} Annotation Bar`, event: () => FormattedTextBox.CanAnnotate = !FormattedTextBox.CanAnnotate, icon: "expand-arrows-alt" }); uicontrols.push({ description: `${this.layoutDoc._showAudio ? "Hide" : "Show"} Dictation Icon`, event: () => this.layoutDoc._showAudio = !this.layoutDoc._showAudio, icon: "expand-arrows-alt" }); uicontrols.push({ description: "Show Highlights...", noexpand: true, subitems: highlighting, icon: "hand-point-right" }); !Doc.UserDoc().noviceMode && uicontrols.push({ description: `Create TimeStamp When ${this.layoutDoc._timeStampOnEnter ? "Pause" : "Enter"}`, event: () => this.layoutDoc._timeStampOnEnter = !this.layoutDoc._timeStampOnEnter, icon: "expand-arrows-alt" }); @@ -1256,9 +1298,9 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp } componentWillUnmount() { + Object.values(this._disposers).forEach(disposer => disposer?.()); this.endUndoTypingBatch(); this.unhighlightSearchTerms(); - Object.values(this._disposers).forEach(disposer => disposer?.()); this._editorView?.destroy(); FormattedTextBoxComment.tooltip && (FormattedTextBoxComment.tooltip.style.display = "none"); } diff --git a/src/client/views/nodes/formattedText/FormattedTextBoxComment.tsx b/src/client/views/nodes/formattedText/FormattedTextBoxComment.tsx index 038a91aa3..5371bd10a 100644 --- a/src/client/views/nodes/formattedText/FormattedTextBoxComment.tsx +++ b/src/client/views/nodes/formattedText/FormattedTextBoxComment.tsx @@ -121,7 +121,7 @@ export class FormattedTextBoxComment { } } } else if (textBox && (FormattedTextBoxComment.tooltipText as any).href) { - textBox.props.addDocTab(Docs.Create.WebDocument((FormattedTextBoxComment.tooltipText as any).href, { title: (FormattedTextBoxComment.tooltipText as any).href, _fitWidth: true, _width: 200, _height: 400, useCors: true }), "add:right"); + textBox.props.addDocTab(Docs.Create.WebDocument((FormattedTextBoxComment.tooltipText as any).href, { title: (FormattedTextBoxComment.tooltipText as any).href, _width: 200, _height: 400, useCors: true }), "add:right"); } keep && textBox && FormattedTextBoxComment.start !== undefined && textBox.adoptAnnotation( FormattedTextBoxComment.start, FormattedTextBoxComment.end, FormattedTextBoxComment.mark); @@ -255,7 +255,7 @@ export class FormattedTextBoxComment { docTarget && DocServer.GetRefField(docTarget).then(async linkDoc => { if (linkDoc instanceof Doc) { (FormattedTextBoxComment.tooltipText as any).href = href; - FormattedTextBoxComment.linkDoc = DocListCast(textBox.props.Document.links).find(link => link.anchor1 === textBox.props.Document || link.anchor2 === textBox.props.Document ? link : undefined) || linkDoc; + FormattedTextBoxComment.linkDoc = linkDoc; const anchor = FieldValue(Doc.AreProtosEqual(FieldValue(Cast(linkDoc.anchor1, Doc)), textBox.dataDoc) ? Cast(linkDoc.anchor2, Doc) : (Cast(linkDoc.anchor1, Doc)) || linkDoc); const target = anchor?.annotationOn ? await DocCastAsync(anchor.annotationOn) : anchor; if (anchor !== target && anchor && target) { diff --git a/src/client/views/nodes/formattedText/ProsemirrorExampleTransfer.ts b/src/client/views/nodes/formattedText/ProsemirrorExampleTransfer.ts index cb5823e86..8d9d36580 100644 --- a/src/client/views/nodes/formattedText/ProsemirrorExampleTransfer.ts +++ b/src/client/views/nodes/formattedText/ProsemirrorExampleTransfer.ts @@ -135,7 +135,7 @@ export function buildKeymap<S extends Schema<any>>(schema: S, props: any, mapKey //Command to create a new Tab with a PDF of all the command shortcuts bind("Mod-/", (state: EditorState<S>, dispatch: (tx: Transaction<S>) => void) => { - const newDoc = Docs.Create.PdfDocument(Utils.prepend("/assets/cheat-sheet.pdf"), { _fitWidth: true, _width: 300, _height: 300 }); + const newDoc = Docs.Create.PdfDocument(Utils.prepend("/assets/cheat-sheet.pdf"), { _width: 300, _height: 300 }); props.addDocTab(newDoc, "add:right"); }); diff --git a/src/client/views/nodes/formattedText/RichTextMenu.tsx b/src/client/views/nodes/formattedText/RichTextMenu.tsx index 07439825f..992194e2b 100644 --- a/src/client/views/nodes/formattedText/RichTextMenu.tsx +++ b/src/client/views/nodes/formattedText/RichTextMenu.tsx @@ -56,7 +56,6 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> { @observable private activeListType: string = ""; @observable private activeAlignment: string = "left"; - @observable private brushIsEmpty: boolean = true; @observable private brushMarks: Set<Mark> = new Set(); @observable private showBrushDropdown: boolean = false; @@ -600,7 +599,7 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> { createBrushButton() { const self = this; - function onBrushClick(e: React.PointerEvent) { + const onBrushClick = (e: React.MouseEvent) => { e.preventDefault(); e.stopPropagation(); self.TextView.endUndoTypingBatch(); @@ -622,8 +621,7 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> { //onPointerDown={onBrushClick} const button = <Tooltip title={<div className="dash-tooltip">style brush</div>} placement="bottom"> - - <button className="antimodeMenu-button" style={this.brushMarks?.size > 0 ? { backgroundColor: "121212" } : {}}> + <button className="antimodeMenu-button" onClick={onBrushClick} style={this.brushMarks?.size > 0 ? { backgroundColor: "121212" } : {}}> <FontAwesomeIcon icon="paint-roller" size="lg" style={{ transitionProperty: "transform", transitionDuration: "0.1s", transform: `rotate(${this.brushMarks?.size > 0 ? 45 : 0}deg)` }} /> </button> </Tooltip>; @@ -636,13 +634,12 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> { </div>; return ( - <ButtonDropdown view={this.view} key={"brush dropdown"} button={button} dropdownContent={dropdownContent} openDropdownOnButton={true} /> + <ButtonDropdown view={this.view} key={"brush dropdown"} button={button} openDropdownOnButton={false} dropdownContent={dropdownContent} /> ); } @action clearBrush() { - RichTextMenu.Instance.brushIsEmpty = true; RichTextMenu.Instance.brushMarks = new Set(); } @@ -650,26 +647,22 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> { fillBrush(state: EditorState<any>, dispatch: any) { if (!this.view) return; - if (this.brushIsEmpty) { + if (!Array.from(this.brushMarks.keys()).length) { const selected_marks = this.getMarksInSelection(this.view.state); if (selected_marks.size >= 0) { this.brushMarks = selected_marks; - this.brushIsEmpty = !this.brushIsEmpty; } } else { const { from, to, $from } = this.view.state.selection; if (!this.view.state.selection.empty && $from && $from.nodeAfter) { - if (this.brushMarks && to - from > 0) { + if (to - from > 0) { this.view.dispatch(this.view.state.tr.removeMark(from, to)); Array.from(this.brushMarks).filter(m => m.type !== schema.marks.user_mark).forEach((mark: Mark) => { this.setMark(mark, this.view!.state, this.view!.dispatch); }); } } - else { - this.brushIsEmpty = !this.brushIsEmpty; - } } } @@ -817,8 +810,7 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> { <button className="remove-button" onPointerDown={e => this.deleteLink()}>Remove link</button> </div>; - return <ButtonDropdown view={this.view} key={"link button"} button={button} dropdownContent={dropdownContent} - openDropdownOnButton={true} link={true} />; + return <ButtonDropdown view={this.view} key={"link button"} button={button} dropdownContent={dropdownContent} openDropdownOnButton={true} link={true} />; } async getTextLinkTargetTitle() { @@ -1027,6 +1019,7 @@ interface ButtonDropdownProps { openDropdownOnButton?: boolean; link?: boolean; pdf?: boolean; + } @observer @@ -1071,9 +1064,11 @@ export class ButtonDropdown extends React.Component<ButtonDropdownProps> { return ( <div className="button-dropdown-wrapper" ref={node => this.ref = node}> {!this.props.pdf ? - <div className="antimodeMenu-button dropdown-button-combined" onPointerDown={this.onDropdownClick}> + <div className="antimodeMenu-button dropdown-button-combined" onPointerDown={this.props.openDropdownOnButton ? this.onDropdownClick : undefined}> {this.props.button} - <div style={{ marginTop: "-8.5" }}><FontAwesomeIcon icon="caret-down" size="sm" /></div> + <div style={{ marginTop: "-8.5", position: "relative" }} onPointerDown={!this.props.openDropdownOnButton ? this.onDropdownClick : undefined}> + <FontAwesomeIcon icon="caret-down" size="sm" /> + </div> </div> : <> diff --git a/src/client/views/pdf/PDFMenu.scss b/src/client/views/pdf/AnchorMenu.scss index fa43a99b2..b7afb26a5 100644 --- a/src/client/views/pdf/PDFMenu.scss +++ b/src/client/views/pdf/AnchorMenu.scss @@ -1,4 +1,4 @@ -.pdfMenu-addTag { +.anchorMenu-addTag { display: grid; width: 200px; padding: 5px; diff --git a/src/client/views/pdf/PDFMenu.tsx b/src/client/views/pdf/AnchorMenu.tsx index 1f8872e3d..d1fdc6c44 100644 --- a/src/client/views/pdf/PDFMenu.tsx +++ b/src/client/views/pdf/AnchorMenu.tsx @@ -1,18 +1,19 @@ import React = require("react"); import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { Tooltip } from "@material-ui/core"; -import { action, computed, observable } from "mobx"; +import { action, computed, observable, IReactionDisposer, reaction } from "mobx"; import { observer } from "mobx-react"; import { ColorState } from "react-color"; import { Doc, Opt } from "../../../fields/Doc"; import { returnFalse, setupMoveUpEvents, unimplementedFunction, Utils } from "../../../Utils"; import { AntimodeMenu, AntimodeMenuProps } from "../AntimodeMenu"; import { ButtonDropdown } from "../nodes/formattedText/RichTextMenu"; -import "./PDFMenu.scss"; +import "./AnchorMenu.scss"; +import { SelectionManager } from "../../util/SelectionManager"; @observer -export class PDFMenu extends AntimodeMenu<AntimodeMenuProps> { - static Instance: PDFMenu; +export class AnchorMenu extends AntimodeMenu<AntimodeMenuProps> { + static Instance: AnchorMenu; private _commentCont = React.createRef<HTMLButtonElement>(); private _palette = [ @@ -39,7 +40,7 @@ export class PDFMenu extends AntimodeMenu<AntimodeMenuProps> { @observable public _colorBtn = false; @observable public Highlighting: boolean = false; - @observable public Status: "pdf" | "annotation" | "" = ""; + @observable public Status: "marquee" | "annotation" | "" = ""; public StartDrag: (e: PointerEvent, ele: HTMLElement) => void = unimplementedFunction; public Highlight: (color: string) => Opt<Doc> = (color: string) => undefined; @@ -54,8 +55,16 @@ export class PDFMenu extends AntimodeMenu<AntimodeMenuProps> { constructor(props: Readonly<{}>) { super(props); - PDFMenu.Instance = this; - PDFMenu.Instance._canFade = false; + AnchorMenu.Instance = this; + AnchorMenu.Instance._canFade = false; + } + + _disposer: IReactionDisposer | undefined; + componentDidMount() { + this._disposer = reaction(() => SelectionManager.Views(), + selected => { + AnchorMenu.Instance.fadeOut(true); + }); } pointerDown = (e: React.PointerEvent) => { @@ -119,7 +128,7 @@ export class PDFMenu extends AntimodeMenu<AntimodeMenuProps> { } render() { - const buttons = this.Status === "pdf" ? + const buttons = this.Status === "marquee" ? [ this.highlighter, @@ -144,7 +153,7 @@ export class PDFMenu extends AntimodeMenu<AntimodeMenuProps> { <FontAwesomeIcon icon="thumbtack" size="lg" /> </button> </Tooltip>, - // <div key="7" className="pdfMenu-addTag" > + // <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>, diff --git a/src/client/views/pdf/Annotation.tsx b/src/client/views/pdf/Annotation.tsx index 5ef57f986..cd32c2c3a 100644 --- a/src/client/views/pdf/Annotation.tsx +++ b/src/client/views/pdf/Annotation.tsx @@ -8,7 +8,7 @@ import { BoolCast, Cast, FieldValue, NumCast, PromiseValue, StrCast } from "../. import { LinkManager } from "../../util/LinkManager"; import { undoBatch } from "../../util/UndoManager"; import "./Annotation.scss"; -import { PDFMenu } from "./PDFMenu"; +import { AnchorMenu } from "./AnchorMenu"; interface IAnnotationProps { anno: Doc; @@ -67,8 +67,8 @@ class RegionAnnotation extends React.Component<IRegionAnnotationProps> { } componentWillUnmount() { - this._brushDisposer && this._brushDisposer(); - this._reactionDisposer && this._reactionDisposer(); + this._brushDisposer?.(); + this._reactionDisposer?.(); } @undoBatch @@ -83,8 +83,8 @@ class RegionAnnotation extends React.Component<IRegionAnnotationProps> { DocListCast(group.annotations).forEach(anno => anno.delete = true); } - - PDFMenu.Instance.fadeOut(true); + AnchorMenu.Instance.fadeOut(true); + this.props.select(false); } @undoBatch @@ -105,14 +105,14 @@ class RegionAnnotation extends React.Component<IRegionAnnotationProps> { @action onPointerDown = (e: React.PointerEvent) => { if (e.button === 2 || e.ctrlKey) { - PDFMenu.Instance.Status = "annotation"; - PDFMenu.Instance.Delete = this.deleteAnnotation.bind(this); - PDFMenu.Instance.Pinned = false; - PDFMenu.Instance.AddTag = this.addTag.bind(this); - PDFMenu.Instance.PinToPres = this.pinToPres; - PDFMenu.Instance.MakePushpin = this.makePushpin; - PDFMenu.Instance.IsPushpin = this.isPushpin; - PDFMenu.Instance.jumpTo(e.clientX, e.clientY, true); + 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.jumpTo(e.clientX, e.clientY, true); e.stopPropagation(); } else if (e.button === 0) { diff --git a/src/client/views/pdf/PDFViewer.scss b/src/client/views/pdf/PDFViewer.scss index 0ecb4dba4..d17b2b5ef 100644 --- a/src/client/views/pdf/PDFViewer.scss +++ b/src/client/views/pdf/PDFViewer.scss @@ -34,6 +34,8 @@ border: unset; } .pdfViewerDash-text-selected { + position: relative; + z-index: -1; .textLayer{ pointer-events: all; user-select: text; @@ -46,12 +48,6 @@ } } - .pdfViewerDash-dragAnnotationBox { - position:absolute; - background-color: transparent; - opacity: 0.1; - } - .pdfViewerDash-overlay, .pdfViewerDash-overlay-inking { transform-origin: left top; position: absolute; @@ -83,11 +79,6 @@ width: 100%; pointer-events: none; mix-blend-mode: multiply; // bcz: makes text fuzzy! - - .pdfViewerDash-annotationBox { - position: absolute; - background-color: rgba(245, 230, 95, 0.616); - } } .pdfViewerDash-waiting { width: 70%; @@ -102,5 +93,4 @@ .pdfViewerDash-interactive { pointer-events: all; -} -
\ No newline at end of file +}
\ No newline at end of file diff --git a/src/client/views/pdf/PDFViewer.tsx b/src/client/views/pdf/PDFViewer.tsx index 8700a5739..fa97bde3f 100644 --- a/src/client/views/pdf/PDFViewer.tsx +++ b/src/client/views/pdf/PDFViewer.tsx @@ -3,35 +3,30 @@ import { observer } from "mobx-react"; import * as Pdfjs from "pdfjs-dist"; import "pdfjs-dist/web/pdf_viewer.css"; import { Dictionary } from "typescript-collections"; -import { AclAddonly, AclAdmin, AclEdit, DataSym, Doc, DocListCast, Field, HeightSym, Opt, WidthSym } from "../../../fields/Doc"; +import { Doc, DocListCast, Field, HeightSym, Opt, WidthSym } from "../../../fields/Doc"; import { documentSchema } from "../../../fields/documentSchemas"; import { Id } from "../../../fields/FieldSymbols"; import { InkTool } from "../../../fields/InkField"; -import { List } from "../../../fields/List"; import { createSchema, makeInterface } from "../../../fields/Schema"; import { ScriptField } from "../../../fields/ScriptField"; import { Cast, NumCast, StrCast } from "../../../fields/Types"; import { PdfField } from "../../../fields/URLField"; -import { GetEffectiveAcl, TraceMobx } from "../../../fields/util"; +import { TraceMobx } from "../../../fields/util"; import { addStyleSheet, addStyleSheetRule, clearStyleSheetRules, emptyFunction, OmitKeys, smoothScroll, Utils } from "../../../Utils"; -import { Docs, DocUtils } from "../../documents/Documents"; -import { DocumentType } from "../../documents/DocumentTypes"; +import { DocUtils } from "../../documents/Documents"; import { Networking } from "../../Network"; -import { CurrentUserUtils } from "../../util/CurrentUserUtils"; -import { DragManager } from "../../util/DragManager"; import { CompiledScript, CompileScript } from "../../util/Scripting"; import { SelectionManager } from "../../util/SelectionManager"; import { SharingManager } from "../../util/SharingManager"; import { SnappingManager } from "../../util/SnappingManager"; -import { undoBatch } from "../../util/UndoManager"; import { CollectionFreeFormView } from "../collections/collectionFreeForm/CollectionFreeFormView"; import { ViewBoxAnnotatableComponent } from "../DocComponent"; +import { MarqueeAnnotator } from "../MarqueeAnnotator"; import { FieldViewProps } from "../nodes/FieldView"; -import { FormattedTextBox } from "../nodes/formattedText/FormattedTextBox"; import { FormattedTextBoxComment } from "../nodes/formattedText/FormattedTextBoxComment"; import { LinkDocPreview } from "../nodes/LinkDocPreview"; import { Annotation } from "./Annotation"; -import { PDFMenu } from "./PDFMenu"; +import { AnchorMenu } from "./AnchorMenu"; import "./PDFViewer.scss"; const pdfjs = require('pdfjs-dist/es5/build/pdf.js'); import React = require("react"); @@ -73,11 +68,7 @@ export class PDFViewer extends ViewBoxAnnotatableComponent<IViewerProps, PdfDocu @observable private _savedAnnotations: Dictionary<number, HTMLDivElement[]> = new Dictionary<number, HTMLDivElement[]>(); @observable private _script: CompiledScript = CompileScript("return true") as CompiledScript; @observable private Index: number = -1; - @observable private _marqueeX: number = 0; - @observable private _marqueeY: number = 0; - @observable private _marqueeWidth: number = 0; - @observable private _marqueeHeight: number = 0; - @observable private _marqueeing: boolean = false; + @observable private _marqueeing: number[] | undefined; @observable private _showWaiting = true; @observable private _showCover = false; @observable private _zoomed = 1; @@ -92,8 +83,6 @@ export class PDFViewer extends ViewBoxAnnotatableComponent<IViewerProps, PdfDocu private _viewer: React.RefObject<HTMLDivElement> = React.createRef(); private _mainCont: React.RefObject<HTMLDivElement> = React.createRef(); private _selectionText: string = ""; - private _startX: number = 0; - private _startY: number = 0; private _downX: number = 0; private _downY: number = 0; private _coverPath: any; @@ -148,7 +137,6 @@ export class PDFViewer extends ViewBoxAnnotatableComponent<IViewerProps, PdfDocu if (!selected) { this._savedAnnotations.values().forEach(v => v.forEach(a => a.remove())); this._savedAnnotations.keys().forEach(k => this._savedAnnotations.setValue(k, [])); - PDFMenu.Instance.fadeOut(true); } (SelectionManager.Views().length === 1) && this.setupPdfJsViewer(); }, @@ -196,13 +184,13 @@ export class PDFViewer extends ViewBoxAnnotatableComponent<IViewerProps, PdfDocu } copy = (e: ClipboardEvent) => { - if (this.props.active(true) && e.clipboardData) { - const annoDoc = this.makeAnnotationDocument("rgba(3,144,152,0.3)"); // copied text markup color (blueish) - if (annoDoc) { - e.clipboardData.setData("text/plain", this._selectionText); - e.clipboardData.setData("dash/pdfOrigin", this.props.Document[Id]); - e.clipboardData.setData("dash/pdfRegion", annoDoc[Id]); - } + if (this.active() && e.clipboardData) { + //const annoDoc = this.makeAnnotationDocument("rgba(3,144,152,0.3)"); // copied text markup color (blueish) + //if (annoDoc) { + e.clipboardData.setData("text/plain", this._selectionText); + // e.clipboardData.setData("dash/pdfOrigin", this.props.Document[Id]); + // e.clipboardData.setData("dash/pdfRegion", annoDoc[Id]); + //} e.preventDefault(); } } @@ -292,57 +280,7 @@ export class PDFViewer extends ViewBoxAnnotatableComponent<IViewerProps, PdfDocu this._pdfViewer.setDocument(this.props.pdf); } - @undoBatch - @action - makeAnnotationDocument = (color: string): Opt<Doc> => { - if (this._savedAnnotations.size() === 0) return undefined; - // let mainAnnoDoc = Docs.Create.InstanceFromProto(new Doc(), "", {}); - let mainAnnoDoc = Docs.Create.FreeformDocument([], { title: "anno", _width: 1, _height: 1 }); - let mainAnnoDocProto = Doc.GetProto(mainAnnoDoc); - const annoDocs: Doc[] = []; - let maxX = -Number.MAX_VALUE; - let minY = Number.MAX_VALUE; - if ((this._savedAnnotations.values()[0][0] as any).marqueeing) { - const anno = this._savedAnnotations.values()[0][0]; - const annoDoc = Docs.Create.FreeformDocument([], { backgroundColor: color.replace(/[0-9.]*\)/, ".3)"), title: "Annotation on " + this.Document.title }); - if (anno.style.left) annoDoc.x = parseInt(anno.style.left); - if (anno.style.top) annoDoc.y = parseInt(anno.style.top); - if (anno.style.height) annoDoc._height = parseInt(anno.style.height); - if (anno.style.width) annoDoc._width = parseInt(anno.style.width); - annoDoc.group = mainAnnoDoc; - annoDocs.push(annoDoc); - anno.remove(); - mainAnnoDoc = annoDoc; - mainAnnoDocProto.type = DocumentType.COL; - mainAnnoDocProto = Doc.GetProto(mainAnnoDoc); - mainAnnoDocProto.y = annoDoc.y; - } else { - this._savedAnnotations.forEach((key: number, value: HTMLDivElement[]) => value.map(anno => { - const annoDoc = new Doc(); - if (anno.style.left) annoDoc.x = parseInt(anno.style.left); - if (anno.style.top) annoDoc.y = parseInt(anno.style.top); - if (anno.style.height) annoDoc._height = parseInt(anno.style.height); - if (anno.style.width) annoDoc._width = parseInt(anno.style.width); - annoDoc.group = mainAnnoDoc; - annoDoc.backgroundColor = color; - annoDocs.push(annoDoc); - anno.remove(); - (annoDoc.y !== undefined) && (minY = Math.min(NumCast(annoDoc.y), minY)); - (annoDoc.x !== undefined) && (maxX = Math.max(NumCast(annoDoc.x) + NumCast(annoDoc._width), maxX)); - })); - - mainAnnoDocProto.y = Math.max(minY, 0); - mainAnnoDocProto.x = Math.max(maxX, 0); - mainAnnoDocProto.type = DocumentType.PDFANNO; - mainAnnoDocProto.text = this._selectionText; - mainAnnoDocProto.annotations = new List<Doc>(annoDocs); - } - mainAnnoDocProto.title = "Annotation on " + this.Document.title; - mainAnnoDocProto.annotationOn = this.props.Document; - this._savedAnnotations.clear(); - this.Index = -1; - return mainAnnoDoc; - } + @action prevAnnotation = () => { this.Index = Math.max(this.Index - 1, 0); @@ -400,25 +338,6 @@ export class PDFViewer extends ViewBoxAnnotatableComponent<IViewerProps, PdfDocu return index; } - @action - createAnnotation = (div: HTMLDivElement, page: number) => { - if (this._annotationLayer.current) { - if (div.style.top) { - div.style.top = (parseInt(div.style.top)/*+ this.getScrollFromPage(page)*/).toString(); - } - this._annotationLayer.current.append(div); - div.style.backgroundColor = "#ACCEF7"; - div.style.opacity = "0.5"; - const savedPage = this._savedAnnotations.getValue(page); - if (savedPage) { - savedPage.push(div); - this._savedAnnotations.setValue(page, savedPage); - } - else { - this._savedAnnotations.setValue(page, [div]); - } - } - } @action search = (searchString: string, fwd: boolean, clear: boolean = false) => { @@ -448,7 +367,6 @@ export class PDFViewer extends ViewBoxAnnotatableComponent<IViewerProps, PdfDocu this._mainCont.current.addEventListener("pagesloaded", executeFind); this._mainCont.current.addEventListener("pagerendered", executeFind); } - } @action @@ -460,57 +378,50 @@ export class PDFViewer extends ViewBoxAnnotatableComponent<IViewerProps, PdfDocu // if alt+left click, drag and annotate this._downX = e.clientX; this._downY = e.clientY; - (e.target as any).tagName === "SPAN" && (this._styleRule = addStyleSheetRule(PDFViewer._annotationStyle, "pdfAnnotation", { "pointer-events": "none" })); if ((this.Document._viewScale || 1) !== 1) return; if ((e.button !== 0 || e.altKey) && this.active(true)) { this._setPreviewCursor?.(e.clientX, e.clientY, true); } - this._marqueeing = false; if (!e.altKey && e.button === 0 && this.active(true)) { - // clear out old marquees and initialize menu for new selection - PDFMenu.Instance.StartDrag = this.startDrag; - PDFMenu.Instance.Highlight = this.highlight; - PDFMenu.Instance.Status = "pdf"; - PDFMenu.Instance.fadeOut(true); - this._savedAnnotations.values().forEach(v => v.forEach(a => a.remove())); - this._savedAnnotations.keys().forEach(k => this._savedAnnotations.setValue(k, [])); - if (e.target && (e.target as any).parentElement.className === "textLayer") { - // start selecting text if mouse down on textLayer spans - } - else if (this._mainCont.current) { - // set marquee x and y positions to the spatially transformed position - const boundingRect = this._mainCont.current.getBoundingClientRect(); - this._startX = this._marqueeX = (e.clientX - boundingRect.left) * (this._mainCont.current.offsetWidth / boundingRect.width); - this._startY = this._marqueeY = (e.clientY - boundingRect.top) * (this._mainCont.current.offsetHeight / boundingRect.height) + this._mainCont.current.scrollTop; - this._marqueeHeight = this._marqueeWidth = 0; - this._marqueeing = true; + if (e.target && ((e.target as any).className.includes("endOfContent") || ((e.target as any).parentElement.className !== "textLayer"))) { + this._marqueeing = [e.clientX, e.clientY]; // if texLayer is hit, then we select text instead of using a marquee + } else { + // clear out old marquees and initialize menu for new selection + AnchorMenu.Instance.Status = "marquee"; + this._savedAnnotations.values().forEach(v => v.forEach(a => a.remove())); + this._savedAnnotations.clear(); + this._styleRule = addStyleSheetRule(PDFViewer._annotationStyle, "pdfAnnotation", { "pointer-events": "none" }); + document.addEventListener("pointerup", this.onSelectEnd); } document.addEventListener("pointermove", this.onSelectMove); - document.addEventListener("pointerup", this.onSelectEnd); - document.addEventListener("pointerup", this.removeStyle, true); } } - removeStyle = () => { - clearStyleSheetRules(PDFViewer._annotationStyle); - document.removeEventListener("pointerup", this.removeStyle); + + @action + finishMarquee = () => { + this._marqueeing = undefined; + document.removeEventListener("pointermove", this.onSelectMove); + this.props.select(false); } @action onSelectMove = (e: PointerEvent): void => { - if (this._marqueeing && this._mainCont.current) { - // transform positions and find the width and height to set the marquee to - const boundingRect = this._mainCont.current.getBoundingClientRect(); - this._marqueeWidth = ((e.clientX - boundingRect.left) * (this._mainCont.current.offsetWidth / boundingRect.width)) - this._startX; - this._marqueeHeight = ((e.clientY - boundingRect.top) * (this._mainCont.current.offsetHeight / boundingRect.height)) - this._startY + this._mainCont.current.scrollTop; - this._marqueeX = Math.min(this._startX, this._startX + this._marqueeWidth); - this._marqueeY = Math.min(this._startY, this._startY + this._marqueeHeight); - this._marqueeWidth = Math.abs(this._marqueeWidth); - this._marqueeHeight = Math.abs(this._marqueeHeight); - e.stopPropagation(); - e.preventDefault(); - } - else if (e.target && (e.target as any).parentElement === this._mainCont.current) { - e.stopPropagation(); + // if (e.target && (e.target as any).parentElement === this._mainCont.current) + e.stopPropagation(); + } + + @action + onSelectEnd = (e: PointerEvent): void => { + clearStyleSheetRules(PDFViewer._annotationStyle); + this.props.select(false); + document.removeEventListener("pointermove", this.onSelectMove); + document.removeEventListener("pointerup", this.onSelectEnd); + + const sel = window.getSelection(); + if (sel?.type === "Range") { + const selRange = sel.getRangeAt(0); + this.createTextAnnotation(sel, selRange); + AnchorMenu.Instance.jumpTo(e.clientX, e.clientY); } } @@ -522,17 +433,16 @@ export class PDFViewer extends ViewBoxAnnotatableComponent<IViewerProps, PdfDocu for (let i = 0; i < clientRects.length; i++) { const rect = clientRects.item(i); if (rect) { - const scaleY = this._mainCont.current.offsetHeight / boundingRect.height; const scaleX = this._mainCont.current.offsetWidth / boundingRect.width; if (rect.width !== this._mainCont.current.clientWidth) { const annoBox = document.createElement("div"); - annoBox.className = "pdfViewerDash-annotationBox"; + annoBox.className = "marqueeAnnotator-annotationBox"; // transforms the positions from screen onto the pdf div annoBox.style.top = ((rect.top - boundingRect.top) * scaleX / this._zoomed + this._mainCont.current.scrollTop).toString(); annoBox.style.left = ((rect.left - boundingRect.left) * scaleX / this._zoomed).toString(); annoBox.style.width = (rect.width * this._mainCont.current.offsetWidth / boundingRect.width / this._zoomed).toString(); annoBox.style.height = (rect.height * this._mainCont.current.offsetHeight / boundingRect.height / this._zoomed).toString(); - this.createAnnotation(annoBox, this.getPageFromScroll(rect.top)); + this._annotationLayer.current && MarqueeAnnotator.previewNewAnnotation(this._savedAnnotations, this._annotationLayer.current, annoBox, this.getPageFromScroll(rect.top)); } } } @@ -547,117 +457,21 @@ export class PDFViewer extends ViewBoxAnnotatableComponent<IViewerProps, PdfDocu } } - @action - onSelectEnd = (e: PointerEvent): void => { - clearStyleSheetRules(PDFViewer._annotationStyle); - this.props.select(false); - this._savedAnnotations.clear(); - if (this._marqueeing) { - if (this._marqueeWidth > 10 || this._marqueeHeight > 10) { - const marquees = this._mainCont.current!.getElementsByClassName("pdfViewerDash-dragAnnotationBox"); - if (marquees?.length) { // copy the marquee and convert it to a permanent annotation. - const style = (marquees[0] as HTMLDivElement).style; - const copy = document.createElement("div"); - copy.style.left = style.left; - copy.style.top = style.top; - copy.style.width = style.width; - copy.style.height = style.height; - copy.style.border = style.border; - copy.style.opacity = style.opacity; - (copy as any).marqueeing = true; - copy.className = "pdfViewerDash-annotationBox"; - this.createAnnotation(copy, this.getPageFromScroll(this._marqueeY)); - } - - if (!e.ctrlKey) { - PDFMenu.Instance.Marquee = { left: this._marqueeX, top: this._marqueeY, width: this._marqueeWidth, height: this._marqueeHeight }; - } - PDFMenu.Instance.jumpTo(e.clientX, e.clientY); - } - this._marqueeing = false; - } - else { - const sel = window.getSelection(); - if (sel?.type === "Range") { - const selRange = sel.getRangeAt(0); - this.createTextAnnotation(sel, selRange); - PDFMenu.Instance.jumpTo(e.clientX, e.clientY); - } - } - - if (PDFMenu.Instance.Highlighting) {// when highlighter has been toggled when menu is pinned, we auto-highlight immediately on mouse up - this.highlight("rgba(245, 230, 95, 0.75)"); // yellowish highlight color for highlighted text (should match PDFMenu's highlight color) - } - else { - PDFMenu.Instance.StartDrag = this.startDrag; - PDFMenu.Instance.Highlight = this.highlight; - } - document.removeEventListener("pointermove", this.onSelectMove); - document.removeEventListener("pointerup", this.onSelectEnd); - } - - @action - highlight = (color: string) => { - // creates annotation documents for current highlights - const effectiveAcl = GetEffectiveAcl(this.props.Document[DataSym]); - const annotationDoc = [AclAddonly, AclEdit, AclAdmin].includes(effectiveAcl) && this.makeAnnotationDocument(color); - annotationDoc && this.addDocument?.(annotationDoc); - return annotationDoc as Doc ?? undefined; - } - - /** - * This is temporary for creating annotations from highlights. It will - * start a drag event and create or put the necessary info into the drag event. - */ - @action - startDrag = async (e: PointerEvent, ele: HTMLElement) => { - e.preventDefault(); - e.stopPropagation(); - - const clipDoc = Doc.MakeAlias(this.dataDoc); - clipDoc._fitWidth = true; - clipDoc._width = this.marqueeWidth(); - clipDoc._height = this.marqueeHeight(); - clipDoc._scrollTop = this.marqueeY(); - const targetDoc = CurrentUserUtils.GetNewTextDoc("Note linked to " + this.props.Document.title, 0, 0, 100, 100); - FormattedTextBox.SelectOnLoad = targetDoc[Id]; - Doc.GetProto(targetDoc).data = new List<Doc>([clipDoc]); - clipDoc.rootDocument = targetDoc; - // DocUtils.makeCustomViewClicked(targetDoc, Docs.Create.StackingDocument, "slideView", undefined); - // targetDoc.layoutKey = "layout"; - // const targetDoc = Docs.Create.TextDocument("", { _width: 200, _height: 200, title: "Note linked to " + this.props.Document.title }); - // Doc.GetProto(targetDoc).snipped = this.dataDoc[this.props.fieldKey][Copy](); - // const snipLayout = Docs.Create.PdfDocument("http://www.msn.com", { title: "snippetView", isTemplateDoc: true, isTemplateForField: "snipped", _fitWidth: true, _width: this.marqueeWidth(), _height: this.marqueeHeight(), _scrollTop: this.marqueeY() }); - // Doc.GetProto(snipLayout).layout = PDFBox.LayoutString("snipped"); - const annotationDoc = this.highlight("rgba(173, 216, 230, 0.75)"); // hyperlink color - if (annotationDoc) { - DragManager.StartPdfAnnoDrag([ele], new DragManager.PdfAnnoDragData(this.props.Document, annotationDoc, targetDoc), e.pageX, e.pageY, { - dragComplete: e => { - if (!e.aborted && e.annoDragData && !e.linkDocument) { - e.linkDocument = DocUtils.MakeLink({ doc: annotationDoc }, { doc: e.annoDragData.dropDocument }, "Annotation"); - } - annotationDoc.isLinkButton = true; // prevents link button fro showing up --- maybe not a good thing? - annotationDoc.isPushpin = e.annoDragData?.dropDocument.annotationOn === this.props.Document; - e.linkDocument && e.annoDragData?.linkDropCallback?.(e as { linkDocument: Doc });// bcz: typescript can't figure out that this is valid even though we tested e.linkDocument above - } - }); - } - } - scrollXf = () => { return this._mainCont.current ? this.props.ScreenToLocalTransform().translate(0, this.layoutDoc._scrollTop || 0) : this.props.ScreenToLocalTransform(); } + onClick = (e: React.MouseEvent) => { - this._setPreviewCursor && - e.button === 0 && - Math.abs(e.clientX - this._downX) < 3 && - Math.abs(e.clientY - this._downY) < 3 && + 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); + } + e.stopPropagation(); } setPreviewCursor = (func?: (x: number, y: number, drag: boolean) => void) => this._setPreviewCursor = func; - getCoverImage = () => { if (!this.props.Document[HeightSym]() || !Doc.NativeHeight(this.props.Document)) { setTimeout((() => { @@ -745,11 +559,6 @@ export class PDFViewer extends ViewBoxAnnotatableComponent<IViewerProps, PdfDocu {this._showWaiting ? <img className="pdfViewerDash-waiting" key="waiting" src={"/assets/loading.gif"} /> : (null)} </>; } - marqueeWidth = () => this._marqueeWidth; - marqueeHeight = () => this._marqueeHeight; - marqueeX = () => this._marqueeX; - marqueeY = () => this._marqueeY; - marqueeing = () => this._marqueeing; contentZoom = () => this._zoomed; render() { TraceMobx(); @@ -766,29 +575,8 @@ export class PDFViewer extends ViewBoxAnnotatableComponent<IViewerProps, PdfDocu {this.overlayLayer} {this.overlayInfo} {this.standinViews} - <PdfViewerMarquee isMarqueeing={this.marqueeing} width={this.marqueeWidth} height={this.marqueeHeight} x={this.marqueeX} y={this.marqueeY} /> + {!this._marqueeing || !this._mainCont.current || !this._annotationLayer.current ? (null) : + <MarqueeAnnotator rootDoc={this.rootDoc} down={this._marqueeing} addDocument={this.addDocument} finishMarquee={this.finishMarquee} getPageFromScroll={this.getPageFromScroll} savedAnnotations={this._savedAnnotations} annotationLayer={this._annotationLayer.current} mainCont={this._mainCont.current} />} </div >; } -} - -export interface PdfViewerMarqueeProps { - isMarqueeing: () => boolean; - width: () => number; - height: () => number; - x: () => number; - y: () => number; -} - -@observer -export class PdfViewerMarquee extends React.Component<PdfViewerMarqueeProps> { - render() { - return !this.props.isMarqueeing() ? (null) : <div className="pdfViewerDash-dragAnnotationBox" - style={{ - left: `${this.props.x()}px`, top: `${this.props.y()}px`, - width: `${this.props.width()}px`, height: `${this.props.height()}px`, - border: `${this.props.width() === 0 ? "" : "2px dashed black"}`, - opacity: 0.2 - }}> - </div>; - } -} +}
\ No newline at end of file diff --git a/src/mobile/ImageUpload.tsx b/src/mobile/ImageUpload.tsx index 65f9e40ff..2183d2172 100644 --- a/src/mobile/ImageUpload.tsx +++ b/src/mobile/ImageUpload.tsx @@ -57,7 +57,7 @@ export class Uploader extends React.Component<ImageUploadProps> { doc = Docs.Create.VideoDocument(path, { _nativeWidth: defaultNativeImageDim, _width: 400, title: name }); // Case 2: File is a PDF document } else if (file.type === "application/pdf") { - doc = Docs.Create.PdfDocument(path, { _nativeWidth: defaultNativeImageDim, _width: 400, _fitWidth: true, title: name }); + doc = Docs.Create.PdfDocument(path, { _nativeWidth: defaultNativeImageDim, _width: 400, title: name }); // Case 3: File is another document type (most likely Image) } else { doc = Docs.Create.ImageDocument(path, { _nativeWidth: defaultNativeImageDim, _width: 400, title: name }); |