aboutsummaryrefslogtreecommitdiff
path: root/src/client/views/MarqueeAnnotator.tsx
diff options
context:
space:
mode:
authorusodhi <61431818+usodhi@users.noreply.github.com>2021-01-20 20:07:48 -0500
committerusodhi <61431818+usodhi@users.noreply.github.com>2021-01-20 20:07:48 -0500
commit7b49bca26f95e7bf1e878d4987f061511a5def39 (patch)
treeb160034ebfd770c73071203410b468778fa93697 /src/client/views/MarqueeAnnotator.tsx
parente4c9218e217b617e513259cac90b077431890eaa (diff)
parent68785a97178d229935c0429791081d7c09312dc3 (diff)
Merge branch 'master' of https://github.com/browngraphicslab/Dash-Web into filters
Diffstat (limited to 'src/client/views/MarqueeAnnotator.tsx')
-rw-r--r--src/client/views/MarqueeAnnotator.tsx215
1 files changed, 215 insertions, 0 deletions
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>;
+ }
+}