Skip to content

Commit

Permalink
EPUB/snapshot: Open text context menu when selection is under annotation
Browse files Browse the repository at this point in the history
And use contextmenu event in annotation overlay, as in DOMView.

zotero/zotero#3531
  • Loading branch information
AbeJellinek committed Dec 11, 2023
1 parent 3ff731d commit 7098f78
Show file tree
Hide file tree
Showing 3 changed files with 62 additions and 28 deletions.
23 changes: 19 additions & 4 deletions src/dom/common/components/overlay/annotation-overlay.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ export type DisplayedAnnotation = {
};

export const AnnotationOverlay: React.FC<AnnotationOverlayProps> = (props) => {
let { iframe, annotations, selectedAnnotationIDs, onPointerDown, onPointerUp, onDragStart, onResizeStart, onResizeEnd } = props;
let { iframe, annotations, selectedAnnotationIDs, onPointerDown, onPointerUp, onContextMenu, onDragStart, onResizeStart, onResizeEnd } = props;

let [isResizing, setResizing] = useState(false);
let [isPointerDownOutside, setPointerDownOutside] = useState(false);
Expand Down Expand Up @@ -93,6 +93,10 @@ export const AnnotationOverlay: React.FC<AnnotationOverlayProps> = (props) => {
onPointerUp(annotation.id!, event);
}, [onPointerUp]);

let handleContextMenu = useCallback((annotation: DisplayedAnnotation, event: React.MouseEvent) => {
onContextMenu(annotation.id!, event);
}, [onContextMenu]);

let handleDragStart = useCallback((annotation: DisplayedAnnotation, dataTransfer: DataTransfer) => {
onDragStart(annotation.id!, dataTransfer);
}, [onDragStart]);
Expand Down Expand Up @@ -121,6 +125,7 @@ export const AnnotationOverlay: React.FC<AnnotationOverlayProps> = (props) => {
singleSelection={selectedAnnotationIDs.length == 1}
onPointerDown={handlePointerDown}
onPointerUp={handlePointerUp}
onContextMenu={handleContextMenu}
onDragStart={handleDragStart}
onResizeStart={handleResizeStart}
onResizeEnd={handleResizeEnd}
Expand Down Expand Up @@ -155,6 +160,7 @@ export const AnnotationOverlay: React.FC<AnnotationOverlayProps> = (props) => {
selectedAnnotationIDs={selectedAnnotationIDs}
onPointerDown={handlePointerDown}
onPointerUp={handlePointerUp}
onContextMenu={handleContextMenu}
onDragStart={handleDragStart}
pointerEventsSuppressed={pointerEventsSuppressed}
/>
Expand All @@ -169,13 +175,14 @@ type AnnotationOverlayProps = {
selectedAnnotationIDs: string[];
onPointerDown: (id: string, event: React.PointerEvent) => void;
onPointerUp: (id: string, event: React.PointerEvent) => void;
onContextMenu: (id: string, event: React.MouseEvent) => void;
onDragStart: (id: string, dataTransfer: DataTransfer) => void;
onResizeStart: (id: string) => void;
onResizeEnd: (id: string, range: Range, cancelled: boolean) => void;
};

const HighlightOrUnderline: React.FC<HighlightOrUnderlineProps> = (props) => {
let { annotation, selected, singleSelection, onPointerDown, onPointerUp, onDragStart, onResizeStart, onResizeEnd, pointerEventsSuppressed, widgetContainer } = props;
let { annotation, selected, singleSelection, onPointerDown, onPointerUp, onContextMenu, onDragStart, onResizeStart, onResizeEnd, pointerEventsSuppressed, widgetContainer } = props;
let [isResizing, setResizing] = useState(false);
let [resizedRange, setResizedRange] = useState(annotation.range);

Expand Down Expand Up @@ -292,6 +299,7 @@ const HighlightOrUnderline: React.FC<HighlightOrUnderlineProps> = (props) => {
draggable={true}
onPointerDown={onPointerDown && (event => onPointerDown!(annotation, event))}
onPointerUp={onPointerUp && (event => onPointerUp!(annotation, event))}
onContextMenu={onPointerUp && (event => onContextMenu!(annotation, event))}
onDragStart={handleDragStart}
data-annotation-id={annotation.id}
/>
Expand Down Expand Up @@ -328,6 +336,7 @@ type HighlightOrUnderlineProps = {
singleSelection: boolean;
onPointerDown?: (annotation: DisplayedAnnotation, event: React.PointerEvent) => void;
onPointerUp?: (annotation: DisplayedAnnotation, event: React.PointerEvent) => void;
onContextMenu?: (annotation: DisplayedAnnotation, event: React.MouseEvent) => void;
onDragStart?: (annotation: DisplayedAnnotation, dataTransfer: DataTransfer) => void;
onResizeStart?: (annotation: DisplayedAnnotation) => void;
onResizeEnd?: (annotation: DisplayedAnnotation, range: Range, cancelled: boolean) => void;
Expand All @@ -336,7 +345,7 @@ type HighlightOrUnderlineProps = {
};

const Note: React.FC<NoteProps> = (props) => {
let { annotation, staggerIndex, selected, onPointerDown, onPointerUp, onDragStart, disablePointerEvents } = props;
let { annotation, staggerIndex, selected, onPointerDown, onPointerUp, onContextMenu, onDragStart, disablePointerEvents } = props;

let dragImageRef = useRef<SVGSVGElement>(null);
let doc = annotation.range.commonAncestorContainer.ownerDocument;
Expand Down Expand Up @@ -375,6 +384,7 @@ const Note: React.FC<NoteProps> = (props) => {
large={true}
onPointerDown={disablePointerEvents || !onPointerDown ? undefined : (event => onPointerDown!(annotation, event))}
onPointerUp={disablePointerEvents || !onPointerUp ? undefined : (event => onPointerUp!(annotation, event))}
onContextMenu={disablePointerEvents || !onContextMenu ? undefined : (event => onContextMenu!(annotation, event))}
onDragStart={disablePointerEvents ? undefined : handleDragStart}
/>
);
Expand All @@ -386,6 +396,7 @@ type NoteProps = {
selected: boolean;
onPointerDown?: (annotation: DisplayedAnnotation, event: React.PointerEvent) => void;
onPointerUp?: (annotation: DisplayedAnnotation, event: React.PointerEvent) => void;
onContextMenu?: (annotation: DisplayedAnnotation, event: React.MouseEvent) => void;
onDragStart?: (annotation: DisplayedAnnotation, dataTransfer: DataTransfer) => void;
disablePointerEvents: boolean;
};
Expand All @@ -408,7 +419,7 @@ type NotePreviewProps = {
};

const StaggeredNotes: React.FC<StaggeredNotesProps> = (props) => {
let { annotations, selectedAnnotationIDs, onPointerDown, onPointerUp, onDragStart, pointerEventsSuppressed } = props;
let { annotations, selectedAnnotationIDs, onPointerDown, onPointerUp, onContextMenu, onDragStart, pointerEventsSuppressed } = props;
let staggerMap = new Map<string | undefined, number>();
return <>
{annotations.map((annotation) => {
Expand All @@ -423,6 +434,7 @@ const StaggeredNotes: React.FC<StaggeredNotesProps> = (props) => {
selected={selectedAnnotationIDs.includes(annotation.id)}
onPointerDown={onPointerDown}
onPointerUp={onPointerUp}
onContextMenu={onContextMenu}
onDragStart={onDragStart}
disablePointerEvents={pointerEventsSuppressed}
/>
Expand All @@ -448,6 +460,7 @@ type StaggeredNotesProps = {
selectedAnnotationIDs: string[];
onPointerDown: (annotation: DisplayedAnnotation, event: React.PointerEvent) => void;
onPointerUp: (annotation: DisplayedAnnotation, event: React.PointerEvent) => void;
onContextMenu: (annotation: DisplayedAnnotation, event: React.MouseEvent) => void;
onDragStart: (annotation: DisplayedAnnotation, dataTransfer: DataTransfer) => void;
pointerEventsSuppressed: boolean;
};
Expand Down Expand Up @@ -696,6 +709,7 @@ let CommentIcon = React.forwardRef<SVGSVGElement, CommentIconProps>((props, ref)
draggable={true}
onPointerDown={props.onPointerDown}
onPointerUp={props.onPointerUp}
onContextMenu={props.onContextMenu}
onDragStart={props.onDragStart}
onDragEnd={props.onDragEnd}
data-annotation-id={props.annotation?.id}
Expand All @@ -716,6 +730,7 @@ type CommentIconProps = {
large?: boolean;
onPointerDown?: (event: React.PointerEvent) => void;
onPointerUp?: (event: React.PointerEvent) => void;
onContextMenu?: (event: React.MouseEvent) => void;
onDragStart?: (event: React.DragEvent) => void;
onDragEnd?: (event: React.DragEvent) => void;
};
63 changes: 39 additions & 24 deletions src/dom/common/dom-view.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,10 @@ import { isElement } from "./lib/nodes";
import { debounce } from "../../common/lib/debounce";
// @ts-ignore
import annotationOverlayCSS from '!!raw-loader!./stylesheets/annotation-overlay.css';
import {
getBoundingRect,
rectContains
} from "./lib/rect";

abstract class DOMView<State extends DOMViewState, Data> {
initializedPromise: Promise<void>;
Expand Down Expand Up @@ -341,6 +345,7 @@ abstract class DOMView<State extends DOMViewState, Data> {
selectedAnnotationIDs={this._selectedAnnotationIDs}
onPointerDown={this._handleAnnotationPointerDown}
onPointerUp={this._handleAnnotationPointerUp}
onContextMenu={this._handleAnnotationContextMenu}
onDragStart={this._handleAnnotationDragStart}
onResizeStart={this._handleAnnotationResizeStart}
onResizeEnd={this._handleAnnotationResizeEnd}
Expand Down Expand Up @@ -689,12 +694,42 @@ abstract class DOMView<State extends DOMViewState, Data> {
}
// Prevent native context menu
event.preventDefault();
if (!(event.target as Element).closest('#annotation-overlay')) {
let br = this._iframe.getBoundingClientRect();
this._options.onOpenViewContextMenu({ x: br.x + event.clientX, y: br.y + event.clientY });
}
let br = this._iframe.getBoundingClientRect();
this._options.onOpenViewContextMenu({ x: br.x + event.clientX, y: br.y + event.clientY });
}

private _handleAnnotationContextMenu = (id: string, event: React.MouseEvent) => {
let selection = this._iframeDocument.getSelection();
let selectionBoundingRect = selection && getBoundingRect(
getSelectionRanges(selection).map(range => range.getBoundingClientRect())
);
if (selectionBoundingRect && rectContains(selectionBoundingRect, event.clientX, event.clientY)) {
return;
}

event.preventDefault();
event.stopPropagation();

let br = this._iframe.getBoundingClientRect();
if (this._selectedAnnotationIDs.includes(id)) {
this._options.onOpenAnnotationContextMenu({
ids: this._selectedAnnotationIDs,
x: br.x + event.clientX,
y: br.y + event.clientY,
view: true,
});
}
else {
this._options.onSelectAnnotations([id], event.nativeEvent);
this._options.onOpenAnnotationContextMenu({
ids: [id],
x: br.x + event.clientX,
y: br.y + event.clientY,
view: true,
});
}
};

private _handleSelectionChange() {
let selection = this._iframeDocument.getSelection();
if (!selection || selection.isCollapsed) {
Expand Down Expand Up @@ -737,26 +772,6 @@ abstract class DOMView<State extends DOMViewState, Data> {
}
}
}
else if (event.button == 2) {
let br = this._iframe.getBoundingClientRect();
if (this._selectedAnnotationIDs.includes(id)) {
this._options.onOpenAnnotationContextMenu({
ids: this._selectedAnnotationIDs,
x: br.x + event.clientX,
y: br.y + event.clientY,
view: true,
});
}
else {
this._options.onSelectAnnotations([id], event.nativeEvent);
this._options.onOpenAnnotationContextMenu({
ids: [id],
x: br.x + event.clientX,
y: br.y + event.clientY,
view: true,
});
}
}
this._handledPointerIDs.add(event.pointerId);
};

Expand Down
4 changes: 4 additions & 0 deletions src/dom/common/lib/rect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,7 @@ export function getBoundingRect(rects: DOMRect[]) {
bottom - top
);
}

export function rectContains(rect: DOMRect, x: number, y: number) {
return x >= rect.left && x <= rect.right && y >= rect.top && y <= rect.bottom;
}

0 comments on commit 7098f78

Please sign in to comment.