Skip to content

Commit 55a28b4

Browse files
author
Mingze
authored
feat(drawing): Add drawing components (#631)
* feat(drawing): Add drawing components * feat(drawing): Address comments * feat(drawing): Address comments
1 parent 7c866cc commit 55a28b4

20 files changed

+740
-14
lines changed

src/@types/model.ts

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -31,11 +31,24 @@ export interface AnnotationRegion extends Annotation {
3131
target: TargetRegion;
3232
}
3333

34+
export type Dimensions = {
35+
height: number;
36+
width: number;
37+
};
38+
3439
export interface Page {
3540
type: 'page';
3641
value: number;
3742
}
3843

44+
export interface Path {
45+
points: Array<Position>;
46+
}
47+
export interface PathGroup {
48+
paths: Array<Path>;
49+
stroke: Stroke;
50+
}
51+
3952
export interface Position {
4053
x: number;
4154
y: number;
@@ -79,12 +92,7 @@ export type Target = TargetDrawing | TargetHighlight | TargetPoint | TargetRegio
7992

8093
export interface TargetDrawing {
8194
location: Page;
82-
paths: [
83-
{
84-
points: [Position];
85-
},
86-
];
87-
stroke: Stroke;
95+
path_groups: Array<PathGroup>;
8896
type: 'drawing';
8997
}
9098

src/document/DocumentAnnotator.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import BaseAnnotator, { ANNOTATION_CLASSES, Options } from '../common/BaseAnnota
22
import PopupManager from '../popup/PopupManager';
33
import { centerHighlight, isHighlight } from '../highlight/highlightUtil';
44
import { centerRegion, isRegion, RegionCreationManager, RegionManager } from '../region';
5+
import { DrawingManager } from '../drawing';
56
import { Event } from '../@types';
67
import { getAnnotation } from '../store/annotations';
78
import { getSelection } from './docUtil';
@@ -66,6 +67,10 @@ export default class DocumentAnnotator extends BaseAnnotator {
6667
if (managers.size === 0) {
6768
managers.add(new PopupManager({ location: pageNumber, referenceEl: pageReferenceEl }));
6869

70+
if (this.isFeatureEnabled('drawing')) {
71+
managers.add(new DrawingManager({ location: pageNumber, referenceEl: pageReferenceEl }));
72+
}
73+
6974
if (this.isFeatureEnabled('highlightText')) {
7075
const textLayer = pageEl.querySelector('.textLayer') as HTMLElement;
7176

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
.ba-DrawingAnnotations-list {
2+
position: absolute;
3+
top: 0;
4+
left: 0;
5+
width: 100%;
6+
height: 100%;
7+
pointer-events: none;
8+
9+
&.is-listening {
10+
.ba-DrawingTarget {
11+
pointer-events: auto; // Delegate event control to avoid re-rendering every target on mousedown/up
12+
}
13+
}
14+
}

src/drawing/DrawingAnnotations.tsx

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import * as React from 'react';
2+
import DrawingList from './DrawingList';
3+
import { AnnotationDrawing } from '../@types';
4+
import './DrawingAnnotations.scss';
5+
6+
export type Props = {
7+
activeAnnotationId: string | null;
8+
annotations: AnnotationDrawing[];
9+
isCurrentFileVersion: boolean;
10+
setActiveAnnotationId: (annotationId: string | null) => void;
11+
};
12+
13+
const DrawingAnnotations = (props: Props): JSX.Element => {
14+
const { activeAnnotationId, annotations, isCurrentFileVersion, setActiveAnnotationId } = props;
15+
16+
const handleAnnotationActive = (annotationId: string | null): void => {
17+
setActiveAnnotationId(annotationId);
18+
};
19+
20+
return (
21+
<DrawingList
22+
activeId={activeAnnotationId}
23+
annotations={annotations}
24+
className="ba-DrawingAnnotations-list"
25+
data-resin-iscurrent={isCurrentFileVersion}
26+
onSelect={handleAnnotationActive}
27+
/>
28+
);
29+
};
30+
31+
export default DrawingAnnotations;
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import { connect } from 'react-redux';
2+
import DrawingAnnotations from './DrawingAnnotations';
3+
import withProviders from '../common/withProviders';
4+
import { AnnotationDrawing } from '../@types';
5+
import {
6+
AppState,
7+
getActiveAnnotationId,
8+
getAnnotationsForLocation,
9+
getIsCurrentFileVersion,
10+
setActiveAnnotationIdAction,
11+
} from '../store';
12+
import { isDrawing } from './drawingUtil';
13+
14+
export type Props = {
15+
activeAnnotationId: string | null;
16+
annotations: AnnotationDrawing[];
17+
isCurrentFileVersion: boolean;
18+
};
19+
20+
export const mapStateToProps = (state: AppState, { location }: { location: number }): Props => {
21+
return {
22+
activeAnnotationId: getActiveAnnotationId(state),
23+
annotations: getAnnotationsForLocation(state, location).filter(isDrawing),
24+
isCurrentFileVersion: getIsCurrentFileVersion(state),
25+
};
26+
};
27+
28+
export const mapDispatchToProps = {
29+
setActiveAnnotationId: setActiveAnnotationIdAction,
30+
};
31+
32+
export default connect(mapStateToProps, mapDispatchToProps)(withProviders(DrawingAnnotations));

src/drawing/DrawingList.tsx

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import React from 'react';
2+
import classNames from 'classnames';
3+
import noop from 'lodash/noop';
4+
import DrawingTarget from './DrawingTarget';
5+
import useOutsideEvent from '../common/useOutsideEvent';
6+
import { AnnotationDrawing } from '../@types';
7+
import { checkValue } from '../utils/util';
8+
import { getShape } from './drawingUtil';
9+
10+
export type Props = {
11+
activeId?: string | null;
12+
annotations: AnnotationDrawing[];
13+
className?: string;
14+
onSelect?: (annotationId: string | null) => void;
15+
};
16+
17+
export function filterDrawing({ target: { path_groups: pathGroups } }: AnnotationDrawing): boolean {
18+
return pathGroups.every(({ paths }) =>
19+
paths.every(({ points }) => points.every(({ x, y }) => checkValue(x) && checkValue(y))),
20+
);
21+
}
22+
23+
export function sortDrawing({ target: targetA }: AnnotationDrawing, { target: targetB }: AnnotationDrawing): number {
24+
const { height: heightA, width: widthA } = getShape(targetA.path_groups);
25+
const { height: heightB, width: widthB } = getShape(targetB.path_groups);
26+
27+
// If B is smaller, the result is negative.
28+
// So, A is sorted to an index lower than B, which means A will be rendered first at bottom
29+
return heightB * widthB - heightA * widthA;
30+
}
31+
32+
export function DrawingList({ activeId = null, annotations, className, onSelect = noop }: Props): JSX.Element {
33+
const [isListening, setIsListening] = React.useState(true);
34+
const rootElRef = React.createRef<SVGSVGElement>();
35+
36+
// Document-level event handlers for focus and pointer control
37+
useOutsideEvent('mousedown', rootElRef, (): void => {
38+
onSelect(null);
39+
setIsListening(false);
40+
});
41+
useOutsideEvent('mouseup', rootElRef, (): void => setIsListening(true));
42+
43+
return (
44+
<svg
45+
ref={rootElRef}
46+
className={classNames(className, { 'is-listening': isListening })}
47+
data-resin-component="drawingList"
48+
preserveAspectRatio="none"
49+
viewBox="0 0 100 100"
50+
>
51+
{annotations
52+
.filter(filterDrawing)
53+
.sort(sortDrawing)
54+
.map(({ id, target }) => (
55+
<DrawingTarget
56+
key={id}
57+
annotationId={id}
58+
isActive={activeId === id}
59+
onSelect={onSelect}
60+
target={target}
61+
/>
62+
))}
63+
</svg>
64+
);
65+
}
66+
67+
export default React.memo(DrawingList);

src/drawing/DrawingManager.tsx

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import * as React from 'react';
2+
import * as ReactDOM from 'react-dom';
3+
import BaseManager, { Props } from '../common/BaseManager';
4+
import DrawingAnnotationsContainer from './DrawingAnnotationsContainer';
5+
6+
export default class DrawingListManager extends BaseManager {
7+
decorate(): void {
8+
this.reactEl.classList.add('ba-Layer--drawing');
9+
this.reactEl.dataset.testid = 'ba-Layer--drawing';
10+
}
11+
12+
render(props: Props): void {
13+
ReactDOM.render(<DrawingAnnotationsContainer location={this.location} {...props} />, this.reactEl);
14+
}
15+
}

src/drawing/DrawingTarget.scss

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
.ba-DrawingTarget {
2+
outline: none;
3+
4+
&:hover {
5+
cursor: pointer;
6+
}
7+
}

src/drawing/DrawingTarget.tsx

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
import * as React from 'react';
2+
import classNames from 'classnames';
3+
import noop from 'lodash/noop';
4+
import { MOUSE_PRIMARY } from '../constants';
5+
import { TargetDrawing } from '../@types';
6+
import { getCenter, getShape } from './drawingUtil';
7+
import './DrawingTarget.scss';
8+
9+
export type Props = {
10+
annotationId: string;
11+
className?: string;
12+
isActive?: boolean;
13+
onSelect?: (annotationId: string) => void;
14+
target: TargetDrawing;
15+
};
16+
17+
export type DrawingTargetRef = HTMLAnchorElement;
18+
19+
export const DrawingTarget = (props: Props, ref: React.Ref<DrawingTargetRef>): JSX.Element => {
20+
const {
21+
annotationId,
22+
className,
23+
isActive = false,
24+
onSelect = noop,
25+
target: { path_groups: pathGroups },
26+
} = props;
27+
const shape = getShape(pathGroups);
28+
const { x: centerX, y: centerY } = getCenter(shape);
29+
30+
const handleFocus = (): void => {
31+
onSelect(annotationId);
32+
};
33+
const handleMouseDown = (event: React.MouseEvent<DrawingTargetRef>): void => {
34+
if (event.buttons !== MOUSE_PRIMARY) {
35+
return;
36+
}
37+
const activeElement = document.activeElement as HTMLElement;
38+
39+
event.preventDefault(); // Prevents focus from leaving the button immediately in some browsers
40+
event.nativeEvent.stopImmediatePropagation(); // Prevents document event handlers from executing
41+
42+
// IE11 won't apply the focus to the SVG anchor, so this workaround attempts to blur the existing
43+
// active element.
44+
if (activeElement && activeElement !== event.currentTarget && activeElement.blur) {
45+
activeElement.blur();
46+
}
47+
48+
event.currentTarget.focus(); // Buttons do not receive focus in Firefox and Safari on MacOS; triggers handleFocus
49+
50+
onSelect(annotationId);
51+
};
52+
53+
return (
54+
// eslint-disable-next-line jsx-a11y/anchor-is-valid
55+
<a
56+
ref={ref}
57+
className={classNames('ba-DrawingTarget', className, { 'is-active': isActive })}
58+
data-resin-itemid={annotationId}
59+
data-resin-target="highlightDrawing"
60+
data-testid={`ba-AnnotationTarget-${annotationId}`}
61+
href="#"
62+
onFocus={handleFocus}
63+
onMouseDown={handleMouseDown}
64+
role="button"
65+
tabIndex={0}
66+
>
67+
<rect
68+
fill="transparent"
69+
transform={`translate(-${centerX * 0.1}, -${centerY * 0.1}) scale(1.1)`}
70+
{...shape}
71+
/>
72+
</a>
73+
);
74+
};
75+
76+
export default React.forwardRef(DrawingTarget);
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
export const annotations = [
2+
{
3+
id: 'anno_1',
4+
target: {
5+
location: { type: 'page' as const, value: 1 },
6+
path_groups: [
7+
{
8+
paths: [
9+
{
10+
points: [
11+
{ x: 10, y: 10 },
12+
{ x: 11, y: 11 },
13+
{ x: 12, y: 12 },
14+
],
15+
},
16+
],
17+
stroke: {
18+
color: 'red',
19+
size: 1,
20+
},
21+
},
22+
{
23+
paths: [
24+
{
25+
points: [
26+
{ x: 20, y: 20 },
27+
{ x: 21, y: 21 },
28+
{ x: 22, y: 22 },
29+
],
30+
},
31+
],
32+
stroke: {
33+
color: 'black',
34+
size: 4,
35+
},
36+
},
37+
],
38+
type: 'drawing' as const,
39+
},
40+
},
41+
{
42+
id: 'anno_2',
43+
target: {
44+
location: { type: 'page' as const, value: 2 },
45+
path_groups: [
46+
{
47+
paths: [
48+
{
49+
points: [
50+
{ x: 20, y: 20 },
51+
{ x: 21, y: 21 },
52+
{ x: 22, y: 22 },
53+
],
54+
},
55+
],
56+
stroke: {
57+
color: 'blue',
58+
size: 1,
59+
},
60+
},
61+
{
62+
paths: [
63+
{
64+
points: [
65+
{ x: 40, y: 40 },
66+
{ x: 41, y: 41 },
67+
{ x: 42, y: 42 },
68+
],
69+
},
70+
],
71+
stroke: {
72+
color: 'green',
73+
size: 4,
74+
},
75+
},
76+
],
77+
type: 'drawing' as const,
78+
},
79+
},
80+
];

0 commit comments

Comments
 (0)