From a0c08273b6c0c1ddd568f5d99fbc07af6fded8db Mon Sep 17 00:00:00 2001 From: Mingze Date: Thu, 2 Apr 2020 14:23:12 -0700 Subject: [PATCH] feat(annotations): Toggle region annotation mode (#1192) * feat(annotations): Toggle region annotation mode * feat(annotations): Move addListener to BaseViewer --- src/lib/AnnotationControls.ts | 74 +++++++++++++++----- src/lib/__tests__/AnnotationControls-test.js | 59 ++++++++++++---- src/lib/viewers/BaseViewer.js | 4 ++ src/lib/viewers/__tests__/BaseViewer-test.js | 8 +++ src/lib/viewers/doc/DocBaseViewer.js | 6 +- 5 files changed, 117 insertions(+), 34 deletions(-) diff --git a/src/lib/AnnotationControls.ts b/src/lib/AnnotationControls.ts index f45578d00..cc56cbb27 100644 --- a/src/lib/AnnotationControls.ts +++ b/src/lib/AnnotationControls.ts @@ -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'; @@ -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] @@ -36,6 +44,9 @@ export default class AnnotationControls { this.controls = controls; this.controlsElement = controls.controlsEl; + this.controlsMap = { + [AnnotationMode.REGION]: this.updateRegionButton, + }; this.attachEventHandlers(); } @@ -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 }); }; /** @@ -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', diff --git a/src/lib/__tests__/AnnotationControls-test.js b/src/lib/__tests__/AnnotationControls-test.js index 0650a02ad..3b596f4c8 100644 --- a/src/lib/__tests__/AnnotationControls-test.js +++ b/src/lib/__tests__/AnnotationControls-test.js @@ -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, @@ -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: { @@ -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; }); @@ -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', () => { @@ -61,10 +65,6 @@ describe('lib/AnnotationControls', () => { }); describe('destroy()', () => { - beforeEach(() => { - stubs.fullscreenRemoveListener = sandbox.stub(fullscreen, 'removeListener'); - }); - it('should remove all listeners', () => { annotationControls.destroy(); @@ -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', () => { @@ -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, }); }); @@ -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; + }); + }); }); diff --git a/src/lib/viewers/BaseViewer.js b/src/lib/viewers/BaseViewer.js index 93f698436..d267c0f0c 100644 --- a/src/lib/viewers/BaseViewer.js +++ b/src/lib/viewers/BaseViewer.js @@ -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); + } } /** diff --git a/src/lib/viewers/__tests__/BaseViewer-test.js b/src/lib/viewers/__tests__/BaseViewer-test.js index e873ee887..892e69938 100644 --- a/src/lib/viewers/__tests__/BaseViewer-test.js +++ b/src/lib/viewers/__tests__/BaseViewer-test.js @@ -1098,6 +1098,7 @@ describe('lib/viewers/BaseViewer', () => { location: { locale: 'en-US', }, + showAnnotationsControls: true, }; base.scale = 1.5; base.annotator = { @@ -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', () => { @@ -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); }); diff --git a/src/lib/viewers/doc/DocBaseViewer.js b/src/lib/viewers/doc/DocBaseViewer.js index ab1c53c09..20c90668f 100644 --- a/src/lib/viewers/doc/DocBaseViewer.js +++ b/src/lib/viewers/doc/DocBaseViewer.js @@ -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'; @@ -1113,7 +1113,9 @@ class DocBaseViewer extends BaseViewer { * @private * @return {void} */ - regionClickHandler() {} + regionClickHandler() { + this.annotator.toggleAnnotationMode(AnnotationMode.REGION); + } /** * Handler for 'pagesinit' event.