Skip to content

Commit

Permalink
feat(region): Add region manager logic and renderers
Browse files Browse the repository at this point in the history
  • Loading branch information
jstoffan committed Mar 31, 2020
1 parent c1dbb06 commit c2ac662
Show file tree
Hide file tree
Showing 27 changed files with 670 additions and 43 deletions.
2 changes: 1 addition & 1 deletion .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ module.exports = {
rules: {
camelcase: 0, // fixme
'class-methods-use-this': 0, // fixme
'import/no-extraneous-dependencies': ['error', { devDependencies: ['scripts/**/*.js', '**/*-test.js'] }],
'import/no-extraneous-dependencies': ['error', { devDependencies: ['scripts/**/*.js', '**/*-test.[j|t]s*'] }],
'import/no-unresolved': 'off', // Allows JS files to import TS files
'prefer-destructuring': ['error', { object: true, array: false }],
'react/default-props-match-prop-types': 0,
Expand Down
2 changes: 1 addition & 1 deletion flow-typed/box-annotations.js
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,7 @@ export type IntlOptions = {
messages?: Object,
language?: string,
locale?: string,
}
};

type StringAnyMap = { [string]: any };
type AnnotationMap = { [string]: AnnotationData };
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@
"@commitlint/cli": "^8.2.0",
"@commitlint/config-conventional": "^8.2.0",
"@types/classnames": "^2.2.10",
"@types/enzyme": "^3.10.5",
"@types/jest": "^25.1.4",
"@types/lodash": "^4.14.149",
"@types/react": "^16.9.25",
Expand Down
11 changes: 7 additions & 4 deletions src/@types/global.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
/* eslint-disable @typescript-eslint/no-explicit-any, @typescript-eslint/no-namespace */
declare namespace Intl {
const RelativeTimeFormat: {};
}

declare namespace NodeJS {
interface Global {
BoxAnnotations: any;
}
}

declare namespace Intl {
const RelativeTimeFormat: {};
}

declare module 'box-annotations-locale-data';
declare module 'box-elements-messages';
declare module 'box-ui-elements/es/components/button';
declare module 'box-ui-elements/es/components/primary-button';
declare module 'box-ui-elements/es/constants';
3 changes: 2 additions & 1 deletion src/@types/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import './global';

export * from './i18n';
export * from './api';
export * from './i18n';
export * from './model';
82 changes: 82 additions & 0 deletions src/@types/model.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
/* eslint-disable flowtype/no-types-missing-file-annotation, no-use-before-define */

// New Data Model Types
export interface Annotation {
createdAt: Date;
createdBy: User;
description?: Reply;
id: string;
modifiedAt: Date;
modifiedBy: User;
permissions: Permissions;
replies?: Array<Reply>;
target: Target;
type: 'annotation';
}

export interface User {
id: string;
login: string;
name: string;
profileImage: string;
type: 'user';
}

export interface Rect {
fill?: string;
height: number;
stroke?: Stroke;
width: number;
x: number;
y: number;
}

export interface Reply {
createdAt: Date;
createdBy: User;
id: string;
message: string;
parentId: string;
type: 'reply';
}

export interface Stroke {
color: string;
size: number;
}

export type Target = TargetDrawing | TargetHighlight | TargetPoint | TargetRegion;

export interface TargetDrawing {
id: string;
page: number;
paths: [
{
points: [number, number];
},
];
stroke: Stroke;
type: 'drawing';
}

export interface TargetHighlight {
id: string;
page: number;
rects: Array<Rect>;
text: string;
type: 'highlight';
}

export interface TargetPoint {
id: string;
page: number;
type: 'point';
x: number;
y: number;
}

export interface TargetRegion {
page: number;
shape: Rect;
type: 'region';
}
17 changes: 17 additions & 0 deletions src/common/BaseAnnotator.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
@font-face {
font-weight: normal;
font-family: 'Lato';
font-style: normal;
src: local('Lato Regular'), local('Lato-Regular'), url('https://cdn01.boxcdn.net/fonts/1.0.2/lato/Lato-Regular.woff2') format('woff2'), url('https://cdn01.boxcdn.net/fonts/1.0.2/lato/Lato-Regular.woff') format('woff');
}

@font-face {
font-weight: bold;
font-family: 'Lato';
font-style: normal;
src: local('Lato Bold'), local('Lato-Bold'), url('https://cdn01.boxcdn.net/fonts/1.0.2/lato/Lato-Bold.woff2') format('woff2'), url('https://cdn01.boxcdn.net/fonts/1.0.2/lato/Lato-Bold.woff') format('woff');
}

.ba {
@import '~box-ui-elements/es/styles/base';
}
14 changes: 9 additions & 5 deletions src/common/BaseAnnotator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import i18n from '../utils/i18n';
import messages from '../messages';
import { ANNOTATOR_EVENT } from '../constants';
import { IntlOptions, Permissions } from '../@types';
import './BaseAnnotator.scss';

export type Container = string | HTMLElement;

Expand All @@ -27,11 +28,11 @@ export type Options = {
export default class BaseAnnotator extends EventEmitter {
api: FileVersionAPI;

intl: IntlShape;

container: Container;

rootEl?: HTMLElement;
intl: IntlShape;

rootEl?: HTMLElement | null;

scale = 1;

Expand All @@ -56,8 +57,8 @@ export default class BaseAnnotator extends EventEmitter {
this.removeListener(ANNOTATOR_EVENT.scale, this.handleScale);
}

getElement(selector: HTMLElement | string): HTMLElement {
return (typeof selector === 'string' ? document.querySelector(selector) : selector) as HTMLElement;
getElement(selector: HTMLElement | string): HTMLElement | null {
return typeof selector === 'string' ? document.querySelector(selector) : selector;
}

handleScale = ({ scale }: { scale: number }): void => {
Expand All @@ -71,7 +72,10 @@ export default class BaseAnnotator extends EventEmitter {

if (!this.rootEl) {
this.emit(ANNOTATOR_EVENT.error, this.intl.formatMessage(messages.annotationsLoadError));
return;
}

this.rootEl.classList.add('ba');
}

scrollToAnnotation(): void {
Expand Down
1 change: 1 addition & 0 deletions src/common/BaseManager.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
export type Options = {
page: string;
pageEl: HTMLElement;
referenceEl: HTMLElement;
};

export type Props = {
Expand Down
13 changes: 7 additions & 6 deletions src/common/__tests__/BaseAnnotator-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,15 +37,15 @@ describe('BaseAnnotator', () => {
});

describe('destroy()', () => {
it('should destroy the api instance', () => {
test('should destroy the api instance', () => {
annotator.destroy();

expect(annotator.api.destroy).toBeCalled();
});
});

describe('handleScale', () => {
it('should call init with the new scale', () => {
test('should call init with the new scale', () => {
annotator.init = jest.fn();
annotator.handleScale({ scale: 5 });

Expand All @@ -54,13 +54,14 @@ describe('BaseAnnotator', () => {
});

describe('init()', () => {
it('should set the root element based on class selector', () => {
test('should set the root element based on class selector', () => {
annotator.init(5);

expect(annotator.rootEl).toBe(defaults.container);
expect(annotator.rootEl && annotator.rootEl.classList).toContain('ba');
});

it('should emit error if no root element exists', () => {
test('should emit error if no root element exists', () => {
annotator = getAnnotator({ container: 'non-existent' });
annotator.emit = jest.fn();
annotator.init(5);
Expand All @@ -71,13 +72,13 @@ describe('BaseAnnotator', () => {
});

describe('scrollToAnnotation()', () => {
it('should exist', () => {
test('should exist', () => {
expect(annotator.scrollToAnnotation).toBeTruthy();
});
});

describe('toggleAnnotationMode()', () => {
it('should exist', () => {
test('should exist', () => {
expect(annotator.toggleAnnotationMode).toBeTruthy();
});
});
Expand Down
52 changes: 52 additions & 0 deletions src/components/AnnotationTarget/AnnotationTarget.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import * as React from 'react';
import noop from 'lodash/noop';
import { KEYS } from 'box-ui-elements/es/constants';

type Props = {
annotationId: string;
children: React.ReactNode;
className?: string;
onSelect?: (annotationId: string) => void;
};

const AnnotationTarget = (props: Props, ref: React.Ref<HTMLAnchorElement>): JSX.Element => {
const { annotationId, children, className, onSelect = noop, ...rest } = props;
const cancelEvent = (event: React.SyntheticEvent): void => {
event.preventDefault();
event.stopPropagation();
event.nativeEvent.stopImmediatePropagation(); // Prevents document event handlers from executing
};
const handleClick = (event: React.MouseEvent): void => {
cancelEvent(event);
onSelect(annotationId);
};
const handleFocus = (event: React.SyntheticEvent): void => {
cancelEvent(event);
onSelect(annotationId);
};
const handleKeyPress = (event: React.KeyboardEvent): void => {
if (event.key !== KEYS.enter) {
return;
}

cancelEvent(event);
onSelect(annotationId);
};

return (
<a
ref={ref}
className={className}
onClick={handleClick}
onFocus={handleFocus}
onKeyPress={handleKeyPress}
role="button"
tabIndex={0}
{...rest}
>
{children}
</a>
);
};

export default React.forwardRef(AnnotationTarget);
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import React from 'react';
import { KEYS } from 'box-ui-elements/es/constants';
import { shallow, ShallowWrapper } from 'enzyme';
import AnnotationTarget from '../AnnotationTarget';

describe('AnnotationTarget', () => {
const defaults = {
annotationId: '1',
onSelect: jest.fn(),
};
const getWrapper = (props = {}): ShallowWrapper => {
return shallow(
<AnnotationTarget {...defaults} {...props}>
Test
</AnnotationTarget>,
);
};

describe('keyboard event handlers', () => {
test.each`
key | callCount
${KEYS.enter} | ${1}
${KEYS.escape} | ${0}
${KEYS.space} | ${0}
`('should handle the $key keypress event', ({ callCount, key }) => {
const mockEvent = {
key,
nativeEvent: {
stopImmediatePropagation: jest.fn(),
},
preventDefault: jest.fn(),
stopPropagation: jest.fn(),
};
const onSelect = jest.fn();
const wrapper = getWrapper({ onSelect });

wrapper.simulate('keyPress', mockEvent);

expect(mockEvent.nativeEvent.stopImmediatePropagation).toHaveBeenCalledTimes(callCount);
expect(mockEvent.preventDefault).toHaveBeenCalledTimes(callCount);
expect(mockEvent.stopPropagation).toHaveBeenCalledTimes(callCount);
expect(onSelect).toHaveBeenCalledTimes(callCount);
});
});

describe('mouse event handlers', () => {
test.each(['click', 'focus'])('should cancel the %s event and trigger onSelect', event => {
const mockEvent = {
nativeEvent: {
stopImmediatePropagation: jest.fn(),
},
preventDefault: jest.fn(),
stopPropagation: jest.fn(),
};
const onSelect = jest.fn();
const wrapper = getWrapper({ onSelect });

wrapper.simulate(event, mockEvent);

expect(mockEvent.nativeEvent.stopImmediatePropagation).toHaveBeenCalled();
expect(mockEvent.preventDefault).toHaveBeenCalled();
expect(mockEvent.stopPropagation).toHaveBeenCalled();
expect(onSelect).toHaveBeenCalledWith('1');
});
});

describe('render()', () => {
test('should pass the requiredprops to the underlying anchor', () => {
const wrapper = getWrapper({ className: 'ba-Test' });

expect(wrapper.props()).toMatchObject({
className: 'ba-Test',
role: 'button',
tabIndex: 0,
});
});
});
});
1 change: 1 addition & 0 deletions src/components/AnnotationTarget/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default } from './AnnotationTarget';
6 changes: 6 additions & 0 deletions src/document/DocumentAnnotator.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
.bp-doc .ba-Layer {
& > div,
& > svg {
margin: 15px 0; // Padding on each page provided by Preview SDK
}
}
Loading

0 comments on commit c2ac662

Please sign in to comment.