aboutsummaryrefslogtreecommitdiff
path: root/src/client/views/MarqueeAnnotator.tsx
diff options
context:
space:
mode:
authorbobzel <zzzman@gmail.com>2023-11-22 11:52:57 -0500
committerbobzel <zzzman@gmail.com>2023-11-22 11:52:57 -0500
commitcc75a03d89a4b553a53b55404464cd2ca93d9b48 (patch)
treed4c455549245a287656d7e03ff9e459231251829 /src/client/views/MarqueeAnnotator.tsx
parent52241c5a42c0fa2d92eca8110523081ce9f353af (diff)
fixed more issues with rotation. restrutured how MarqueeAnnotator works to be simpler and more correct.
Diffstat (limited to 'src/client/views/MarqueeAnnotator.tsx')
-rw-r--r--src/client/views/MarqueeAnnotator.tsx284
1 files changed, 131 insertions, 153 deletions
diff --git a/src/client/views/MarqueeAnnotator.tsx b/src/client/views/MarqueeAnnotator.tsx
index 10d2d8568..20c7a08fa 100644
--- a/src/client/views/MarqueeAnnotator.tsx
+++ b/src/client/views/MarqueeAnnotator.tsx
@@ -1,4 +1,4 @@
-import { action, observable, ObservableMap, runInAction } from 'mobx';
+import { action, computed, observable, ObservableMap, runInAction, trace } from 'mobx';
import { observer } from 'mobx-react';
import { Doc, Opt } from '../../fields/Doc';
import { AclAdmin, AclAugment, AclEdit, AclSelfEdit, DocData } from '../../fields/DocSymbols';
@@ -21,13 +21,13 @@ const _global = (window /* browser */ || global) /* node */ as any;
export interface MarqueeAnnotatorProps {
rootDoc: Doc;
down?: number[];
- iframe?: () => undefined | HTMLIFrameElement;
scrollTop: number;
scaling?: () => number;
- iframeScaling?: () => number;
+ annotationLayerScaling?: () => number;
+ annotationLayerScrollTop: number;
containerOffset?: () => number[];
mainCont: HTMLDivElement;
- docView: DocumentView;
+ docView: () => DocumentView;
savedAnnotations: () => ObservableMap<number, HTMLDivElement[]>;
selectionText: () => string;
annotationLayer: HTMLDivElement;
@@ -40,27 +40,11 @@ export interface MarqueeAnnotatorProps {
}
@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;
+ private _start: { x: number; y: number } = { x: 0, y: 0 };
@observable private _width: number = 0;
@observable private _height: number = 0;
-
- constructor(props: any) {
- super(props);
-
- AnchorMenu.Instance.OnCrop = (e: PointerEvent) => {
- if (this.props.anchorMenuCrop) {
- UndoManager.RunInBatch(() => this.props.anchorMenuCrop?.(this.highlight('', true, undefined, false), true), 'cropping');
- }
- };
- AnchorMenu.Instance.OnClick = undoable((e: PointerEvent) => this.props.anchorMenuClick?.()?.(this.highlight(this.props.highlightDragSrcColor ?? 'rgba(173, 216, 230, 0.75)', true, undefined, true)), 'make sidebar annotation');
- AnchorMenu.Instance.OnAudio = unimplementedFunction;
- AnchorMenu.Instance.Highlight = this.highlight;
- AnchorMenu.Instance.GetAnchor = (savedAnnotations?: ObservableMap<number, HTMLDivElement[]>, addAsAnnotation?: boolean) => this.highlight('rgba(173, 216, 230, 0.75)', true, savedAnnotations, true);
- AnchorMenu.Instance.onMakeAnchor = () => AnchorMenu.Instance.GetAnchor(undefined, true);
- }
+ @computed get top() { return Math.min(this._start.y, this._start.y + this._height); } // prettier-ignore
+ @computed get left() { return Math.min(this._start.x, this._start.x + this._width);} // prettier-ignore
@action
static clearAnnotations(savedAnnotations: ObservableMap<number, HTMLDivElement[]>) {
@@ -71,81 +55,15 @@ export class MarqueeAnnotator extends React.Component<MarqueeAnnotatorProps> {
savedAnnotations.clear();
}
- @action gotDownPoint() {
- if (!this._width && !this._height) {
- const downPt = this.props.down!;
- // set marquee x and y positions to the spatially transformed position
- const boundingRect = this.props.mainCont.getBoundingClientRect();
- this._startX = this._left = (downPt[0] - boundingRect.left) * (this.props.mainCont.offsetWidth / boundingRect.width);
- this._startY = this._top = (downPt[1] - boundingRect.top) * (this.props.mainCont.offsetHeight / boundingRect.height) + this.props.mainCont.scrollTop;
- }
-
- const doc = this.props.iframe?.()?.contentDocument ?? document;
- doc.removeEventListener('pointermove', this.onSelectMove);
- doc.removeEventListener('pointerup', this.onSelectEnd);
- doc.addEventListener('pointermove', this.onSelectMove);
- doc.addEventListener('pointerup', this.onSelectEnd);
-
- /**
- * 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((e: PointerEvent, ele: HTMLElement) => {
- e.preventDefault();
- e.stopPropagation();
- const sourceAnchorCreator = () => this.highlight(this.props.highlightDragSrcColor ?? 'rgba(173, 216, 230, 0.75)', true, undefined, true); // hyperlink color
-
- const targetCreator = (annotationOn: Doc | undefined) => {
- const target = DocUtils.GetNewTextDoc('Note linked to ' + this.props.rootDoc.title, 0, 0, 100, 100, undefined, annotationOn, undefined, 'yellow');
- FormattedTextBox.SelectOnLoad = target[Id];
- return target;
- };
- DragManager.StartAnchorAnnoDrag([ele], new DragManager.AnchorAnnoDragData(this.props.docView, sourceAnchorCreator, targetCreator), e.pageX, e.pageY, {
- dragComplete: e => {
- if (!e.aborted && e.annoDragData && e.annoDragData.linkSourceDoc && e.annoDragData.dropDocument && e.linkDocument) {
- e.annoDragData.linkSourceDoc.followLinkToggle = e.annoDragData.dropDocument.annotationOn === this.props.rootDoc;
- e.annoDragData.linkSourceDoc.followLinkZoom = false;
- }
- },
- });
- });
- /**
- * 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.StartCropDrag = !this.props.anchorMenuCrop
- ? unimplementedFunction
- : action((e: PointerEvent, ele: HTMLElement) => {
- e.preventDefault();
- e.stopPropagation();
- var cropRegion: Doc | undefined;
- const sourceAnchorCreator = () => (cropRegion = this.highlight('', true, undefined, true)); // hyperlink color
- const targetCreator = (annotationOn: Doc | undefined) => this.props.anchorMenuCrop!(cropRegion, false)!;
- DragManager.StartAnchorAnnoDrag([ele], new DragManager.AnchorAnnoDragData(this.props.docView, sourceAnchorCreator, targetCreator), e.pageX, e.pageY, {
- dragComplete: e => {
- if (!e.aborted && e.linkDocument) {
- Doc.GetProto(e.linkDocument).link_relationship = 'cropped image';
- Doc.GetProto(e.linkDocument).title = 'crop: ' + this.props.docView.rootDoc.title;
- Doc.GetProto(e.linkDocument).link_displayLine = false;
- }
- },
- });
- });
- }
- releaseDownPt() {
- const doc = this.props.iframe?.()?.contentDocument ?? document;
- doc.removeEventListener('pointermove', this.onSelectMove);
- doc.removeEventListener('pointerup', this.onSelectEnd);
- }
-
@undoBatch
@action
makeAnnotationDocument = (color: string, isLinkButton?: boolean, savedAnnotations?: ObservableMap<number, HTMLDivElement[]>): Opt<Doc> => {
const savedAnnoMap = savedAnnotations?.values() && Array.from(savedAnnotations?.values()).length ? savedAnnotations : this.props.savedAnnotations();
if (savedAnnoMap.size === 0) return undefined;
const savedAnnos = Array.from(savedAnnoMap.values())[0];
+ const doc = this.props.docView().rootDoc;
+ const scale = (this.props.annotationLayerScaling?.() || 1) * NumCast(doc._freeform_scale, 1);
if (savedAnnos.length && (savedAnnos[0] as any).marqueeing) {
- const scale = this.props.scaling?.() || 1;
const anno = savedAnnos[0];
const containerOffset = this.props.containerOffset?.() || [0, 0];
const marqueeAnno = Docs.Create.FreeformDocument([], {
@@ -154,10 +72,10 @@ export class MarqueeAnnotator extends React.Component<MarqueeAnnotatorProps> {
annotationOn: this.props.rootDoc,
title: 'Annotation on ' + this.props.rootDoc.title,
});
- marqueeAnno.x = NumCast(this.props.docView.props.Document.freeform_panX_min) + (parseInt(anno.style.left || '0') - containerOffset[0]) / scale / NumCast(this.props.docView.props.Document._freeform_scale, 1);
- marqueeAnno.y = NumCast(this.props.docView.props.Document.freeform_panY_min) + (parseInt(anno.style.top || '0') - containerOffset[1]) / scale / NumCast(this.props.docView.props.Document._freeform_scale, 1) + NumCast(this.props.scrollTop);
- marqueeAnno._height = parseInt(anno.style.height || '0') / scale / NumCast(this.props.docView.props.Document._freeform_scale, 1);
- marqueeAnno._width = parseInt(anno.style.width || '0') / scale / NumCast(this.props.docView.props.Document._freeform_scale, 1);
+ marqueeAnno.x = NumCast(doc.freeform_panX_min) + (parseInt(anno.style.left || '0') - containerOffset[0]) / scale;
+ marqueeAnno.y = NumCast(doc.freeform_panY_min) + (parseInt(anno.style.top || '0') - containerOffset[1]) / scale;
+ marqueeAnno._height = parseInt(anno.style.height || '0') / scale;
+ marqueeAnno._width = parseInt(anno.style.width || '0') / scale;
anno.remove();
savedAnnoMap.clear();
return marqueeAnno;
@@ -215,83 +133,143 @@ export class MarqueeAnnotator extends React.Component<MarqueeAnnotatorProps> {
};
public static previewNewAnnotation = action((savedAnnotations: ObservableMap<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';
+ annotationLayer.append(div);
const savedPage = savedAnnotations.get(page);
- if (savedPage) {
- savedPage.push(div);
- savedAnnotations.set(page, savedPage);
- } else {
- savedAnnotations.set(page, [div]);
- }
+ if (savedPage) savedPage.push(div);
+ savedAnnotations.set(page, savedPage ?? [div]);
});
+ getTransformedScreenPt = (down: number[]) => {
+ const boundingRect = this.props.mainCont.getBoundingClientRect();
+ const center = { x: boundingRect.x + boundingRect.width / 2, y: boundingRect.y + boundingRect.height / 2 };
+ const downPt = Utils.rotPt(down[0] - center.x, down[1] - center.y, NumCast(this.props.docView().screenToLocalTransform().Rotate));
+ const scale = this.props.docView().props.ScreenToLocalTransform().Scale;
+ const scalex = this.props.mainCont.offsetWidth / NumCast(this.props.rootDoc.width);
+ const scaley = this.props.mainCont.offsetHeight / NumCast(this.props.rootDoc.height);
+ // set marquee x and y positions to the spatially transformed position
+ return { x: scalex * (downPt.x + NumCast(this.props.rootDoc.width) / scale / 2) * scale,
+ y: scaley * (downPt.y + NumCast(this.props.rootDoc.height) / scale / 2) * scale + this.props.annotationLayerScrollTop }; // prettier-ignore
+ };
+
+ @action
+ public onInitiateSelection(down: number[]) {
+ this._width = this._height = 0;
+ this._start = this.getTransformedScreenPt(down);
+
+ document.removeEventListener('pointermove', this.onSelectMove);
+ document.removeEventListener('pointerup', this.onSelectEnd);
+ document.addEventListener('pointermove', this.onSelectMove);
+ document.addEventListener('pointerup', this.onSelectEnd);
+
+ AnchorMenu.Instance.OnCrop = (e: PointerEvent) => {
+ if (this.props.anchorMenuCrop) {
+ UndoManager.RunInBatch(() => this.props.anchorMenuCrop?.(this.highlight('', true, undefined, false), true), 'cropping');
+ }
+ };
+ AnchorMenu.Instance.OnClick = undoable((e: PointerEvent) => this.props.anchorMenuClick?.()?.(this.highlight(this.props.highlightDragSrcColor ?? 'rgba(173, 216, 230, 0.75)', true, undefined, true)), 'make sidebar annotation');
+ AnchorMenu.Instance.OnAudio = unimplementedFunction;
+ AnchorMenu.Instance.Highlight = this.highlight;
+ AnchorMenu.Instance.GetAnchor = (savedAnnotations?: ObservableMap<number, HTMLDivElement[]>, addAsAnnotation?: boolean) => this.highlight('rgba(173, 216, 230, 0.75)', true, savedAnnotations, true);
+ AnchorMenu.Instance.onMakeAnchor = () => AnchorMenu.Instance.GetAnchor(undefined, true);
+
+ /**
+ * 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((e: PointerEvent, ele: HTMLElement) => {
+ e.preventDefault();
+ e.stopPropagation();
+ const sourceAnchorCreator = () => this.highlight(this.props.highlightDragSrcColor ?? 'rgba(173, 216, 230, 0.75)', true, undefined, true); // hyperlink color
+
+ const targetCreator = (annotationOn: Doc | undefined) => {
+ const target = DocUtils.GetNewTextDoc('Note linked to ' + this.props.rootDoc.title, 0, 0, 100, 100, undefined, annotationOn, undefined, 'yellow');
+ FormattedTextBox.SelectOnLoad = target[Id];
+ return target;
+ };
+ DragManager.StartAnchorAnnoDrag([ele], new DragManager.AnchorAnnoDragData(this.props.docView(), sourceAnchorCreator, targetCreator), e.pageX, e.pageY, {
+ dragComplete: e => {
+ if (!e.aborted && e.annoDragData && e.annoDragData.linkSourceDoc && e.annoDragData.dropDocument && e.linkDocument) {
+ e.annoDragData.linkSourceDoc.followLinkToggle = e.annoDragData.dropDocument.annotationOn === this.props.rootDoc;
+ e.annoDragData.linkSourceDoc.followLinkZoom = false;
+ }
+ },
+ });
+ });
+ /**
+ * 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.StartCropDrag = !this.props.anchorMenuCrop
+ ? unimplementedFunction
+ : action((e: PointerEvent, ele: HTMLElement) => {
+ e.preventDefault();
+ e.stopPropagation();
+ var cropRegion: Doc | undefined;
+ const sourceAnchorCreator = () => (cropRegion = this.highlight('', true, undefined, true)); // hyperlink color
+ const targetCreator = (annotationOn: Doc | undefined) => this.props.anchorMenuCrop!(cropRegion, false)!;
+ DragManager.StartAnchorAnnoDrag([ele], new DragManager.AnchorAnnoDragData(this.props.docView(), sourceAnchorCreator, targetCreator), e.pageX, e.pageY, {
+ dragComplete: e => {
+ if (!e.aborted && e.linkDocument) {
+ Doc.GetProto(e.linkDocument).link_relationship = 'cropped image';
+ Doc.GetProto(e.linkDocument).title = 'crop: ' + this.props.docView().rootDoc.title;
+ Doc.GetProto(e.linkDocument).link_displayLine = false;
+ }
+ },
+ });
+ });
+ }
+ public onTerminateSelection() {
+ document.removeEventListener('pointermove', this.onSelectMove);
+ document.removeEventListener('pointerup', this.onSelectEnd);
+ }
+
@action
onSelectMove = (e: PointerEvent) => {
- // transform positions and find the width and height to set the marquee to
- const boundingRect = (this.props.iframe?.()?.contentDocument?.body || this.props.mainCont).getBoundingClientRect();
- const mainRect = this.props.mainCont.getBoundingClientRect();
- const cliX = e.clientX * (this.props.iframeScaling?.() || 1) - boundingRect.left;
- const cliY = e.clientY * (this.props.iframeScaling?.() || 1) - boundingRect.top;
- this._width = cliX * (this.props.mainCont.offsetWidth / mainRect.width) - this._startX;
- this._height = cliY * (this.props.mainCont.offsetHeight / mainRect.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);
+ const movLoc = this.getTransformedScreenPt([e.clientX, e.clientY]);
+ this._width = movLoc.x - this._start.x;
+ this._height = movLoc.y - this._start.y;
//e.stopPropagation(); // overlay documents are all 'active', yet they can be dragged. if we stop propagation, then they can be marqueed but not dragged. if we don't stop, then they will be marqueed and dragged, but the marquee will be zero width since the doc will move along with the cursor.
};
+ @action
onSelectEnd = (e: PointerEvent) => {
- const mainRect = this.props.mainCont.getBoundingClientRect();
- const cliX = e.clientX * (this.props.iframeScaling?.() || 1) + (this.props.iframe ? mainRect.left : 0);
- const cliY = e.clientY * (this.props.iframeScaling?.() || 1) + (this.props.iframe ? mainRect.top : 0);
- if (this._width > 10 || this._height > 10) {
+ e.stopPropagation();
+ const marquees = this.props.mainCont.getElementsByClassName('marqueeAnnotator-dragBox');
+ const marqueeStyle = (Array.from(marquees).lastElement() as HTMLDivElement)?.style;
+ if (!this.isEmpty && marqueeStyle) {
// 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');
- ['border', 'opacity'].forEach(prop => (copy.style[prop as any] = (marquees[0] as HTMLDivElement).style[prop as any]));
- const bounds = (marquees[0] as HTMLDivElement).getBoundingClientRect();
- const uitls = Utils.GetScreenTransform(marquees[0] as HTMLDivElement);
- const rbounds = { top: uitls.translateY, left: uitls.translateX, width: bounds.right - bounds.left, height: bounds.bottom - bounds.top };
- const otls = Utils.GetScreenTransform(this.props.annotationLayer);
- const fbounds = { top: (rbounds.top - otls.translateY) / otls.scale, left: (rbounds.left - otls.translateX) / otls.scale, width: rbounds.width / otls.scale, height: rbounds.height / otls.scale };
- copy.style.top = fbounds.top.toString() + 'px';
- copy.style.left = fbounds.left.toString() + 'px';
- copy.style.width = fbounds.width.toString() + 'px';
- copy.style.height = fbounds.height.toString() + 'px';
- 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(cliX, cliY);
-
- this.props.finishMarquee(undefined, undefined, e);
- runInAction(() => (this._width = this._height = 0));
- } else {
- runInAction(() => (this._width = this._height = 0));
- this.props.finishMarquee(cliX, cliY, e);
+ // copy the temporary marquee to allow for multiple selections (not currently available though).
+ const copy = document.createElement('div');
+ const scale = (this.props.scaling?.() || 1) * NumCast(this.props.docView().rootDoc._freeform_scale, 1);
+ ['border', 'opacity', 'top', 'left', 'width', 'height'].forEach(prop => (copy.style[prop as any] = marqueeStyle[prop as any]));
+ copy.className = 'marqueeAnnotator-annotationBox';
+ copy.style.top = parseInt(marqueeStyle.top.toString().replace('px', '')) / scale + this.props.scrollTop + 'px';
+ copy.style.left = parseInt(marqueeStyle.left.toString().replace('px', '')) / scale + 'px';
+ copy.style.width = parseInt(marqueeStyle.width.toString().replace('px', '')) / scale + 'px';
+ copy.style.height = parseInt(marqueeStyle.height.toString().replace('px', '')) / scale + 'px';
+ (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);
}
+ this.props.finishMarquee(this.isEmpty ? e.clientX : undefined, this.isEmpty ? e.clientY : undefined, e);
+ this._width = this._height = 0;
};
+ get isEmpty() {
+ return Math.abs(this._width) <= 10 && Math.abs(this._height) <= 10;
+ }
+
render() {
- return !this.props.down ? null : (
+ return (
<div
- ref={r => (r ? this.gotDownPoint() : this.releaseDownPt())}
className="marqueeAnnotator-dragBox"
style={{
- left: `${this._left}px`,
- top: `${this._top}px`,
- width: `${this._width}px`,
- height: `${this._height}px`,
+ left: `${this.left}px`,
+ top: `${this.top}px`,
+ width: `${Math.abs(this._width)}px`,
+ height: `${Math.abs(this._height)}px`,
border: `${this._width === 0 ? '' : '2px dashed black'}`,
}}
/>