Skip to content

Commit 9535ee4

Browse files
author
Mingze
authored
feat(highlight): Change cursor in Highlight Text mode (#544)
* feat(highlight): Change cursor in Highlight Text mode * feat(highlight): Add tests * feat(highlight): Address feedbacks
1 parent 24660b8 commit 9535ee4

14 files changed

+264
-1
lines changed

src/document/DocumentAnnotator.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import BaseAnnotator from '../common/BaseAnnotator';
22
import BaseManager from '../common/BaseManager';
33
import { centerRegion, isRegion, RegionManager } from '../region';
44
import { getAnnotation } from '../store/annotations';
5+
import { HighlightManager } from '../highlight';
56
import { scrollToLocation } from '../utils/scroll';
67
import './DocumentAnnotator.scss';
78

@@ -30,6 +31,7 @@ export default class DocumentAnnotator extends BaseAnnotator {
3031
// Lazily instantiate managers as pages are added or re-rendered
3132
if (managers.size === 0) {
3233
managers.add(new RegionManager({ location: pageNumber, referenceEl: pageReferenceEl }));
34+
managers.add(new HighlightManager({ location: pageNumber, referenceEl: pageReferenceEl }));
3335
}
3436

3537
return managers;

src/document/__tests__/DocumentAnnotator-test.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { annotations as regions } from '../../region/__mocks__/data';
66
import { fetchAnnotationsAction } from '../../store';
77
import { scrollToLocation } from '../../utils/scroll';
88

9+
jest.mock('../../highlight/HighlightManager');
910
jest.mock('../../region/RegionManager');
1011
jest.mock('../../utils/scroll');
1112

@@ -69,7 +70,7 @@ describe('DocumentAnnotator', () => {
6970
test('should create new managers given a new page element', () => {
7071
const managers = annotator.getPageManagers(getPage());
7172

72-
expect(managers.size).toBe(1);
73+
expect(managers.size).toBe(2);
7374
expect(managers.values().next().value).toBeInstanceOf(RegionManager);
7475
});
7576

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
.ba-HighlightAnnotations-creator {
2+
position: absolute;
3+
top: 0;
4+
left: 0;
5+
width: 100%;
6+
height: 100%;
7+
pointer-events: auto;
8+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import * as React from 'react';
2+
import HighlightCreator from './HighlightCreator';
3+
4+
import './HighlightAnnotations.scss';
5+
6+
type Props = {
7+
isCreating: boolean;
8+
};
9+
10+
export default class HighlightAnnotations extends React.PureComponent<Props> {
11+
static defaultProps = {
12+
isCreating: false,
13+
};
14+
15+
render(): JSX.Element {
16+
const { isCreating } = this.props;
17+
18+
return <>{isCreating && <HighlightCreator className="ba-HighlightAnnotations-creator" />}</>;
19+
}
20+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import { connect } from 'react-redux';
2+
import { AppState, getAnnotationMode } from '../store';
3+
import HighlightAnnotations from './HighlightAnnotations';
4+
import withProviders from '../common/withProviders';
5+
6+
export type Props = {
7+
isCreating: boolean;
8+
};
9+
10+
export const mapStateToProps = (state: AppState): Props => ({
11+
isCreating: getAnnotationMode(state) === 'highlight',
12+
});
13+
14+
export default connect(mapStateToProps)(withProviders(HighlightAnnotations));
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
$text_cursor_32: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAABGdBTUEAALGPC/xhBQAAADhlWElmTU0AKgAAAAgAAYdpAAQAAAABAAAAGgAAAAAAAqACAAQAAAABAAAAIKADAAQAAAABAAAAIAAAAACPTkDJAAABrklEQVRYCWNgGAWjITAaAqMhQCAEfv36NeX///9/gBgO/v379/f716/TCWglSpqRkKq/f//+tjAxYfn8+TNcKS8vL8OJM2f+MDMzs8IFacUA+RTkY5D3NVRUYKHwBxQytLITq7nIDgBa/g3mEmz0169fP3z58qUfKMeG1TAkQRYkNlHMT58+/Q3w8eF8/vw5TvXiEhL8nd3daTq6uiA1hTgVAiWY8Elik3v86NFnZMvZ2NgY/AMCGOISEhgEBAXBWl6+eMFQUVrKxcjImIjNDGQxkh2ArBnETkpJYdDS1mYwMjb+P3vu3N8w+RdAR3BxcfHD+Lhoih0wY9o0huVLlzIcO3KEUVtHh+QoJVkDuk8mTpnyhZOT89/ZM2f4gEFOMFuj66c4BGxsbVnu3r37WkxMDB786Jbg41McAknx8RzmFhbKRw8fZrh69SrcLgkJCYZv3759hAvgYJDsAFk5OV5JSUkGWE64eOECAwiDAMwBwGzI0NHdDSor5uOwl3RhUIEDKwl//vz5HcTHBUgpiAi6BLkygjlg5FZGSEFO/8qIYFyNKhgNgdEQGKohAAD+bzKe7UZRtgAAAABJRU5ErkJggg==';
2+
$text_cursor_32_2x: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAAABGdBTUEAALGPC/xhBQAAADhlWElmTU0AKgAAAAgAAYdpAAQAAAABAAAAGgAAAAAAAqACAAQAAAABAAAAQKADAAQAAAABAAAAQAAAAABlmWCKAAADvElEQVR4Ae2ZzUsbQRTA37a1BSkVhNjEg400h2ouQiWpaFIpftGmxP4FQqs9RLykiIj0JD3YIgVvFnvrUYppUq0kSk9Wi0URcrNgtGCKFyW9JKHZvrfNVjTJmt3R3bSdBy+zO/ve7rzfvJnMzgJw4QQ4AU6AE+AEOAFOgBPgBDgBToAT4AQ4AU5AJQFRFGtQw6hJ1JOEbMi2RuVjStc8G9BJgR+/Hi6ViATWhmBkSbzHxVuNjXBwcKB4u4qKClheXSWblCAIlxSNdbp4Tqfn/LuPKTQEbthsImkBKZkhcBoZ8Ai7N4KaKqKbyYZsyackhBkAjuVt1HYa06hKc8qXbMRtWMYKZIZSdQIvLqP2oSo9RxVYZgAqnnYTbS+qsD9uehkrnKivUCMIwXLcQMv5BS1Oan3SqZT4xO8XPi4uQjqdVusu2ZtMJnA4nTA4NCReNZvvYOUbhNCGSSdqumHWSZcMCMzMCOH5ec3BU1v39vbgfSgEXo9H+B6PU9AEoZeusYguAOLxOEsbj/jSWuPF2Jg8BzBPproMgSMR5Dmx1taCr78f6urrwVJdDVtbWzA1OQkf5ubyWAN8XlmR6+3ygdZSlwxQapzL7YZAMAj3vV4wVVWBmMmA3W6HlxMT0NHVldeVhkNWaGJkEsMBrK+twbtAAAZ8PqDldEtTE7ydnpaC6u3rYwquGGfDASQSCXg6MgKR8O/FYTKZhLnZWant1222YmJgsimJOYAisGGw7tZWuGa1ghWVpLy8XCrP8sdwAJWVldJ4p//4TCYjxmIxIYVZoJcYPgSej49LC5yN9fWNZqdTuNvRAc9GR/WKHwwH0NDQIAW7EInE9vf3pePmlhbdABg+BKLRqJQBvoGB23X490cTX7XF8hMJUOfIC54zA2J4BowMD8OnpSUK8IrL5YKvm5vwwOs9HwoGhfjubt7Aab2QlR/ygdbS8Az4trMDD3t6cto/6Pfn1MkVDodDPozKB1pLXTLAbDZrbV+OH+0r0hth9sLrHAOVFboA8HZ3i+2dnVBWVqayeYfm9Dp8z+OBQChEr8M0NyyiTh1alMgRvqNLcsKeoGymtVxAx1PZENElA7J9Q1tixewbFupKmvDoNfAxKm2E5J8hC3kXqGeeBLEn6CsPjUU3qtKWF22J/dkUxQC28dxwOY0MoODbUJWClwMlG7JlnrzkG7KWzAsNzAD+ZYi1F/5qf8wA+tqrVkrmyxAzfIz8//48zkyQ34AT4AQ4AU6AE+AEOAFOgBPgBDgBToAT4AT0JvAL80N/mFc/WlQAAAAASUVORK5CYII=';
3+
$text_cursor_32_3x: 'data:image/png;base64,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';
4+
5+
.ba-HighlightCreator {
6+
cursor: url($text_cursor_32) 16 16, text; /* Legacy */
7+
cursor: image-set(url($text_cursor_32) 1x, url($text_cursor_32_2x) 2x, url($text_cursor_32_3x) 3x) 16 16, text; /* Webkit */ /* stylelint-disable-line */
8+
}

src/highlight/HighlightCreator.tsx

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import * as React from 'react';
2+
import classNames from 'classnames';
3+
import './HighlightCreator.scss';
4+
5+
type Props = {
6+
className?: string;
7+
};
8+
9+
export default function HighlightCreator({ className }: Props): JSX.Element {
10+
return <div className={classNames(className, 'ba-HighlightCreator')} data-testid="ba-HighlightCreator" />;
11+
}

src/highlight/HighlightManager.tsx

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import * as React from 'react';
2+
import * as ReactDOM from 'react-dom';
3+
import BaseManager, { Options, Props } from '../common/BaseManager';
4+
import HighlightContainer from './HighlightContainer';
5+
6+
export default class HighlightManager implements BaseManager {
7+
location: number;
8+
9+
reactEl: HTMLElement;
10+
11+
constructor({ location = 1, referenceEl }: Options) {
12+
this.location = location;
13+
this.reactEl = this.insert(referenceEl);
14+
}
15+
16+
destroy(): void {
17+
ReactDOM.unmountComponentAtNode(this.reactEl);
18+
19+
this.reactEl.remove();
20+
}
21+
22+
exists(parentEl: HTMLElement): boolean {
23+
return parentEl.contains(this.reactEl);
24+
}
25+
26+
insert(referenceEl: HTMLElement): HTMLElement {
27+
// Find the nearest applicable reference and document elements
28+
const documentEl = referenceEl.ownerDocument || document;
29+
const parentEl = referenceEl.parentNode || documentEl;
30+
31+
// Construct a layer element where we can inject a root React component
32+
const rootLayerEl = documentEl.createElement('div');
33+
rootLayerEl.classList.add('ba-Layer');
34+
rootLayerEl.classList.add('ba-Layer--highlight');
35+
rootLayerEl.dataset.testid = 'ba-Layer--highlight';
36+
rootLayerEl.setAttribute('data-resin-feature', 'annotations');
37+
38+
// Insert the new layer element immediately after the reference element
39+
return parentEl.insertBefore(rootLayerEl, referenceEl.nextSibling);
40+
}
41+
42+
render(props: Props): void {
43+
ReactDOM.render(<HighlightContainer {...props} />, this.reactEl);
44+
}
45+
46+
style(styles: Partial<CSSStyleDeclaration>): CSSStyleDeclaration {
47+
return Object.assign(this.reactEl.style, styles);
48+
}
49+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import React from 'react';
2+
import { shallow, ShallowWrapper } from 'enzyme';
3+
import HighlightAnnotations from '../HighlightAnnotations';
4+
import HighlightCreator from '../HighlightCreator';
5+
6+
jest.mock('../HighlightCreator');
7+
8+
describe('components/highlight/HighlightAnnotations', () => {
9+
const defaults = {
10+
isCreating: false,
11+
};
12+
13+
const getWrapper = (props = {}): ShallowWrapper => shallow(<HighlightAnnotations {...defaults} {...props} />);
14+
15+
describe('render()', () => {
16+
test('should render a RegionCreator if in creation mode', () => {
17+
const wrapper = getWrapper({ isCreating: true });
18+
const creator = wrapper.find(HighlightCreator);
19+
20+
expect(creator.hasClass('ba-HighlightAnnotations-creator')).toBe(true);
21+
});
22+
23+
test('should not render creation components if not in creation mode', () => {
24+
const wrapper = getWrapper({ isCreating: false });
25+
26+
expect(wrapper.exists(HighlightCreator)).toBe(false);
27+
});
28+
});
29+
});
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import * as React from 'react';
2+
import { IntlShape } from 'react-intl';
3+
import { mount, ReactWrapper } from 'enzyme';
4+
import HighlightAnnotations from '../HighlightAnnotations';
5+
import HighlightContainer, { Props } from '../HighlightContainer';
6+
import { createStore } from '../../store';
7+
8+
jest.mock('../../common/withProviders');
9+
jest.mock('../HighlightAnnotations');
10+
11+
describe('HighlightContainer', () => {
12+
const defaults = {
13+
intl: {} as IntlShape,
14+
location: 1,
15+
store: createStore(),
16+
};
17+
const getWrapper = (props = {}): ReactWrapper<Props> => mount(<HighlightContainer {...defaults} {...props} />);
18+
19+
describe('render', () => {
20+
test('should connect the underlying component and wrap it with a root provider', () => {
21+
const wrapper = getWrapper();
22+
23+
expect(wrapper.exists('RootProvider')).toBe(true);
24+
expect(wrapper.find(HighlightAnnotations).props()).toMatchObject({
25+
isCreating: false,
26+
});
27+
});
28+
});
29+
});

0 commit comments

Comments
 (0)