Skip to content

Commit

Permalink
feat(annotations): Toggle region annotation mode (#1192)
Browse files Browse the repository at this point in the history
* feat(annotations): Toggle region annotation mode

* feat(annotations): Move addListener to BaseViewer
  • Loading branch information
Mingze authored Apr 2, 2020
1 parent ff45da5 commit a0c0827
Show file tree
Hide file tree
Showing 5 changed files with 117 additions and 34 deletions.
74 changes: 57 additions & 17 deletions src/lib/AnnotationControls.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import noop from 'lodash/noop';

import { ICON_REGION_COMMENT } from './icons/icons';
import Controls, { CLASS_BOX_CONTROLS_GROUP_BUTTON } from './Controls';
import fullscreen from './Fullscreen';
Expand All @@ -9,22 +10,29 @@ export const CLASS_REGION_BUTTON = 'bp-AnnotationControls-regionBtn';
export const CLASS_BUTTON_ACTIVE = 'is-active';
export const CLASS_GROUP_HIDE = 'is-hidden';

export type RegionHandler = ({ isRegionActive, event }: { isRegionActive: boolean; event: MouseEvent }) => void;
export enum AnnotationMode {
NONE = 'none',
REGION = 'region',
}
export type ClickHandler = ({ activeControl, event }: { activeControl: AnnotationMode; event: MouseEvent }) => void;
export type Options = {
onRegionClick?: RegionHandler;
onRegionClick?: ClickHandler;
};

declare const __: (key: string) => string;

interface ControlsMap {
[key: string]: () => void;
}

export default class AnnotationControls {
/** @property {Controls} - Controls object */
private controls: Controls;

/** @property {HTMLElement} - Controls element */
private controlsElement: HTMLElement;

/** @property {boolean} - Region comment mode active state */
private isRegionActive = false;
private controlsMap: ControlsMap;

private currentActiveControl: AnnotationMode = AnnotationMode.NONE;

/**
* [constructor]
Expand All @@ -36,6 +44,9 @@ export default class AnnotationControls {

this.controls = controls;
this.controlsElement = controls.controlsEl;
this.controlsMap = {
[AnnotationMode.REGION]: this.updateRegionButton,
};

this.attachEventHandlers();
}
Expand Down Expand Up @@ -84,21 +95,50 @@ export default class AnnotationControls {
private handleFullscreenExit = (): void => this.handleFullscreenChange(false);

/**
* Region comment button click handler
* Deactivate current control button
*/
public resetControls = (): void => {
if (this.currentActiveControl === AnnotationMode.NONE) {
return;
}

const updateButton = this.controlsMap[this.currentActiveControl];

this.currentActiveControl = AnnotationMode.NONE;
updateButton();
};

/**
* Update region button UI
*/
private handleRegionClick = (onRegionClick: RegionHandler) => (event: MouseEvent): void => {
private updateRegionButton = (): void => {
const regionButtonElement = this.controlsElement.querySelector(`.${CLASS_REGION_BUTTON}`);

if (regionButtonElement) {
this.isRegionActive = !this.isRegionActive;
if (this.isRegionActive) {
regionButtonElement.classList.add(CLASS_BUTTON_ACTIVE);
} else {
regionButtonElement.classList.remove(CLASS_BUTTON_ACTIVE);
}
if (!regionButtonElement) {
return;
}

if (this.currentActiveControl === AnnotationMode.REGION) {
regionButtonElement.classList.add(CLASS_BUTTON_ACTIVE);
} else {
regionButtonElement.classList.remove(CLASS_BUTTON_ACTIVE);
}
};

/**
* Region comment button click handler
*/
private handleClick = (onClick: ClickHandler, mode: AnnotationMode) => (event: MouseEvent): void => {
const prevActiveControl = this.currentActiveControl;

this.resetControls();

if (prevActiveControl !== mode) {
this.currentActiveControl = mode as AnnotationMode;
this.controlsMap[mode]();
}

onRegionClick({ isRegionActive: this.isRegionActive, event });
onClick({ activeControl: this.currentActiveControl, event });
};

/**
Expand All @@ -108,7 +148,7 @@ export default class AnnotationControls {
const groupElement = this.controls.addGroup(CLASS_ANNOTATIONS_GROUP);
this.controls.add(
__('region_comment'),
this.handleRegionClick(onRegionClick),
this.handleClick(onRegionClick, AnnotationMode.REGION),
`${CLASS_BOX_CONTROLS_GROUP_BUTTON} ${CLASS_REGION_BUTTON}`,
ICON_REGION_COMMENT,
'button',
Expand Down
59 changes: 44 additions & 15 deletions src/lib/__tests__/AnnotationControls-test.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
/* eslint-disable no-unused-expressions */
import { ICON_REGION_COMMENT } from '../icons/icons';
import AnnotationControls, {
AnnotationMode,
CLASS_ANNOTATIONS_GROUP,
CLASS_BUTTON_ACTIVE,
CLASS_GROUP_HIDE,
Expand All @@ -21,11 +22,10 @@ describe('lib/AnnotationControls', () => {

beforeEach(() => {
fixture.load('__tests__/AnnotationControls-test.html');
const controls = new Controls(document.getElementById('test-annotation-controls-container'));

stubs.classListAdd = sandbox.stub();
stubs.classListRemove = sandbox.stub();
stubs.fullscreenAddListener = sandbox.stub(fullscreen, 'addListener');
stubs.fullscreenRemoveListener = sandbox.stub(fullscreen, 'removeListener');
stubs.onRegionClick = sandbox.stub();
stubs.querySelector = sandbox.stub().returns({
classList: {
Expand All @@ -34,6 +34,7 @@ describe('lib/AnnotationControls', () => {
},
});

const controls = new Controls(document.getElementById('test-annotation-controls-container'));
annotationControls = new AnnotationControls(controls);
annotationControls.controlsElement.querySelector = stubs.querySelector;
});
Expand All @@ -49,6 +50,9 @@ describe('lib/AnnotationControls', () => {
describe('constructor()', () => {
it('should create the correct DOM structure', () => {
expect(annotationControls.controls).not.to.be.undefined;
expect(annotationControls.controlsElement).not.to.be.undefined;
expect(annotationControls.controlsMap).not.to.be.undefined;
expect(annotationControls.currentActiveControl).to.equal(AnnotationMode.NONE);
});

it('should attach event listeners', () => {
Expand All @@ -61,10 +65,6 @@ describe('lib/AnnotationControls', () => {
});

describe('destroy()', () => {
beforeEach(() => {
stubs.fullscreenRemoveListener = sandbox.stub(fullscreen, 'removeListener');
});

it('should remove all listeners', () => {
annotationControls.destroy();

Expand All @@ -76,7 +76,7 @@ describe('lib/AnnotationControls', () => {
beforeEach(() => {
stubs.add = sandbox.stub(annotationControls.controls, 'add');
stubs.regionHandler = sandbox.stub();
sandbox.stub(annotationControls, 'handleRegionClick').returns(stubs.regionHandler);
sandbox.stub(annotationControls, 'handleClick').returns(stubs.regionHandler);
});

it('should add the controls', () => {
Expand All @@ -93,24 +93,28 @@ describe('lib/AnnotationControls', () => {
});
});

describe('handleRegionClick()', () => {
describe('handleClick()', () => {
beforeEach(() => {
stubs.event = sandbox.stub({});
});

it('should activate region button then deactivate', () => {
expect(annotationControls.isRegionActive).to.be.false;
expect(annotationControls.currentActiveControl).to.equal(AnnotationMode.NONE);

annotationControls.handleRegionClick(stubs.onRegionClick)(stubs.event);
expect(annotationControls.isRegionActive).to.be.true;
annotationControls.handleClick(stubs.onRegionClick, AnnotationMode.REGION)(stubs.event);
expect(annotationControls.currentActiveControl).to.equal(AnnotationMode.REGION);
expect(stubs.classListAdd).to.be.calledWith(CLASS_BUTTON_ACTIVE);

annotationControls.handleRegionClick(stubs.onRegionClick)(stubs.event);
expect(annotationControls.isRegionActive).to.be.false;
annotationControls.handleClick(stubs.onRegionClick, AnnotationMode.REGION)(stubs.event);
expect(annotationControls.currentActiveControl).to.equal(AnnotationMode.NONE);
expect(stubs.classListRemove).to.be.calledWith(CLASS_BUTTON_ACTIVE);
});

it('should call onRegionClick', () => {
annotationControls.handleRegionClick(stubs.onRegionClick)(stubs.event);
annotationControls.handleClick(stubs.onRegionClick, AnnotationMode.REGION)(stubs.event);

expect(stubs.onRegionClick).to.be.calledWith({
isRegionActive: true,
activeControl: AnnotationMode.REGION,
event: stubs.event,
});
});
Expand All @@ -127,4 +131,29 @@ describe('lib/AnnotationControls', () => {
expect(stubs.classListRemove).to.be.calledWith(CLASS_GROUP_HIDE);
});
});

describe('resetControls()', () => {
beforeEach(() => {
stubs.updateRegionButton = sandbox.stub();
annotationControls.controlsMap = {
[AnnotationMode.REGION]: stubs.updateRegionButton,
};
});

it('should not change if no current active control', () => {
annotationControls.resetControls();

expect(annotationControls.currentActiveControl).to.equal(AnnotationMode.NONE);
expect(stubs.updateRegionButton).not.to.be.called;
});

it('should call updateRegionButton if current control is region', () => {
annotationControls.currentActiveControl = AnnotationMode.REGION;

annotationControls.resetControls();

expect(annotationControls.currentActiveControl).to.equal(AnnotationMode.NONE);
expect(stubs.updateRegionButton).to.be.called;
});
});
});
4 changes: 4 additions & 0 deletions src/lib/viewers/BaseViewer.js
Original file line number Diff line number Diff line change
Expand Up @@ -963,6 +963,10 @@ class BaseViewer extends EventEmitter {

// Add a custom listener for events emmited by the annotator
this.annotator.addListener('annotatorevent', this.handleAnnotatorEvents);

if (this.options.showAnnotationsControls && this.annotationControls) {
this.annotator.addListener('annotationcreate', this.annotationControls.resetControls);
}
}

/**
Expand Down
8 changes: 8 additions & 0 deletions src/lib/viewers/__tests__/BaseViewer-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -1098,6 +1098,7 @@ describe('lib/viewers/BaseViewer', () => {
location: {
locale: 'en-US',
},
showAnnotationsControls: true,
};
base.scale = 1.5;
base.annotator = {
Expand All @@ -1107,6 +1108,9 @@ describe('lib/viewers/BaseViewer', () => {
base.annotatorConf = {
CONSTRUCTOR: sandbox.stub().returns(base.annotator),
};
base.annotationControls = {
resetControls: sandbox.stub(),
};
});

it('should initialize the annotator', () => {
Expand All @@ -1118,6 +1122,10 @@ describe('lib/viewers/BaseViewer', () => {
expect(base.addListener).to.be.calledWith('toggleannotationmode', sinon.match.func);
expect(base.addListener).to.be.calledWith('scale', sinon.match.func);
expect(base.addListener).to.be.calledWith('scrolltoannotation', sinon.match.func);
expect(base.annotator.addListener).to.be.calledWith(
'annotationcreate',
base.annotationControls.resetControls,
);
expect(base.annotator.addListener).to.be.calledWith('annotatorevent', sinon.match.func);
expect(base.emit).to.be.calledWith('annotator', base.annotator);
});
Expand Down
6 changes: 4 additions & 2 deletions src/lib/viewers/doc/DocBaseViewer.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import throttle from 'lodash/throttle';
import AnnotationControls from '../../AnnotationControls';
import AnnotationControls, { AnnotationMode } from '../../AnnotationControls';
import BaseViewer from '../BaseViewer';
import Browser from '../../Browser';
import Controls from '../../Controls';
Expand Down Expand Up @@ -1113,7 +1113,9 @@ class DocBaseViewer extends BaseViewer {
* @private
* @return {void}
*/
regionClickHandler() {}
regionClickHandler() {
this.annotator.toggleAnnotationMode(AnnotationMode.REGION);
}

/**
* Handler for 'pagesinit' event.
Expand Down

0 comments on commit a0c0827

Please sign in to comment.