From 04ea8305fdc3e1dc918b980f9b5a2fdfacc9a2fa Mon Sep 17 00:00:00 2001 From: Mingze Date: Thu, 26 Mar 2020 17:08:53 -0700 Subject: [PATCH] feat(annotations): Add region comment button in toolbar (#1183) * feat(annotations): Add region comment button in toolbar * feat(annotations): Correct styles * feat(annotations): Address comments * feat(annotations): Rename variables * feat(annotations): Add tests, SUIT style, and typescript * feat(annotations): Fix typescript * feat(annotations): Address comments * feat(annotations): Address comments * feat(annotations): Address comments --- src/i18n/en-US.properties | 2 + src/lib/AnnotationControls.scss | 22 +++++ src/lib/AnnotationControls.ts | 74 ++++++++++++++ src/lib/Controls.scss | 16 ++- src/lib/Preview.js | 5 +- src/lib/ZoomControls.js | 2 +- src/lib/ZoomControls.scss | 6 ++ .../__tests__/AnnotationControls-test.html | 1 + src/lib/__tests__/AnnotationControls-test.js | 97 +++++++++++++++++++ src/lib/__tests__/ZoomControls-test.js | 4 +- src/lib/icons/icons.js | 2 + src/lib/icons/region_comment.svg | 1 + src/lib/viewers/doc/DocBaseViewer.js | 19 ++++ 13 files changed, 238 insertions(+), 13 deletions(-) create mode 100644 src/lib/AnnotationControls.scss create mode 100644 src/lib/AnnotationControls.ts create mode 100644 src/lib/ZoomControls.scss create mode 100644 src/lib/__tests__/AnnotationControls-test.html create mode 100644 src/lib/__tests__/AnnotationControls-test.js create mode 100644 src/lib/icons/region_comment.svg diff --git a/src/i18n/en-US.properties b/src/i18n/en-US.properties index fe8a6ea56..8a494b034 100644 --- a/src/i18n/en-US.properties +++ b/src/i18n/en-US.properties @@ -10,6 +10,8 @@ zoom_out=Zoom out enter_fullscreen=Enter fullscreen # Button tooltip for exiting a full screen preview exit_fullscreen=Exit fullscreen +# Button tooltip for annotation region comment +region_comment=Comment on Region # Button tooltip for going to the previous page in a preview previous_page=Previous page # Input tooltip for navigating to a specific page in a preview diff --git a/src/lib/AnnotationControls.scss b/src/lib/AnnotationControls.scss new file mode 100644 index 000000000..36359b2f1 --- /dev/null +++ b/src/lib/AnnotationControls.scss @@ -0,0 +1,22 @@ +.bp-AnnotationControls-group { + padding: 0 4px 0 8px; + border-left: 1px solid $twos; + + .bp-AnnotationControls-regionBtn { + width: $controls-button-width; + height: $controls-button-width; + border-radius: 4px; + + svg { + fill: $white; + } + + &.is-active { + background-color: $white; + + svg { + fill: $black; + } + } + } +} diff --git a/src/lib/AnnotationControls.ts b/src/lib/AnnotationControls.ts new file mode 100644 index 000000000..c625dc027 --- /dev/null +++ b/src/lib/AnnotationControls.ts @@ -0,0 +1,74 @@ +import noop from 'lodash/noop'; +import { ICON_REGION_COMMENT } from './icons/icons'; +import Controls, { CLASS_BOX_CONTROLS_GROUP_BUTTON } from './Controls'; + +export const CLASS_ANNOTATIONS_GROUP = 'bp-AnnotationControls-group'; +export const CLASS_REGION_BUTTON = 'bp-AnnotationControls-regionBtn'; +export const CLASS_BUTTON_ACTIVE = 'is-active'; + +export type RegionHandler = ({ isRegionActive, event }: { isRegionActive: boolean; event: MouseEvent }) => void; +export type Options = { + onRegionClick?: RegionHandler; +}; + +declare const __: (key: string) => string; + +export default class AnnotationControls { + /** @property {Controls} - Controls object */ + private controls: Controls; + + /** @property {boolean} - Region comment mode active state */ + private isRegionActive = false; + + /** + * [constructor] + * + * @param {Controls} controls - Viewer controls + * @return {AnnotationControls} Instance of AnnotationControls + */ + constructor(controls: Controls) { + if (!controls || !(controls instanceof Controls)) { + throw Error('controls must be an instance of Controls'); + } + + this.controls = controls; + } + + /** + * Region comment button click handler + * + * @param {RegionHandler} onRegionClick - region click handler in options + * @param {MouseEvent} event - mouse event + * @return {void} + */ + private handleRegionClick = (onRegionClick: RegionHandler) => (event: MouseEvent): void => { + const regionButtonElement = event.target as HTMLButtonElement; + + this.isRegionActive = !this.isRegionActive; + if (this.isRegionActive) { + regionButtonElement.classList.add(CLASS_BUTTON_ACTIVE); + } else { + regionButtonElement.classList.remove(CLASS_BUTTON_ACTIVE); + } + + onRegionClick({ isRegionActive: this.isRegionActive, event }); + }; + + /** + * Initialize the annotation controls with options. + * + * @param {RegionHandler} [options.onRegionClick] - Callback when region comment button is clicked + * @return {void} + */ + public init({ onRegionClick = noop }: Options = {}): void { + const groupElement = this.controls.addGroup(CLASS_ANNOTATIONS_GROUP); + this.controls.add( + __('region_comment'), + this.handleRegionClick(onRegionClick), + `${CLASS_BOX_CONTROLS_GROUP_BUTTON} ${CLASS_REGION_BUTTON}`, + ICON_REGION_COMMENT, + 'button', + groupElement, + ); + } +} diff --git a/src/lib/Controls.scss b/src/lib/Controls.scss index b5ea4a18e..93d49d0c5 100644 --- a/src/lib/Controls.scss +++ b/src/lib/Controls.scss @@ -1,3 +1,8 @@ +$controls-button-width: 32px; + +@import './AnnotationControls'; +@import './ZoomControls'; + .bp-controls-wrapper { position: absolute; bottom: 25px; @@ -11,7 +16,7 @@ position: relative; left: -50%; display: flex; - background: fade-out($twos, .05); + background: fade-out($black, .2); border-radius: 3px; opacity: 0; transition: opacity .5s; @@ -159,13 +164,6 @@ } } -.bp-zoom-current-scale { - min-width: 48px; - color: $white; - font-size: 14px; - text-align: center; -} - .bp-controls-group { display: flex; align-items: center; @@ -173,7 +171,7 @@ margin-left: 4px; .bp-controls-group-btn { - width: 32px; + width: $controls-button-width; } & + .bp-controls-cell { diff --git a/src/lib/Preview.js b/src/lib/Preview.js index 43194b265..1197f184b 100644 --- a/src/lib/Preview.js +++ b/src/lib/Preview.js @@ -921,9 +921,12 @@ class Preview extends EventEmitter { // Whether download button should be shown this.options.showDownload = !!options.showDownload; - // Whether annotations and annotation controls should be shown + // Whether annotations v2 should be shown this.options.showAnnotations = !!options.showAnnotations; + // Whether annotations v4 buttons should be shown in toolbar + this.options.showAnnotationsControls = !!options.showAnnotationsControls; + // Enable or disable hotkeys this.options.useHotkeys = options.useHotkeys !== false; diff --git a/src/lib/ZoomControls.js b/src/lib/ZoomControls.js index 05ac6c42a..16c86cdc0 100644 --- a/src/lib/ZoomControls.js +++ b/src/lib/ZoomControls.js @@ -3,7 +3,7 @@ import noop from 'lodash/noop'; import { ICON_ZOOM_IN, ICON_ZOOM_OUT } from './icons/icons'; import Controls, { CLASS_BOX_CONTROLS_GROUP_BUTTON } from './Controls'; -const CLASS_ZOOM_CURRENT_SCALE = 'bp-zoom-current-scale'; +const CLASS_ZOOM_CURRENT_SCALE = 'bp-ZoomControls-currentScale'; const CLASS_ZOOM_CURRENT_SCALE_VALUE = 'bp-zoom-current-scale-value'; const CLASS_ZOOM_IN_BUTTON = 'bp-zoom-in-btn'; const CLASS_ZOOM_OUT_BUTTON = 'bp-zoom-out-btn'; diff --git a/src/lib/ZoomControls.scss b/src/lib/ZoomControls.scss new file mode 100644 index 000000000..9d43b41fe --- /dev/null +++ b/src/lib/ZoomControls.scss @@ -0,0 +1,6 @@ +.bp-ZoomControls-currentScale { + min-width: 48px; + color: $white; + font-size: 14px; + text-align: center; +} diff --git a/src/lib/__tests__/AnnotationControls-test.html b/src/lib/__tests__/AnnotationControls-test.html new file mode 100644 index 000000000..bb1d1e682 --- /dev/null +++ b/src/lib/__tests__/AnnotationControls-test.html @@ -0,0 +1 @@ +
diff --git a/src/lib/__tests__/AnnotationControls-test.js b/src/lib/__tests__/AnnotationControls-test.js new file mode 100644 index 000000000..bd4a2b561 --- /dev/null +++ b/src/lib/__tests__/AnnotationControls-test.js @@ -0,0 +1,97 @@ +/* eslint-disable no-unused-expressions */ +import AnnotationControls, { CLASS_REGION_BUTTON, CLASS_BUTTON_ACTIVE } from '../AnnotationControls'; +import Controls, { CLASS_BOX_CONTROLS_GROUP_BUTTON } from '../Controls'; +import { ICON_REGION_COMMENT } from '../icons/icons'; + +let annotationControls; +let stubs = {}; + +const sandbox = sinon.sandbox.create(); + +describe('lib/AnnotationControls', () => { + before(() => { + fixture.setBase('src/lib'); + }); + + beforeEach(() => { + fixture.load('__tests__/AnnotationControls-test.html'); + const controls = new Controls(document.getElementById('test-annotation-controls-container')); + annotationControls = new AnnotationControls(controls); + stubs.onRegionClick = sandbox.stub(); + }); + + afterEach(() => { + fixture.cleanup(); + sandbox.verifyAndRestore(); + + annotationControls = null; + stubs = {}; + }); + + describe('constructor()', () => { + it('should create the correct DOM structure', () => { + expect(annotationControls.controls).not.to.be.undefined; + }); + + it('should throw an exception if controls is not provided', () => { + expect(() => new AnnotationControls()).to.throw(Error, 'controls must be an instance of Controls'); + }); + }); + + describe('init()', () => { + beforeEach(() => { + stubs.add = sandbox.stub(annotationControls.controls, 'add'); + stubs.regionHandler = sandbox.stub(); + sandbox.stub(annotationControls, 'handleRegionClick').returns(stubs.regionHandler); + }); + + it('should add the controls', () => { + annotationControls.init({ onRegionClick: stubs.onRegionClick }); + + expect(stubs.add).to.be.calledWith( + __('region_comment'), + stubs.regionHandler, + `${CLASS_BOX_CONTROLS_GROUP_BUTTON} ${CLASS_REGION_BUTTON}`, + ICON_REGION_COMMENT, + 'button', + sinon.match.any, + ); + }); + }); + + describe('handleRegionClick()', () => { + beforeEach(() => { + stubs.classListAdd = sandbox.stub(); + stubs.classListRemove = sandbox.stub(); + stubs.event = sandbox.stub({ + target: { + classList: { + add: stubs.classListAdd, + remove: stubs.classListRemove, + }, + }, + }); + }); + + it('should activate region button then deactivate', () => { + expect(annotationControls.isRegionActive).to.be.false; + + annotationControls.handleRegionClick(stubs.onRegionClick)(stubs.event); + expect(annotationControls.isRegionActive).to.be.true; + expect(stubs.classListAdd).to.be.calledWith(CLASS_BUTTON_ACTIVE); + + annotationControls.handleRegionClick(stubs.onRegionClick)(stubs.event); + expect(annotationControls.isRegionActive).to.be.false; + expect(stubs.classListRemove).to.be.calledWith(CLASS_BUTTON_ACTIVE); + }); + + it('should call onRegionClick', () => { + annotationControls.handleRegionClick(stubs.onRegionClick)(stubs.event); + + expect(stubs.onRegionClick).to.be.calledWith({ + isRegionActive: true, + event: stubs.event, + }); + }); + }); +}); diff --git a/src/lib/__tests__/ZoomControls-test.js b/src/lib/__tests__/ZoomControls-test.js index 5be478aeb..10874d846 100644 --- a/src/lib/__tests__/ZoomControls-test.js +++ b/src/lib/__tests__/ZoomControls-test.js @@ -59,7 +59,7 @@ describe('lib/ZoomControls', () => { expect(stubs.add).to.be.calledWith( __('zoom_current_scale'), undefined, - 'bp-zoom-current-scale', + 'bp-ZoomControls-currentScale', sinon.match.string, 'div', sinon.match.any, @@ -125,7 +125,7 @@ describe('lib/ZoomControls', () => { expect(stubs.add).to.be.calledWith( __('zoom_current_scale'), undefined, - 'bp-zoom-current-scale', + 'bp-ZoomControls-currentScale', sinon.match.string, 'div', sinon.match.any, diff --git a/src/lib/icons/icons.js b/src/lib/icons/icons.js index 8a1c67198..f3c8a55cf 100644 --- a/src/lib/icons/icons.js +++ b/src/lib/icons/icons.js @@ -6,6 +6,7 @@ import FULLSCREEN_OUT from './full_screen_out_24px.svg'; import ROTATE_LEFT from './rotate_left_24px.svg'; import ZOOM_IN from './zoom_in.svg'; import ZOOM_OUT from './zoom_out.svg'; +import REGION_COMMENT from './region_comment.svg'; import ARROW_LEFT from './arrow_left_24px.svg'; import ARROW_RIGHT from './arrow_right_24px.svg'; import CHECK_MARK from './checkmark_24px.svg'; @@ -55,6 +56,7 @@ export const ICON_FULLSCREEN_OUT = FULLSCREEN_OUT; export const ICON_ROTATE_LEFT = ROTATE_LEFT; export const ICON_ZOOM_IN = ZOOM_IN; export const ICON_ZOOM_OUT = ZOOM_OUT; +export const ICON_REGION_COMMENT = REGION_COMMENT; export const ICON_ARROW_LEFT = ARROW_LEFT; export const ICON_ARROW_RIGHT = ARROW_RIGHT; export const ICON_CHECK_MARK = CHECK_MARK; diff --git a/src/lib/icons/region_comment.svg b/src/lib/icons/region_comment.svg new file mode 100644 index 000000000..44d60d9e7 --- /dev/null +++ b/src/lib/icons/region_comment.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/lib/viewers/doc/DocBaseViewer.js b/src/lib/viewers/doc/DocBaseViewer.js index 4e090af52..a4f59b5a8 100644 --- a/src/lib/viewers/doc/DocBaseViewer.js +++ b/src/lib/viewers/doc/DocBaseViewer.js @@ -1,4 +1,5 @@ import throttle from 'lodash/throttle'; +import AnnotationControls from '../../AnnotationControls'; import BaseViewer from '../BaseViewer'; import Browser from '../../Browser'; import Controls from '../../Controls'; @@ -96,6 +97,7 @@ class DocBaseViewer extends BaseViewer { this.pinchToZoomEndHandler = this.pinchToZoomEndHandler.bind(this); this.pinchToZoomStartHandler = this.pinchToZoomStartHandler.bind(this); this.print = this.print.bind(this); + this.regionClickHandler = this.regionClickHandler.bind(this); this.setPage = this.setPage.bind(this); this.throttledScrollHandler = this.getScrollHandler().bind(this); this.toggleThumbnails = this.toggleThumbnails.bind(this); @@ -1009,6 +1011,9 @@ class DocBaseViewer extends BaseViewer { this.controls = new Controls(this.containerEl); this.pageControls = new PageControls(this.controls, this.docEl); this.zoomControls = new ZoomControls(this.controls); + if (this.options.showAnnotationsControls) { + this.annotationControls = new AnnotationControls(this.controls); + } this.pageControls.addListener('pagechange', this.setPage); this.bindControlListeners(); } @@ -1090,8 +1095,22 @@ class DocBaseViewer extends BaseViewer { ICON_FULLSCREEN_IN, ); this.controls.add(__('exit_fullscreen'), this.toggleFullscreen, 'bp-exit-fullscreen-icon', ICON_FULLSCREEN_OUT); + + if (this.options.showAnnotationsControls) { + this.annotationControls.init({ + onRegionClick: this.regionClickHandler, + }); + } } + /** + * Handler for annotation toolbar region comment button click event. + * + * @private + * @return {void} + */ + regionClickHandler() {} + /** * Handler for 'pagesinit' event. *