Skip to content

Commit e1081b0

Browse files
Conrad Chanmergify[bot]
andauthored
feat(highlights): Scroll to highlight annotation (#562)
* feat(highlights): Scroll to highlight annotation * chore: pr comments * chore: top level Shape type Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
1 parent 9f79428 commit e1081b0

File tree

10 files changed

+130
-19
lines changed

10 files changed

+130
-19
lines changed

src/@types/model.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,8 @@ export interface Position {
4141
y: number;
4242
}
4343

44+
export type Shape = Required<DOMRectInit>;
45+
4446
export interface User {
4547
id: string;
4648
login: string;

src/document/DocumentAnnotator.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import BaseAnnotator, { Options } from '../common/BaseAnnotator';
22
import BaseManager from '../common/BaseManager';
3+
import { centerHighlight, isHighlight } from '../highlight/highlightUtil';
34
import { centerRegion, isRegion, RegionManager } from '../region';
45
import { Event } from '../@types';
56
import { getAnnotation } from '../store/annotations';
@@ -145,6 +146,10 @@ export default class DocumentAnnotator extends BaseAnnotator {
145146
scrollToLocation(this.annotatedEl, annotationPageEl, {
146147
offsets: centerRegion(annotation.target.shape),
147148
});
149+
} else if (isHighlight(annotation)) {
150+
scrollToLocation(this.annotatedEl, annotationPageEl, {
151+
offsets: centerHighlight(annotation.target.shapes),
152+
});
148153
}
149154
}
150155
}

src/document/__tests__/DocumentAnnotator-test.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import DocumentAnnotator from '../DocumentAnnotator';
33
import HighlightListener from '../../highlight/HighlightListener';
44
import RegionManager from '../../region/RegionManager';
55
import { Annotation, Event } from '../../@types';
6+
import { annotation as highlight } from '../../highlight/__mocks__/data';
67
import { annotations as regions } from '../../region/__mocks__/data';
78
import { fetchAnnotationsAction } from '../../store';
89
import { HighlightManager } from '../../highlight';
@@ -39,7 +40,7 @@ describe('DocumentAnnotator', () => {
3940
};
4041

4142
const payload = {
42-
entries: regions as Annotation[],
43+
entries: [...regions, highlight] as Annotation[],
4344
limit: 1000,
4445
next_marker: null,
4546
previous_marker: null,
@@ -263,5 +264,13 @@ describe('DocumentAnnotator', () => {
263264
annotator.scrollToAnnotation(null);
264265
expect(scrollToLocation).not.toHaveBeenCalled();
265266
});
267+
268+
test('should call scrollToLocation for highlight annotations', () => {
269+
const parentEl = annotator.annotatedEl as HTMLElement;
270+
const referenceEl = parentEl.querySelector('[data-page-number="1"]');
271+
272+
annotator.scrollToAnnotation('223');
273+
expect(scrollToLocation).toHaveBeenCalledWith(parentEl, referenceEl, { offsets: { x: 15, y: 10 } });
274+
});
266275
});
267276
});
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import { centerHighlight, getBoundingRect } from '../highlightUtil';
2+
import { Shape } from '../../@types';
3+
4+
const shape1: Shape = {
5+
height: 10,
6+
width: 10,
7+
x: 0,
8+
y: 0,
9+
};
10+
11+
const shape2: Shape = {
12+
height: 10,
13+
width: 20,
14+
x: 10,
15+
y: 10,
16+
};
17+
18+
describe('highlightUtil', () => {
19+
describe('getBoundingRect()', () => {
20+
test('should be the same rect for a single shape', () => {
21+
expect(getBoundingRect([shape1])).toEqual(shape1);
22+
});
23+
24+
test('should get the bounding rect for multiple shapes', () => {
25+
expect(getBoundingRect([shape1, shape2])).toEqual({
26+
height: 20,
27+
width: 30,
28+
x: 0,
29+
y: 0,
30+
});
31+
});
32+
});
33+
34+
describe('centerHighlight()', () => {
35+
test.each`
36+
shapes | expectedCenter
37+
${[shape1]} | ${{ x: 5, y: 5 }}
38+
${[shape2]} | ${{ x: 20, y: 15 }}
39+
${[shape1, shape2]} | ${{ x: 15, y: 10 }}
40+
`('should get the center of the highlight to be $expectedCenter', ({ shapes, expectedCenter }) => {
41+
expect(centerHighlight(shapes)).toEqual(expectedCenter);
42+
});
43+
});
44+
});

src/highlight/highlightUtil.ts

Lines changed: 56 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,59 @@
1-
import { Annotation, AnnotationHighlight, Type } from '../@types';
1+
import { Annotation, AnnotationHighlight, Position, Shape, Type } from '../@types';
2+
3+
export const getBoundingRect = (shapes: Shape[]): Shape => {
4+
let minX = Number.MAX_VALUE;
5+
let minY = Number.MAX_VALUE;
6+
let maxX = Number.MIN_VALUE;
7+
let maxY = Number.MIN_VALUE;
8+
9+
shapes.forEach(({ height, width, x, y }) => {
10+
const x2 = x + width;
11+
const y2 = y + height;
12+
13+
if (x < minX) {
14+
minX = x;
15+
}
16+
17+
if (y < minY) {
18+
minY = y;
19+
}
20+
21+
if (x2 > maxX) {
22+
maxX = x2;
23+
}
24+
25+
if (y2 > maxY) {
26+
maxY = y2;
27+
}
28+
});
29+
30+
return {
31+
x: minX,
32+
y: minY,
33+
width: maxX - minX,
34+
height: maxY - minY,
35+
};
36+
};
37+
38+
const centerShape = (shape: Shape): Position => {
39+
const { height, width } = shape;
40+
41+
return {
42+
x: width / 2,
43+
y: height / 2,
44+
};
45+
};
46+
47+
export const centerHighlight = (shapes: Shape[]): Position => {
48+
const boundingShape = getBoundingRect(shapes);
49+
const { x: shapeX, y: shapeY } = boundingShape;
50+
const { x: centerX, y: centerY } = centerShape(boundingShape);
51+
52+
return {
53+
x: centerX + shapeX,
54+
y: centerY + shapeY,
55+
};
56+
};
257

358
export function isHighlight(annotation: Annotation): annotation is AnnotationHighlight {
459
return annotation?.target?.type === Type.highlight;

src/region/RegionAnnotation.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@ import classNames from 'classnames';
44
import noop from 'lodash/noop';
55
import { getIsCurrentFileVersion } from '../store';
66
import { MOUSE_PRIMARY } from '../constants';
7-
import { Shape, styleShape } from './regionUtil';
7+
import { Shape } from '../@types';
8+
import { styleShape } from './regionUtil';
89
import './RegionAnnotation.scss';
910

1011
type Props = {

src/region/RegionRect.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import * as React from 'react';
22
import classNames from 'classnames';
3-
import { Shape, styleShape } from './regionUtil';
3+
import { Shape } from '../@types';
4+
import { styleShape } from './regionUtil';
45
import './RegionRect.scss';
56

67
type Props = {

src/region/regionUtil.ts

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import * as React from 'react';
2-
import { Annotation, AnnotationRegion, Position } from '../@types';
2+
import { Annotation, AnnotationRegion, Position, Shape } from '../@types';
33
import { invertYCoordinate, Point, rotatePoint, translatePoint } from './transformUtil';
44

55
// Possible rotation values as supplied by box-content-preview
@@ -15,13 +15,6 @@ export type Dimensions = {
1515
width: number;
1616
};
1717

18-
export type Shape = {
19-
height: number;
20-
width: number;
21-
x: number;
22-
y: number;
23-
};
24-
2518
export type Translation = {
2619
dx?: number;
2720
dy?: number;

src/store/selection/actions.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { createAction } from '@reduxjs/toolkit';
2-
import { DOMRectMini, SelectionItem } from './types';
2+
import { SelectionItem } from './types';
3+
import { Shape } from '../../@types';
34

45
export type SelectionArg = {
56
location: number;
@@ -10,7 +11,7 @@ type Payload = {
1011
payload: SelectionItem | null;
1112
};
1213

13-
export const getDOMRectMini = ({ height, width, x, y }: DOMRect): DOMRectMini => ({
14+
export const getShape = ({ height, width, x, y }: DOMRect): Shape => ({
1415
height,
1516
width,
1617
x,
@@ -30,9 +31,9 @@ export const setSelectionAction = createAction(
3031

3132
return {
3233
payload: {
33-
boundingRect: getDOMRectMini(range.getBoundingClientRect()),
34+
boundingRect: getShape(range.getBoundingClientRect()),
3435
location,
35-
rects: Array.from(range.getClientRects()).map(getDOMRectMini),
36+
rects: Array.from(range.getClientRects()).map(getShape),
3637
},
3738
};
3839
},

src/store/selection/types.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
1-
export type DOMRectMini = Required<DOMRectInit>;
1+
import { Shape } from '../../@types';
22

33
export type SelectionState = {
44
selection: SelectionItem | null;
55
};
66

77
export type SelectionItem = {
8-
boundingRect: DOMRectMini;
8+
boundingRect: Shape;
99
location: number;
10-
rects: Array<DOMRectMini>;
10+
rects: Array<Shape>;
1111
};

0 commit comments

Comments
 (0)