diff --git a/src/@types/model.ts b/src/@types/model.ts index 13ae24a76..d6742c03c 100644 --- a/src/@types/model.ts +++ b/src/@types/model.ts @@ -41,6 +41,8 @@ export interface Position { y: number; } +export type Shape = Required; + export interface User { id: string; login: string; diff --git a/src/document/DocumentAnnotator.ts b/src/document/DocumentAnnotator.ts index dbde2ed9e..4e472077a 100644 --- a/src/document/DocumentAnnotator.ts +++ b/src/document/DocumentAnnotator.ts @@ -1,5 +1,6 @@ import BaseAnnotator, { Options } from '../common/BaseAnnotator'; import BaseManager from '../common/BaseManager'; +import { centerHighlight, isHighlight } from '../highlight/highlightUtil'; import { centerRegion, isRegion, RegionManager } from '../region'; import { Event } from '../@types'; import { getAnnotation } from '../store/annotations'; @@ -145,6 +146,10 @@ export default class DocumentAnnotator extends BaseAnnotator { scrollToLocation(this.annotatedEl, annotationPageEl, { offsets: centerRegion(annotation.target.shape), }); + } else if (isHighlight(annotation)) { + scrollToLocation(this.annotatedEl, annotationPageEl, { + offsets: centerHighlight(annotation.target.shapes), + }); } } } diff --git a/src/document/__tests__/DocumentAnnotator-test.ts b/src/document/__tests__/DocumentAnnotator-test.ts index 56958be5b..2da1bcffa 100644 --- a/src/document/__tests__/DocumentAnnotator-test.ts +++ b/src/document/__tests__/DocumentAnnotator-test.ts @@ -3,6 +3,7 @@ import DocumentAnnotator from '../DocumentAnnotator'; import HighlightListener from '../../highlight/HighlightListener'; import RegionManager from '../../region/RegionManager'; import { Annotation, Event } from '../../@types'; +import { annotation as highlight } from '../../highlight/__mocks__/data'; import { annotations as regions } from '../../region/__mocks__/data'; import { fetchAnnotationsAction } from '../../store'; import { HighlightManager } from '../../highlight'; @@ -39,7 +40,7 @@ describe('DocumentAnnotator', () => { }; const payload = { - entries: regions as Annotation[], + entries: [...regions, highlight] as Annotation[], limit: 1000, next_marker: null, previous_marker: null, @@ -263,5 +264,13 @@ describe('DocumentAnnotator', () => { annotator.scrollToAnnotation(null); expect(scrollToLocation).not.toHaveBeenCalled(); }); + + test('should call scrollToLocation for highlight annotations', () => { + const parentEl = annotator.annotatedEl as HTMLElement; + const referenceEl = parentEl.querySelector('[data-page-number="1"]'); + + annotator.scrollToAnnotation('223'); + expect(scrollToLocation).toHaveBeenCalledWith(parentEl, referenceEl, { offsets: { x: 15, y: 10 } }); + }); }); }); diff --git a/src/highlight/__tests__/highlightUtil-test.ts b/src/highlight/__tests__/highlightUtil-test.ts new file mode 100644 index 000000000..a7aea29ec --- /dev/null +++ b/src/highlight/__tests__/highlightUtil-test.ts @@ -0,0 +1,44 @@ +import { centerHighlight, getBoundingRect } from '../highlightUtil'; +import { Shape } from '../../@types'; + +const shape1: Shape = { + height: 10, + width: 10, + x: 0, + y: 0, +}; + +const shape2: Shape = { + height: 10, + width: 20, + x: 10, + y: 10, +}; + +describe('highlightUtil', () => { + describe('getBoundingRect()', () => { + test('should be the same rect for a single shape', () => { + expect(getBoundingRect([shape1])).toEqual(shape1); + }); + + test('should get the bounding rect for multiple shapes', () => { + expect(getBoundingRect([shape1, shape2])).toEqual({ + height: 20, + width: 30, + x: 0, + y: 0, + }); + }); + }); + + describe('centerHighlight()', () => { + test.each` + shapes | expectedCenter + ${[shape1]} | ${{ x: 5, y: 5 }} + ${[shape2]} | ${{ x: 20, y: 15 }} + ${[shape1, shape2]} | ${{ x: 15, y: 10 }} + `('should get the center of the highlight to be $expectedCenter', ({ shapes, expectedCenter }) => { + expect(centerHighlight(shapes)).toEqual(expectedCenter); + }); + }); +}); diff --git a/src/highlight/highlightUtil.ts b/src/highlight/highlightUtil.ts index 47e4528bb..d66b94bfb 100644 --- a/src/highlight/highlightUtil.ts +++ b/src/highlight/highlightUtil.ts @@ -1,4 +1,59 @@ -import { Annotation, AnnotationHighlight, Type } from '../@types'; +import { Annotation, AnnotationHighlight, Position, Shape, Type } from '../@types'; + +export const getBoundingRect = (shapes: Shape[]): Shape => { + let minX = Number.MAX_VALUE; + let minY = Number.MAX_VALUE; + let maxX = Number.MIN_VALUE; + let maxY = Number.MIN_VALUE; + + shapes.forEach(({ height, width, x, y }) => { + const x2 = x + width; + const y2 = y + height; + + if (x < minX) { + minX = x; + } + + if (y < minY) { + minY = y; + } + + if (x2 > maxX) { + maxX = x2; + } + + if (y2 > maxY) { + maxY = y2; + } + }); + + return { + x: minX, + y: minY, + width: maxX - minX, + height: maxY - minY, + }; +}; + +const centerShape = (shape: Shape): Position => { + const { height, width } = shape; + + return { + x: width / 2, + y: height / 2, + }; +}; + +export const centerHighlight = (shapes: Shape[]): Position => { + const boundingShape = getBoundingRect(shapes); + const { x: shapeX, y: shapeY } = boundingShape; + const { x: centerX, y: centerY } = centerShape(boundingShape); + + return { + x: centerX + shapeX, + y: centerY + shapeY, + }; +}; export function isHighlight(annotation: Annotation): annotation is AnnotationHighlight { return annotation?.target?.type === Type.highlight; diff --git a/src/region/RegionAnnotation.tsx b/src/region/RegionAnnotation.tsx index e059d6f1a..0144fb00b 100644 --- a/src/region/RegionAnnotation.tsx +++ b/src/region/RegionAnnotation.tsx @@ -4,7 +4,8 @@ import classNames from 'classnames'; import noop from 'lodash/noop'; import { getIsCurrentFileVersion } from '../store'; import { MOUSE_PRIMARY } from '../constants'; -import { Shape, styleShape } from './regionUtil'; +import { Shape } from '../@types'; +import { styleShape } from './regionUtil'; import './RegionAnnotation.scss'; type Props = { diff --git a/src/region/RegionRect.tsx b/src/region/RegionRect.tsx index 20de63394..1c1d2dde2 100644 --- a/src/region/RegionRect.tsx +++ b/src/region/RegionRect.tsx @@ -1,6 +1,7 @@ import * as React from 'react'; import classNames from 'classnames'; -import { Shape, styleShape } from './regionUtil'; +import { Shape } from '../@types'; +import { styleShape } from './regionUtil'; import './RegionRect.scss'; type Props = { diff --git a/src/region/regionUtil.ts b/src/region/regionUtil.ts index acec523ad..65243a11e 100644 --- a/src/region/regionUtil.ts +++ b/src/region/regionUtil.ts @@ -1,5 +1,5 @@ import * as React from 'react'; -import { Annotation, AnnotationRegion, Position } from '../@types'; +import { Annotation, AnnotationRegion, Position, Shape } from '../@types'; import { invertYCoordinate, Point, rotatePoint, translatePoint } from './transformUtil'; // Possible rotation values as supplied by box-content-preview @@ -15,13 +15,6 @@ export type Dimensions = { width: number; }; -export type Shape = { - height: number; - width: number; - x: number; - y: number; -}; - export type Translation = { dx?: number; dy?: number; diff --git a/src/store/selection/actions.ts b/src/store/selection/actions.ts index 672a345cb..fe7a41218 100644 --- a/src/store/selection/actions.ts +++ b/src/store/selection/actions.ts @@ -1,5 +1,6 @@ import { createAction } from '@reduxjs/toolkit'; -import { DOMRectMini, SelectionItem } from './types'; +import { SelectionItem } from './types'; +import { Shape } from '../../@types'; export type SelectionArg = { location: number; @@ -10,7 +11,7 @@ type Payload = { payload: SelectionItem | null; }; -export const getDOMRectMini = ({ height, width, x, y }: DOMRect): DOMRectMini => ({ +export const getShape = ({ height, width, x, y }: DOMRect): Shape => ({ height, width, x, @@ -30,9 +31,9 @@ export const setSelectionAction = createAction( return { payload: { - boundingRect: getDOMRectMini(range.getBoundingClientRect()), + boundingRect: getShape(range.getBoundingClientRect()), location, - rects: Array.from(range.getClientRects()).map(getDOMRectMini), + rects: Array.from(range.getClientRects()).map(getShape), }, }; }, diff --git a/src/store/selection/types.ts b/src/store/selection/types.ts index 4250818bf..34fb59df3 100644 --- a/src/store/selection/types.ts +++ b/src/store/selection/types.ts @@ -1,11 +1,11 @@ -export type DOMRectMini = Required; +import { Shape } from '../../@types'; export type SelectionState = { selection: SelectionItem | null; }; export type SelectionItem = { - boundingRect: DOMRectMini; + boundingRect: Shape; location: number; - rects: Array; + rects: Array; };