diff --git a/build/webpack.config.js b/build/webpack.config.js index 8fef3595d..f577ad3e4 100644 --- a/build/webpack.config.js +++ b/build/webpack.config.js @@ -54,7 +54,8 @@ function updateConfig(conf, language, index) { const config = Object.assign(conf, { entry: { preview: [`${lib}/Preview.js`], - csv: [`${lib}/viewers/text/BoxCSV.js`] + csv: [`${lib}/viewers/text/BoxCSV.js`], + annotations: [`${lib}/annotations/BoxAnnotations.js`] }, output: { path: path.resolve('dist', version, language), diff --git a/src/lib/Preview.js b/src/lib/Preview.js index 840f4bc0e..491e151e8 100644 --- a/src/lib/Preview.js +++ b/src/lib/Preview.js @@ -36,7 +36,6 @@ import { hideLoadingIndicator, showDownloadButton, showLoadingDownloadButton, - showAnnotateButton, showPrintButton, showNavigation } from './ui'; @@ -46,7 +45,6 @@ import { CLASS_NAVIGATION_VISIBILITY, FILE_EXT_ERROR_MAP, PERMISSION_DOWNLOAD, - PERMISSION_ANNOTATE, PERMISSION_PREVIEW, PREVIEW_SCRIPT_NAME, X_REP_HINT_BASE, @@ -924,10 +922,6 @@ class Preview extends EventEmitter { } } - if (checkPermission(this.file, PERMISSION_ANNOTATE) && !Browser.isMobile() && checkFeature(this.viewer, 'isAnnotatable', 'point')) { - showAnnotateButton(this.viewer.getPointModeClickHandler()); - } - const { error } = data; if (error) { // Bump up preview count diff --git a/src/lib/__tests__/Preview-test.js b/src/lib/__tests__/Preview-test.js index e299097e0..e0907c79a 100644 --- a/src/lib/__tests__/Preview-test.js +++ b/src/lib/__tests__/Preview-test.js @@ -1285,7 +1285,6 @@ describe('lib/Preview', () => { stubs.canDownload = sandbox.stub(Browser, 'canDownload'); stubs.showDownloadButton = sandbox.stub(ui, 'showDownloadButton'); stubs.showPrintButton = sandbox.stub(ui, 'showPrintButton'); - stubs.showAnnotateButton = sandbox.stub(ui, 'showAnnotateButton'); stubs.hideLoadingIndicator = sandbox.stub(ui, 'hideLoadingIndicator'); stubs.emit = sandbox.stub(preview, 'emit'); stubs.logPreviewEvent = sandbox.stub(preview, 'logPreviewEvent'); @@ -1369,43 +1368,6 @@ describe('lib/Preview', () => { expect(stubs.showPrintButton).to.be.called; }); - it('should show the annotation button if you have annotation permissions', () => { - stubs.checkPermission.onCall(1).returns(false).onCall(2).returns(true); - - - preview.finishLoading(); - expect(stubs.showAnnotateButton).to.not.be.called; - - stubs.checkPermission.onCall(3).returns(true); - - preview.finishLoading(); - expect(stubs.showAnnotateButton).to.be.called; - }); - - it('should show the annotation button if you are not on a mobile browser', () => { - stubs.isMobile.returns(true); - - preview.finishLoading(); - expect(stubs.showAnnotateButton).to.not.be.called; - - stubs.isMobile.returns(false); - - preview.finishLoading(); - expect(stubs.showAnnotateButton).to.be.called; - }); - - it('should show the annotation button if the viewer has annotation functionality', () => { - stubs.checkFeature.returns(false); - - preview.finishLoading(); - expect(stubs.showAnnotateButton).to.not.be.called; - - stubs.checkFeature.returns(true); - - preview.finishLoading(); - expect(stubs.showAnnotateButton).to.be.called; - }); - it('should increment the preview count', () => { preview.count.success = 0; diff --git a/src/lib/__tests__/ui-test.js b/src/lib/__tests__/ui-test.js index 040ec58e3..3cbf5a88a 100644 --- a/src/lib/__tests__/ui-test.js +++ b/src/lib/__tests__/ui-test.js @@ -127,19 +127,6 @@ describe('lib/ui', () => { }); }); - describe('showAnnotateButton()', () => { - it('should set up and show annotate button', () => { - const buttonEl = containerEl.querySelector(constants.SELECTOR_BOX_PREVIEW_BTN_ANNOTATE); - buttonEl.classList.add(constants.CLASS_HIDDEN); - sandbox.mock(buttonEl).expects('addEventListener').withArgs('click', handler); - - ui.showAnnotateButton(handler); - - expect(buttonEl.title).to.equal('Point annotation mode'); - expect(buttonEl.classList.contains(constants.CLASS_HIDDEN)).to.be.false; - }); - }); - describe('showPrintButton()', () => { it('should set up and show print button', () => { const buttonEl = containerEl.querySelector(constants.SELECTOR_BOX_PREVIEW_BTN_PRINT); diff --git a/src/lib/annotations/Annotator.js b/src/lib/annotations/Annotator.js index 7ce70672a..de82d5bf6 100644 --- a/src/lib/annotations/Annotator.js +++ b/src/lib/annotations/Annotator.js @@ -32,13 +32,13 @@ class Annotator extends EventEmitter { */ constructor(data) { super(); - this.annotatedElement = data.annotatedElement; - this.annotationService = data.annotationService; + + this.canAnnotate = data.canAnnotate; + this.container = data.container; + this.options = data.options; this.fileVersionID = data.fileVersionID; this.locale = data.locale; this.validationErrorDisplayed = false; - - this.notification = new Notification(this.annotatedElement); } /** @@ -65,8 +65,29 @@ class Annotator extends EventEmitter { * @return {void} */ init() { + this.annotatedElement = this.getAnnotatedEl(this.container); + this.annotationService = this.initAnnotationService(this.options); + this.notification = new Notification(this.annotatedElement); + this.setScale(1); this.setupAnnotations(); + this.showAnnotations(); + } + + /** + * Initializes the Annotation Service with appropriate options + * + * @param {Object} options - Options passed from the viewer to the annotator + * @return {AnnotationService} AnnotationService instance + */ + initAnnotationService(options) { + const { apiHost, fileId, token } = options; + return new AnnotationService({ + apiHost, + fileId, + token, + canAnnotate: this.canAnnotate + }); } /** @@ -219,6 +240,16 @@ class Annotator extends EventEmitter { createAnnotationThread(annotations, location, type) {} /* eslint-enable no-unused-vars */ + /** + * Must be implemented to determine the annotated element in the viewer. + * + * @param {HTMLElement} containerEl - Container element for the viewer + * @return {HTMLElement} Annotated element in the viewer + */ + /* eslint-disable no-unused-vars */ + getAnnotatedEl(containerEl) {} + /* eslint-enable no-unused-vars */ + //-------------------------------------------------------------------------- // Protected //-------------------------------------------------------------------------- diff --git a/src/lib/annotations/BoxAnnotations.js b/src/lib/annotations/BoxAnnotations.js new file mode 100644 index 000000000..f0ed8e7a7 --- /dev/null +++ b/src/lib/annotations/BoxAnnotations.js @@ -0,0 +1,54 @@ +import DocAnnotator from './doc/DocAnnotator'; +import ImageAnnotator from './image/ImageAnnotator'; + +const ANNOTATORS = [ + { + NAME: 'Document', + CONSTRUCTOR: DocAnnotator, + VIEWER: ['Document', 'Presentation'], + TYPE: ['point', 'highlight'] + }, + { + NAME: 'Image', + CONSTRUCTOR: ImageAnnotator, + VIEWER: ['Image'], + TYPE: ['point'] + } +]; + +class BoxAnnotations { + + /** + * [constructor] + * + * @return {BoxAnnotations} BoxAnnotations instance + */ + constructor() { + this.annotators = ANNOTATORS; + } + + /** + * Returns the available annotators + * + * @return {Array} List of supported annotators + */ + getAnnotators() { + return Array.isArray(this.annotators) ? this.annotators : []; + } + + /** + * Chooses a annotator based on file extension. + * + * @param {Object} file - Box file + * @param {Array} [disabledAnnotators] - List of disabled annotators + * @return {Object} The annotator to use + */ + determineAnnotator(viewer, disabledAnnotators = []) { + return this.annotators.find((annotator) => + !(disabledAnnotators.includes(annotator.NAME)) && annotator.VIEWER.includes(viewer) + ); + } +} + +global.BoxAnnotations = BoxAnnotations; +export default BoxAnnotations; diff --git a/src/lib/annotations/__tests__/Annotator-test.js b/src/lib/annotations/__tests__/Annotator-test.js index 3c68d4e43..51f7bc5bf 100644 --- a/src/lib/annotations/__tests__/Annotator-test.js +++ b/src/lib/annotations/__tests__/Annotator-test.js @@ -16,9 +16,11 @@ describe('lib/annotations/Annotator', () => { fixture.load('annotations/__tests__/Annotator-test.html'); annotator = new Annotator({ - annotatedElement: document.querySelector('.annotated-element'), + canAnnotate: true, + container: document, annotationService: {}, - fileVersionID: 1 + fileVersionID: 1, + options: {} }); stubs.thread = { @@ -63,12 +65,6 @@ describe('lib/annotations/Annotator', () => { stubs = {}; }); - describe('constructor()', () => { - it('should initialize a notification', () => { - expect(annotator.notification).to.not.be.null; - }); - }); - describe('destroy()', () => { it('should unbind custom listeners on thread and unbind DOM listeners', () => { annotator.threads = { @@ -89,13 +85,31 @@ describe('lib/annotations/Annotator', () => { describe('init()', () => { it('should set scale and setup annotations', () => { + const annotatedEl = document.querySelector('.annotated-element'); + sandbox.stub(annotator, 'getAnnotatedEl').returns(annotatedEl); + sandbox.stub(annotator, 'initAnnotationService').returns({}); const scaleStub = sandbox.stub(annotator, 'setScale'); const setupAnnotations = sandbox.stub(annotator, 'setupAnnotations'); + const showAnnotations = sandbox.stub(annotator, 'showAnnotations'); annotator.init(); expect(scaleStub).to.be.called; expect(setupAnnotations).to.be.called; + expect(showAnnotations).to.be.called; + }); + }); + + describe('initAnnotationService()', () => { + it('should initialize the anontation service with the appropriate options', () => { + const options = { + apiHost: 'url', + fileId: 123, + token: 'token' + }; + annotator.canAnnotate = true; + annotator.initAnnotationService(options); + expect(annotator.annotationService).to.not.be.null; }); }); @@ -114,414 +128,412 @@ describe('lib/annotations/Annotator', () => { }); }); - describe('hideAnnotations()', () => { - it('should call hide on each thread in map', () => { - annotator.threads = { - 1: [stubs.thread], - 2: [stubs.thread2, stubs.thread3] - }; - - stubs.threadMock.expects('hide'); - stubs.threadMock2.expects('hide'); - stubs.threadMock3.expects('hide'); - annotator.hideAnnotations(); - }); - }); + describe('setupAnnotations', () => { + it('should initialize thread map and bind DOM listeners', () => { + sandbox.stub(annotator, 'bindDOMListeners'); + sandbox.stub(annotator, 'bindCustomListenersOnService'); + sandbox.stub(annotator, 'addListener'); - describe('hideAnnotationsOnPage()', () => { - it('should call hide on each thread in map on page 1', () => { - annotator.threads = { - 1: [stubs.thread], - 2: [stubs.thread2, stubs.thread3] - }; + annotator.setupAnnotations(); - stubs.threadMock.expects('hide'); - stubs.threadMock2.expects('hide').never(); - stubs.threadMock3.expects('hide').never(); - annotator.hideAnnotationsOnPage('1'); + expect(Object.keys(annotator.threads).length === 0).to.be.true; + expect(annotator.bindDOMListeners).to.be.called; + expect(annotator.bindCustomListenersOnService).to.be.called; }); }); - describe('renderAnnotations()', () => { - it('should call show on each thread', () => { - annotator.threads = { - 1: [stubs.thread], - 2: [stubs.thread2, stubs.thread3] - }; - - stubs.threadMock.expects('show'); - stubs.threadMock2.expects('show'); - stubs.threadMock3.expects('show'); - annotator.renderAnnotations(); - }); - }); + describe('once annotator is initialized', () => { + beforeEach(() => { + const annotatedEl = document.querySelector('.annotated-element'); + sandbox.stub(annotator, 'getAnnotatedEl').returns(annotatedEl); + sandbox.stub(annotator, 'initAnnotationService').returns({}); + sandbox.stub(annotator, 'setupAnnotations'); + sandbox.stub(annotator, 'showAnnotations'); - describe('renderAnnotationsOnPage()', () => { - it('should call show on each thread', () => { - stubs.thread2.location = { page: 2 }; - stubs.thread3.location = { page: 2 }; annotator.threads = { 1: [stubs.thread], 2: [stubs.thread2, stubs.thread3] }; - stubs.threadMock.expects('show'); - stubs.threadMock2.expects('show').never(); - stubs.threadMock3.expects('show').never(); - annotator.renderAnnotationsOnPage('1'); + annotator.init(); }); - }); - describe('setScale()', () => { - it('should set a data-scale attribute on the annotated element', () => { - annotator.setScale(10); - const annotatedEl = document.querySelector('.annotated-element'); - expect(annotatedEl).to.have.attribute('data-scale', '10'); + describe('hideAnnotations()', () => { + it('should call hide on each thread in map', () => { + stubs.threadMock.expects('hide'); + stubs.threadMock2.expects('hide'); + stubs.threadMock3.expects('hide'); + annotator.hideAnnotations(); + }); }); - }); - describe('togglePointModeHandler()', () => { - beforeEach(() => { - stubs.pointMode = sandbox.stub(annotator, 'isInPointMode'); - sandbox.stub(annotator.notification, 'show'); - sandbox.stub(annotator.notification, 'hide'); - sandbox.stub(annotator, 'unbindDOMListeners'); - sandbox.stub(annotator, 'bindDOMListeners'); - sandbox.stub(annotator, 'bindPointModeListeners'); - sandbox.stub(annotator, 'unbindPointModeListeners'); + describe('hideAnnotationsOnPage()', () => { + it('should call hide on each thread in map on page 1', () => { + stubs.threadMock.expects('hide'); + stubs.threadMock2.expects('hide').never(); + stubs.threadMock3.expects('hide').never(); + annotator.hideAnnotationsOnPage('1'); + }); }); - it('should turn annotation mode on if it is off', () => { - const destroyStub = sandbox.stub(annotator, 'destroyPendingThreads'); - stubs.pointMode.returns(false); + describe('renderAnnotations()', () => { + it('should call show on each thread', () => { + stubs.threadMock.expects('show'); + stubs.threadMock2.expects('show'); + stubs.threadMock3.expects('show'); + annotator.renderAnnotations(); + }); + }); - annotator.togglePointModeHandler(); + describe('renderAnnotationsOnPage()', () => { + it('should call show on each thread', () => { + stubs.thread2.location = { page: 2 }; + stubs.thread3.location = { page: 2 }; - const annotatedEl = document.querySelector('.annotated-element'); - expect(destroyStub).to.be.called; - expect(annotator.notification.show).to.be.called; - expect(annotator.emit).to.be.calledWith('pointmodeenter'); - expect(annotatedEl).to.have.class(constants.CLASS_ANNOTATION_POINT_MODE); - expect(annotator.unbindDOMListeners).to.be.called; - expect(annotator.bindPointModeListeners).to.be.called; + stubs.threadMock.expects('show'); + stubs.threadMock2.expects('show').never(); + stubs.threadMock3.expects('show').never(); + annotator.renderAnnotationsOnPage('1'); + }); }); - it('should turn annotation mode off if it is on', () => { - const destroyStub = sandbox.stub(annotator, 'destroyPendingThreads'); - stubs.pointMode.returns(true); + describe('setScale()', () => { + it('should set a data-scale attribute on the annotated element', () => { + annotator.setScale(10); + const annotatedEl = document.querySelector('.annotated-element'); + expect(annotatedEl).to.have.attribute('data-scale', '10'); + }); + }); - annotator.togglePointModeHandler(); + describe('togglePointModeHandler()', () => { + beforeEach(() => { + stubs.pointMode = sandbox.stub(annotator, 'isInPointMode'); + sandbox.stub(annotator.notification, 'show'); + sandbox.stub(annotator.notification, 'hide'); + sandbox.stub(annotator, 'unbindDOMListeners'); + sandbox.stub(annotator, 'bindDOMListeners'); + sandbox.stub(annotator, 'bindPointModeListeners'); + sandbox.stub(annotator, 'unbindPointModeListeners'); + }); - const annotatedEl = document.querySelector('.annotated-element'); - expect(destroyStub).to.be.called; - expect(annotator.notification.hide).to.be.called; - expect(annotator.emit).to.be.calledWith('pointmodeexit'); - expect(annotatedEl).to.not.have.class(constants.CLASS_ANNOTATION_POINT_MODE); - expect(annotator.unbindPointModeListeners).to.be.called; - expect(annotator.bindDOMListeners).to.be.called; - }); - }); + it('should turn annotation mode on if it is off', () => { + const destroyStub = sandbox.stub(annotator, 'destroyPendingThreads'); + stubs.pointMode.returns(false); - describe('setupAnnotations', () => { - it('should initialize thread map and bind DOM listeners', () => { - sandbox.stub(annotator, 'bindDOMListeners'); - sandbox.stub(annotator, 'bindCustomListenersOnService'); - sandbox.stub(annotator, 'addListener'); + annotator.togglePointModeHandler(); - annotator.setupAnnotations(); + const annotatedEl = document.querySelector('.annotated-element'); + expect(destroyStub).to.be.called; + expect(annotator.notification.show).to.be.called; + expect(annotator.emit).to.be.calledWith('pointmodeenter'); + expect(annotatedEl).to.have.class(constants.CLASS_ANNOTATION_POINT_MODE); + expect(annotator.unbindDOMListeners).to.be.called; + expect(annotator.bindPointModeListeners).to.be.called; + }); - expect(Object.keys(annotator.threads).length === 0).to.be.true; - expect(annotator.bindDOMListeners).to.be.called; - expect(annotator.bindCustomListenersOnService).to.be.called; - }); - }); + it('should turn annotation mode off if it is on', () => { + const destroyStub = sandbox.stub(annotator, 'destroyPendingThreads'); + stubs.pointMode.returns(true); - describe('fetchAnnotations', () => { - beforeEach(() => { - annotator.annotationService = { - getThreadMap: () => {} - }; - stubs.serviceMock = sandbox.mock(annotator.annotationService); + annotator.togglePointModeHandler(); - const threadMap = { - someID: [{}, {}], - someID2: [{}] - }; - stubs.threadPromise = Promise.resolve(threadMap); - stubs.serviceMock.expects('getThreadMap').returns(stubs.threadPromise); + const annotatedEl = document.querySelector('.annotated-element'); + expect(destroyStub).to.be.called; + expect(annotator.notification.hide).to.be.called; + expect(annotator.emit).to.be.calledWith('pointmodeexit'); + expect(annotatedEl).to.not.have.class(constants.CLASS_ANNOTATION_POINT_MODE); + expect(annotator.unbindPointModeListeners).to.be.called; + expect(annotator.bindDOMListeners).to.be.called; + }); }); - it('should reset and create a new thread map by fetching annotation data from the server', () => { - stubs.createThread = sandbox.stub(annotator, 'createAnnotationThread'); - stubs.createThread.onFirstCall(); - stubs.createThread.onSecondCall().returns(stubs.thread); - sandbox.stub(annotator, 'bindCustomListenersOnThread'); + describe('fetchAnnotations', () => { + beforeEach(() => { + annotator.annotationService = { + getThreadMap: () => {} + }; + stubs.serviceMock = sandbox.mock(annotator.annotationService); - const result = annotator.fetchAnnotations(); + const threadMap = { + someID: [{}, {}], + someID2: [{}] + }; + stubs.threadPromise = Promise.resolve(threadMap); + stubs.serviceMock.expects('getThreadMap').returns(stubs.threadPromise); + }); - return stubs.threadPromise.then(() => { - expect(Object.keys(annotator.threads).length === 0).to.be.true; - expect(annotator.createAnnotationThread).to.be.calledTwice; - expect(annotator.bindCustomListenersOnThread).to.be.calledOnce; - expect(result).to.be.an.object; + it('should reset and create a new thread map by fetching annotation data from the server', () => { + stubs.createThread = sandbox.stub(annotator, 'createAnnotationThread'); + stubs.createThread.onFirstCall(); + stubs.createThread.onSecondCall().returns(stubs.thread); + sandbox.stub(annotator, 'bindCustomListenersOnThread'); + + const result = annotator.fetchAnnotations(); + return stubs.threadPromise.then(() => { + expect(Object.keys(annotator.threads).length === 0).to.be.true; + expect(annotator.createAnnotationThread).to.be.calledTwice; + expect(annotator.bindCustomListenersOnThread).to.be.calledOnce; + expect(result).to.be.an.object; + }); }); }); - }); - describe('bindCustomListenersOnService', () => { - it('should do nothing if the service does not exist', () => { - annotator.annotationService = { - addListener: sandbox.stub() - }; + describe('bindCustomListenersOnService', () => { + it('should do nothing if the service does not exist', () => { + annotator.annotationService = { + addListener: sandbox.stub() + }; - annotator.bindCustomListenersOnService(); - expect(annotator.annotationService.addListener).to.not.be.called; - }); - - it('should add an event listener', () => { - annotator.annotationService = new AnnotationService({ - apiHost: 'API', - fileId: 1, - token: 'someToken', - canAnnotate: true, - canDelete: true + annotator.bindCustomListenersOnService(); + expect(annotator.annotationService.addListener).to.not.be.called; }); - const addListenerStub = sandbox.stub(annotator.annotationService, 'addListener'); - annotator.bindCustomListenersOnService(); - expect(addListenerStub).to.be.calledWith('annotationerror', sinon.match.func); + it('should add an event listener', () => { + annotator.annotationService = new AnnotationService({ + apiHost: 'API', + fileId: 1, + token: 'someToken', + canAnnotate: true, + canDelete: true + }); + const addListenerStub = sandbox.stub(annotator.annotationService, 'addListener'); + + annotator.bindCustomListenersOnService(); + expect(addListenerStub).to.be.calledWith('annotationerror', sinon.match.func); + }); }); - }); - describe('unbindCustomListenersOnService', () => { - it('should do nothing if the service does not exist', () => { - annotator.annotationService = { - removeListener: sandbox.stub() - }; - annotator.unbindCustomListenersOnService(); - expect(annotator.annotationService.removeListener).to.not.be.called; - }); + describe('unbindCustomListenersOnService', () => { + it('should do nothing if the service does not exist', () => { + annotator.annotationService = { + removeListener: sandbox.stub() + }; - it('should remove an event listener', () => { - annotator.annotationService = new AnnotationService({ - apiHost: 'API', - fileId: 1, - token: 'someToken', - canAnnotate: true, - canDelete: true + annotator.unbindCustomListenersOnService(); + expect(annotator.annotationService.removeListener).to.not.be.called; }); - const removeListenerStub = sandbox.stub(annotator.annotationService, 'removeAllListeners'); - annotator.unbindCustomListenersOnService(); - expect(removeListenerStub).to.be.called; + it('should remove an event listener', () => { + annotator.annotationService = new AnnotationService({ + apiHost: 'API', + fileId: 1, + token: 'someToken', + canAnnotate: true, + canDelete: true + }); + const removeListenerStub = sandbox.stub(annotator.annotationService, 'removeAllListeners'); + + annotator.unbindCustomListenersOnService(); + expect(removeListenerStub).to.be.called; + }); }); - }); - describe('bindCustomListenersOnThread', () => { - it('should bind custom listeners on the thread', () => { - stubs.threadMock.expects('addListener').withArgs('threaddeleted', sinon.match.func); - stubs.threadMock.expects('addListener').withArgs('threadcleanup', sinon.match.func); - annotator.bindCustomListenersOnThread(stubs.thread); + describe('bindCustomListenersOnThread', () => { + it('should bind custom listeners on the thread', () => { + stubs.threadMock.expects('addListener').withArgs('threaddeleted', sinon.match.func); + stubs.threadMock.expects('addListener').withArgs('threadcleanup', sinon.match.func); + annotator.bindCustomListenersOnThread(stubs.thread); + }); }); - }); - describe('unbindCustomListenersOnThread', () => { - it('should unbind custom listeners from the thread', () => { - stubs.threadMock.expects('removeAllListeners').withArgs('threaddeleted'); - stubs.threadMock.expects('removeAllListeners').withArgs('threadcleanup'); - annotator.unbindCustomListenersOnThread(stubs.thread); + describe('unbindCustomListenersOnThread', () => { + it('should unbind custom listeners from the thread', () => { + stubs.threadMock.expects('removeAllListeners').withArgs('threaddeleted'); + stubs.threadMock.expects('removeAllListeners').withArgs('threadcleanup'); + annotator.unbindCustomListenersOnThread(stubs.thread); + }); }); - }); - describe('bindPointModeListeners', () => { - it('should bind point mode click handler', () => { - sandbox.stub(annotator.annotatedElement, 'addEventListener'); - annotator.bindPointModeListeners(); - expect(annotator.annotatedElement.addEventListener).to.be.calledWith('click', annotator.pointClickHandler); + describe('bindPointModeListeners', () => { + it('should bind point mode click handler', () => { + sandbox.stub(annotator.annotatedElement, 'addEventListener'); + annotator.bindPointModeListeners(); + expect(annotator.annotatedElement.addEventListener).to.be.calledWith('click', annotator.pointClickHandler); + }); }); - }); - describe('unbindPointModeListeners', () => { - it('should unbind point mode click handler', () => { - sandbox.stub(annotator.annotatedElement, 'removeEventListener'); - annotator.unbindPointModeListeners(); - expect(annotator.annotatedElement.removeEventListener).to.be.calledWith('click', annotator.pointClickHandler); + describe('unbindPointModeListeners', () => { + it('should unbind point mode click handler', () => { + sandbox.stub(annotator.annotatedElement, 'removeEventListener'); + annotator.unbindPointModeListeners(); + expect(annotator.annotatedElement.removeEventListener).to.be.calledWith('click', annotator.pointClickHandler); + }); }); - }); - describe('pointClickHandler', () => { - const event = { - stopPropagation: () => {} - }; + describe('pointClickHandler', () => { + const event = { + stopPropagation: () => {} + }; - beforeEach(() => { - stubs.destroy = sandbox.stub(annotator, 'destroyPendingThreads'); - stubs.create = sandbox.stub(annotator, 'createAnnotationThread'); - stubs.getLocation = sandbox.stub(annotator, 'getLocationFromEvent'); - sandbox.stub(annotator, 'bindCustomListenersOnThread'); - sandbox.stub(annotator, 'togglePointModeHandler'); - }); + beforeEach(() => { + stubs.destroy = sandbox.stub(annotator, 'destroyPendingThreads'); + stubs.create = sandbox.stub(annotator, 'createAnnotationThread'); + stubs.getLocation = sandbox.stub(annotator, 'getLocationFromEvent'); + sandbox.stub(annotator, 'bindCustomListenersOnThread'); + sandbox.stub(annotator, 'togglePointModeHandler'); + }); - it('should not do anything if there are pending threads', () => { - stubs.destroy.returns(true); - stubs.create.returns(stubs.thread); + it('should not do anything if there are pending threads', () => { + stubs.destroy.returns(true); + stubs.create.returns(stubs.thread); - stubs.threadMock.expects('show').never(); - annotator.pointClickHandler(event); + stubs.threadMock.expects('show').never(); + annotator.pointClickHandler(event); - expect(annotator.getLocationFromEvent).to.not.be.called; - expect(annotator.bindCustomListenersOnThread).to.not.be.called; - expect(annotator.togglePointModeHandler).to.not.be.called; - }); + expect(annotator.getLocationFromEvent).to.not.be.called; + expect(annotator.bindCustomListenersOnThread).to.not.be.called; + expect(annotator.togglePointModeHandler).to.not.be.called; + }); - it('should not do anything if thread is invalid', () => { - stubs.destroy.returns(false); + it('should not do anything if thread is invalid', () => { + stubs.destroy.returns(false); - stubs.threadMock.expects('show').never(); - annotator.pointClickHandler(event); + stubs.threadMock.expects('show').never(); + annotator.pointClickHandler(event); - expect(annotator.getLocationFromEvent).to.be.called; - expect(annotator.togglePointModeHandler).to.be.called; - expect(annotator.bindCustomListenersOnThread).to.not.be.called; - }); + expect(annotator.getLocationFromEvent).to.be.called; + expect(annotator.togglePointModeHandler).to.be.called; + expect(annotator.bindCustomListenersOnThread).to.not.be.called; + }); - it('should not create a thread if a location object cannot be inferred from the event', () => { - stubs.destroy.returns(false); - stubs.getLocation.returns(null); - stubs.create.returns(stubs.thread); + it('should not create a thread if a location object cannot be inferred from the event', () => { + stubs.destroy.returns(false); + stubs.getLocation.returns(null); + stubs.create.returns(stubs.thread); - stubs.threadMock.expects('show').never(); - annotator.pointClickHandler(event); + stubs.threadMock.expects('show').never(); + annotator.pointClickHandler(event); - expect(annotator.getLocationFromEvent).to.be.called; - expect(annotator.bindCustomListenersOnThread).to.not.be.called; - expect(annotator.togglePointModeHandler).to.be.called; - }); + expect(annotator.getLocationFromEvent).to.be.called; + expect(annotator.bindCustomListenersOnThread).to.not.be.called; + expect(annotator.togglePointModeHandler).to.be.called; + }); - it('should create, show, and bind listeners to a thread', () => { - stubs.destroy.returns(false); - stubs.getLocation.returns({}); - stubs.create.returns(stubs.thread); + it('should create, show, and bind listeners to a thread', () => { + stubs.destroy.returns(false); + stubs.getLocation.returns({}); + stubs.create.returns(stubs.thread); - stubs.threadMock.expects('show'); - annotator.pointClickHandler(event); + stubs.threadMock.expects('show'); + annotator.pointClickHandler(event); - expect(annotator.getLocationFromEvent).to.be.called; - expect(annotator.bindCustomListenersOnThread).to.be.called; - expect(annotator.togglePointModeHandler).to.be.called; + expect(annotator.getLocationFromEvent).to.be.called; + expect(annotator.bindCustomListenersOnThread).to.be.called; + expect(annotator.togglePointModeHandler).to.be.called; + }); }); - }); - describe('addToThreadMap', () => { - it('should add valid threads to the thread map', () => { - stubs.thread.location = { page: 2 }; - stubs.thread2.location = { page: 3 }; - stubs.thread3.location = { page: 2 }; + describe('addToThreadMap', () => { + it('should add valid threads to the thread map', () => { + stubs.thread.location = { page: 2 }; + stubs.thread2.location = { page: 3 }; + stubs.thread3.location = { page: 2 }; - annotator.init(); - annotator.addThreadToMap(stubs.thread); + annotator.threads = {}; + annotator.addThreadToMap(stubs.thread); - expect(annotator.threads).to.deep.equal({ - 2: [stubs.thread] - }); + expect(annotator.threads).to.deep.equal({ + 2: [stubs.thread] + }); - annotator.addThreadToMap(stubs.thread2); - annotator.addThreadToMap(stubs.thread3); + annotator.addThreadToMap(stubs.thread2); + annotator.addThreadToMap(stubs.thread3); - expect(annotator.threads).to.deep.equal({ - 2: [stubs.thread, stubs.thread3], - 3: [stubs.thread2] + expect(annotator.threads).to.deep.equal({ + 2: [stubs.thread, stubs.thread3], + 3: [stubs.thread2] + }); }); }); - }); - describe('isInPointMode', () => { - it('should return whether the annotator is in point mode or not', () => { - annotator.annotatedElement.classList.add(constants.CLASS_ANNOTATION_POINT_MODE); - expect(annotator.isInPointMode()).to.be.true; + describe('isInPointMode', () => { + it('should return whether the annotator is in point mode or not', () => { + annotator.annotatedElement.classList.add(constants.CLASS_ANNOTATION_POINT_MODE); + expect(annotator.isInPointMode()).to.be.true; - annotator.annotatedElement.classList.remove(constants.CLASS_ANNOTATION_POINT_MODE); - expect(annotator.isInPointMode()).to.be.false; - }); - }); - - describe('destroyPendingThreads', () => { - beforeEach(() => { - stubs.thread = { - location: { page: 2 }, - type: 'type', - state: constants.ANNOTATION_STATE_PENDING, - destroy: () => {}, - unbindCustomListenersOnThread: () => {}, - removeAllListeners: () => {} - }; - stubs.threadMock = sandbox.mock(stubs.thread); - }); - - it('should destroy and return true if there are any pending threads', () => { - annotator.init(); - annotator.addThreadToMap(stubs.thread); - stubs.threadMock.expects('destroy'); - const destroyed = annotator.destroyPendingThreads(); - expect(destroyed).to.equal(true); - }); - - it('should not destroy and return false if there are no threads', () => { - annotator.init(); - stubs.threadMock.expects('destroy').never(); - const destroyed = annotator.destroyPendingThreads(); - expect(destroyed).to.equal(false); + annotator.annotatedElement.classList.remove(constants.CLASS_ANNOTATION_POINT_MODE); + expect(annotator.isInPointMode()).to.be.false; + }); }); - it('should not destroy and return false if the threads are not pending', () => { - stubs.thread.state = 'NOT_PENDING'; - annotator.init(); - annotator.addThreadToMap(stubs.thread); - stubs.threadMock.expects('destroy').never(); - const destroyed = annotator.destroyPendingThreads(); - expect(destroyed).to.equal(false); - }); + describe('destroyPendingThreads', () => { + beforeEach(() => { + stubs.thread = { + location: { page: 2 }, + type: 'type', + state: constants.ANNOTATION_STATE_PENDING, + destroy: () => {}, + unbindCustomListenersOnThread: () => {}, + removeAllListeners: () => {} + }; + stubs.threadMock = sandbox.mock(stubs.thread); + }); - it('should destroy only pending threads, and return true', () => { - stubs.thread.state = 'NOT_PENDING'; - const pendingThread = { - location: { page: 2 }, - type: 'type', - state: constants.ANNOTATION_STATE_PENDING, - destroy: () => {}, - unbindCustomListenersOnThread: () => {}, - removeAllListeners: () => {} - }; - stubs.pendingMock = sandbox.mock(pendingThread); + it('should destroy and return true if there are any pending threads', () => { + annotator.init(); + annotator.addThreadToMap(stubs.thread); + stubs.threadMock.expects('destroy'); + const destroyed = annotator.destroyPendingThreads(); + expect(destroyed).to.equal(true); + }); - annotator.init(); - annotator.addThreadToMap(stubs.thread); - annotator.addThreadToMap(pendingThread); + it('should not destroy and return false if there are no threads', () => { + annotator.init(); + stubs.threadMock.expects('destroy').never(); + const destroyed = annotator.destroyPendingThreads(); + expect(destroyed).to.equal(false); + }); - stubs.threadMock.expects('destroy').never(); - stubs.pendingMock.expects('destroy'); - const destroyed = annotator.destroyPendingThreads(); + it('should not destroy and return false if the threads are not pending', () => { + stubs.thread.state = 'NOT_PENDING'; + annotator.init(); + annotator.addThreadToMap(stubs.thread); + stubs.threadMock.expects('destroy').never(); + const destroyed = annotator.destroyPendingThreads(); + expect(destroyed).to.equal(false); + }); - expect(destroyed).to.equal(true); + it('should destroy only pending threads, and return true', () => { + stubs.thread.state = 'NOT_PENDING'; + const pendingThread = { + location: { page: 2 }, + type: 'type', + state: constants.ANNOTATION_STATE_PENDING, + destroy: () => {}, + unbindCustomListenersOnThread: () => {}, + removeAllListeners: () => {} + }; + stubs.pendingMock = sandbox.mock(pendingThread); + + annotator.init(); + annotator.addThreadToMap(stubs.thread); + annotator.addThreadToMap(pendingThread); + + stubs.threadMock.expects('destroy').never(); + stubs.pendingMock.expects('destroy'); + const destroyed = annotator.destroyPendingThreads(); + + expect(destroyed).to.equal(true); + }); }); - }); - describe('handleValidationError()', () => { - it('should do nothing if a validation notification was already displayed', () => { - annotator.validationErrorDisplayed = true; - stubs.showNotification = sandbox.stub(annotator.notification, 'show'); - annotator.handleValidationError(); - expect(stubs.showNotification).to.not.be.called; - expect(annotator.validationErrorDisplayed).to.be.true; - }); + describe('handleValidationError()', () => { + it('should do nothing if a validation notification was already displayed', () => { + annotator.validationErrorDisplayed = true; + stubs.showNotification = sandbox.stub(annotator.notification, 'show'); + annotator.handleValidationError(); + expect(stubs.showNotification).to.not.be.called; + expect(annotator.validationErrorDisplayed).to.be.true; + }); - it('should display validation error notification on first error', () => { - annotator.validationErrorDisplayed = false; - stubs.showNotification = sandbox.stub(annotator.notification, 'show'); - annotator.handleValidationError(); - expect(stubs.showNotification).to.be.called; - expect(annotator.validationErrorDisplayed).to.be.true; + it('should display validation error notification on first error', () => { + annotator.validationErrorDisplayed = false; + stubs.showNotification = sandbox.stub(annotator.notification, 'show'); + annotator.handleValidationError(); + expect(stubs.showNotification).to.be.called; + expect(annotator.validationErrorDisplayed).to.be.true; + }); }); }); }); diff --git a/src/lib/annotations/__tests__/BoxAnnotations-test.js b/src/lib/annotations/__tests__/BoxAnnotations-test.js new file mode 100644 index 000000000..ace2f55c2 --- /dev/null +++ b/src/lib/annotations/__tests__/BoxAnnotations-test.js @@ -0,0 +1,50 @@ +/* eslint-disable no-unused-expressions */ +import BoxAnnotations from '../BoxAnnotations'; + +let loader; +const sandbox = sinon.sandbox.create(); + +describe('lib/annotators/BoxAnnotations', () => { + beforeEach(() => { + loader = new BoxAnnotations(); + }); + + afterEach(() => { + sandbox.verifyAndRestore(); + + if (typeof loader.destroy === 'function') { + loader.destroy(); + } + + loader = null; + }); + + describe('getAnnotators()', () => { + it('should return the loader\'s annotators', () => { + expect(loader.getAnnotators()).to.deep.equal(loader.annotators); + }); + + it('should return an empty array if the loader doesn\'t have annotators', () => { + loader.annotators = []; + expect(loader.getAnnotators()).to.deep.equal([]); + }); + }); + + describe('determineAnnotator()', () => { + it('should choose the first annotator that matches the viewer', () => { + const viewer = 'Document'; + const annotator = loader.determineAnnotator(viewer); + expect(annotator.NAME).to.equal(viewer); + }); + + it('should not choose a disabled annotator', () => { + const annotator = loader.determineAnnotator('Image', ['Image']); + expect(annotator).to.be.undefined; + }); + + it('should not return a annotator if no matching annotator is found', () => { + const annotator = loader.determineAnnotator('Swf'); + expect(annotator).to.be.undefined; + }); + }); +}); diff --git a/src/lib/annotations/doc/DocAnnotator.js b/src/lib/annotations/doc/DocAnnotator.js index 4d4ca623d..436dd88c6 100644 --- a/src/lib/annotations/doc/DocAnnotator.js +++ b/src/lib/annotations/doc/DocAnnotator.js @@ -27,6 +27,16 @@ class DocAnnotator extends Annotator { // Abstract Implementations //-------------------------------------------------------------------------- + /** + * Determines the annotated element in the viewer + * + * @param {HTMLElement} containerEl - Container element for the viewer + * @return {HTMLElement} Annotated element in the viewer + */ + getAnnotatedEl(containerEl) { + return containerEl.querySelector('.bp-doc'); + } + /** * Returns an annotation location on a document from the DOM event or null * if no correct annotation location can be inferred from the event. For diff --git a/src/lib/annotations/doc/__tests__/DocAnnotator-test.html b/src/lib/annotations/doc/__tests__/DocAnnotator-test.html index 5ed98fbe4..9e0fcdc12 100644 --- a/src/lib/annotations/doc/__tests__/DocAnnotator-test.html +++ b/src/lib/annotations/doc/__tests__/DocAnnotator-test.html @@ -1 +1 @@ -
+
diff --git a/src/lib/annotations/doc/__tests__/DocAnnotator-test.js b/src/lib/annotations/doc/__tests__/DocAnnotator-test.js index 9f90e8ff3..53c9d5dc8 100644 --- a/src/lib/annotations/doc/__tests__/DocAnnotator-test.js +++ b/src/lib/annotations/doc/__tests__/DocAnnotator-test.js @@ -27,10 +27,14 @@ describe('lib/annotations/doc/DocAnnotator', () => { sandbox.stub(Browser, 'isMobile').returns(false); annotator = new DocAnnotator({ - annotatedElement: document.querySelector('.annotated-element'), + canAnnotate: true, + container: document, annotationService: {}, - fileVersionID: 1 + fileVersionID: 1, + options: {} }); + annotator.annotatedElement = annotator.getAnnotatedEl(document); + annotator.annotationService = {}; stubs.getPage = sandbox.stub(docAnnotatorUtil, 'getPageElAndPageNumber'); }); @@ -44,6 +48,12 @@ describe('lib/annotations/doc/DocAnnotator', () => { stubs = {}; }); + describe('getAnnotatedEl()', () => { + it('should return the annotated element as the document', () => { + expect(annotator.annotatedElement).to.not.be.null; + }); + }); + describe('getLocationFromEvent()', () => { const x = 100; const y = 200; diff --git a/src/lib/annotations/image/ImageAnnotator.js b/src/lib/annotations/image/ImageAnnotator.js index 843de1788..545fca256 100644 --- a/src/lib/annotations/image/ImageAnnotator.js +++ b/src/lib/annotations/image/ImageAnnotator.js @@ -12,6 +12,16 @@ class ImageAnnotator extends Annotator { // Abstract Implementations //-------------------------------------------------------------------------- + /** + * Determines the annotated element in the viewer + * + * @param {HTMLElement} containerEl - Container element for the viewer + * @return {HTMLElement} Annotated element in the viewer + */ + getAnnotatedEl(containerEl) { + return containerEl.querySelector('.bp-image'); + } + /** * Returns an annotation location on an image from the DOM event or null * if no correct annotation location can be inferred from the event. For diff --git a/src/lib/annotations/image/__tests__/ImageAnnotator-test.html b/src/lib/annotations/image/__tests__/ImageAnnotator-test.html index 5c2c3bab2..96e739ba9 100644 --- a/src/lib/annotations/image/__tests__/ImageAnnotator-test.html +++ b/src/lib/annotations/image/__tests__/ImageAnnotator-test.html @@ -1,4 +1,4 @@ -
+
diff --git a/src/lib/annotations/image/__tests__/ImageAnnotator-test.js b/src/lib/annotations/image/__tests__/ImageAnnotator-test.js index 767656382..b70a4e36f 100644 --- a/src/lib/annotations/image/__tests__/ImageAnnotator-test.js +++ b/src/lib/annotations/image/__tests__/ImageAnnotator-test.js @@ -16,10 +16,14 @@ describe('lib/annotations/image/ImageAnnotator', () => { fixture.load('annotations/image/__tests__/ImageAnnotator-test.html'); annotator = new ImageAnnotator({ - annotatedElement: document.querySelector('.annotated-element'), + canAnnotate: true, + container: document, annotationService: {}, - fileVersionID: 1 + fileVersionID: 1, + options: {} }); + annotator.annotatedElement = annotator.getAnnotatedEl(document); + annotator.annotationService = {}; }); afterEach(() => { @@ -28,15 +32,16 @@ describe('lib/annotations/image/ImageAnnotator', () => { annotator = null; }); + describe('getAnnotatedEl()', () => { + it('should return the annotated element as the document', () => { + expect(annotator.annotatedElement).to.not.be.null; + }); + }); + describe('getLocationFromEvent()', () => { it('should not return a location if image isn\'t inside viewer', () => { - const tempAnnotator = new ImageAnnotator({ - annotatedElement: document.createElement('div'), - annotationService: {}, - fileVersionID: 1 - }); - - const location = tempAnnotator.getLocationFromEvent({}); + annotator.annotatedElement = document.createElement('div'); + const location = annotator.getLocationFromEvent({}); expect(location).to.be.null; }); diff --git a/src/lib/ui.js b/src/lib/ui.js index 454715b6d..be0f4f146 100644 --- a/src/lib/ui.js +++ b/src/lib/ui.js @@ -9,7 +9,6 @@ import { CLASS_PREVIEW_LOADED, SELECTOR_BOX_PREVIEW_CONTAINER, SELECTOR_BOX_PREVIEW, - SELECTOR_BOX_PREVIEW_BTN_ANNOTATE, SELECTOR_BOX_PREVIEW_BTN_PRINT, SELECTOR_BOX_PREVIEW_BTN_DOWNLOAD, SELECTOR_BOX_PREVIEW_BTN_LOADING_DOWNLOAD, @@ -208,23 +207,6 @@ export function showNavigation(id, collection) { } } -/** - * Shows the point annotate button if the viewers implement annotations - * - * @param {Function} handler - Annotation button handler - * @return {void} - */ -export function showAnnotateButton(handler) { - const annotateButtonEl = container.querySelector(SELECTOR_BOX_PREVIEW_BTN_ANNOTATE); - if (!annotateButtonEl) { - return; - } - - annotateButtonEl.title = __('annotation_point_toggle'); - annotateButtonEl.classList.remove(CLASS_HIDDEN); - annotateButtonEl.addEventListener('click', handler); -} - /** * Shows the print button if the viewers implement print * diff --git a/src/lib/viewers/BaseViewer.js b/src/lib/viewers/BaseViewer.js index cfd6127a7..e80f470f9 100644 --- a/src/lib/viewers/BaseViewer.js +++ b/src/lib/viewers/BaseViewer.js @@ -12,15 +12,20 @@ import { prefetchAssets, createAssetUrlCreator } from '../util'; +import { checkPermission } from '../file'; import Browser from '../Browser'; import { + PERMISSION_ANNOTATE, CLASS_FULLSCREEN, + CLASS_HIDDEN, CLASS_BOX_PREVIEW_MOBILE, SELECTOR_BOX_PREVIEW, + SELECTOR_BOX_PREVIEW_BTN_ANNOTATE, STATUS_SUCCESS, STATUS_VIEWABLE } from '../constants'; +const ANNOTATIONS_JS = ['annotations.js']; const LOAD_TIMEOUT_MS = 180000; // 3m const RESIZE_WAIT_TIME_IN_MILLIS = 300; @@ -64,6 +69,19 @@ class BaseViewer extends EventEmitter { if (Browser.isMobile()) { this.containerEl.classList.add(CLASS_BOX_PREVIEW_MOBILE); } + + // Attempts to load annotations assets and initializes annotations if + // the assets are available, the showAnnotations flag is true, and the + // expiring embed is not a shared link + // TODO(@spramod): Determine the expected behavior on shared links + const { showAnnotations, sharedLink } = this.options; + if (showAnnotations && !sharedLink) { + this.loadAssets(ANNOTATIONS_JS) + .then(() => { + this.annotationsLoaded = true; + }) + .catch(this.handleAssetError); + } } /** @@ -255,6 +273,12 @@ class BaseViewer extends EventEmitter { this.addListener('togglepointannotationmode', () => { this.annotator.togglePointModeHandler(); }); + + this.addListener('load', () => { + if (this.annotationsLoaded && this.options.showAnnotations) { + this.loadAnnotator(); + } + }); } /** @@ -483,6 +507,118 @@ class BaseViewer extends EventEmitter { const status = RepStatus.getStatus(representation); return status === STATUS_SUCCESS || status === STATUS_VIEWABLE; } + + //-------------------------------------------------------------------------- + // Annotations + //-------------------------------------------------------------------------- + + /** + * Loads the appropriate annotator and loads the file's annotations + * + * @protected + * @return {void} + */ + loadAnnotator() { + /* global BoxAnnotations */ + this.loader = new BoxAnnotations(); + + this.annotatorLoader = this.loader.determineAnnotator(this.options.viewer.NAME); + if (this.annotatorLoader) { + this.annotationTypes = this.annotatorLoader.TYPE; + + if (this.isAnnotatable()) { + const { file } = this.options; + if (checkPermission(file, PERMISSION_ANNOTATE) && !Browser.isMobile()) { + this.showAnnotateButton(this.getPointModeClickHandler()); + } + this.initAnnotations(); + } + } + } + + /** + * Initializes annotations. + * + * @protected + * @return {void} + */ + initAnnotations() { + const { apiHost, file, location, token } = this.options; + const fileVersionID = file.file_version.id; + + // Users can currently only view annotations on mobile + const canAnnotate = checkPermission(file, PERMISSION_ANNOTATE) && !Browser.isMobile(); + const annotationOptions = { + apiHost, + fileId: file.id, + token + }; + + // Construct and init annotator + this.annotator = new this.annotatorLoader.CONSTRUCTOR({ + canAnnotate, + container: this.options.container, + options: annotationOptions, + fileVersionID, + locale: location.locale + }); + this.annotator.init(); + } + + /** + * Returns whether or not viewer is annotatable. If an optional type is + * passed in, we check if that type of annotation is allowed. + * + * @param {string} [type] - Type of annotation + * @return {boolean} Whether or not viewer is annotatable + */ + isAnnotatable(type) { + if (type && this.annotationTypes) { + const supportedType = this.annotationTypes.some((annotationType) => { + return type === annotationType; + }); + + if (!supportedType) { + return false; + } + } + + // Respect viewer-specific annotation option if it is set + const viewerAnnotations = this.getViewerOption('annotations'); + if (typeof viewerAnnotations === 'boolean') { + return viewerAnnotations; + } + + // Otherwise, use global preview annotation option + return this.options.showAnnotations; + } + + /** + * Shows the point annotate button if the viewers implement annotations + * + * @param {Function} handler - Annotation button handler + * @return {void} + */ + showAnnotateButton(handler) { + const { container } = this.options; + const annotateButtonEl = container.querySelector(SELECTOR_BOX_PREVIEW_BTN_ANNOTATE); + if (!annotateButtonEl) { + return; + } + + annotateButtonEl.title = __('annotation_point_toggle'); + annotateButtonEl.classList.remove(CLASS_HIDDEN); + annotateButtonEl.addEventListener('click', handler); + } + + /** + * Returns click handler for toggling point annotation mode. + * + * @return {Function|null} Click handler + */ + /* eslint-disable no-unused-vars */ + getPointModeClickHandler(containerEl) {} + /* eslint-enable no-unused-vars */ } export default BaseViewer; diff --git a/src/lib/viewers/__tests__/BaseViewer-test.js b/src/lib/viewers/__tests__/BaseViewer-test.js index 5fec82ae7..8a289335d 100644 --- a/src/lib/viewers/__tests__/BaseViewer-test.js +++ b/src/lib/viewers/__tests__/BaseViewer-test.js @@ -5,9 +5,12 @@ import Browser from '../../Browser'; import RepStatus from '../../RepStatus'; import fullscreen from '../../Fullscreen'; import * as util from '../../util'; +import * as file from '../../file'; +import * as constants from '../../constants'; let base; let containerEl; +let stubs = {}; const sandbox = sinon.sandbox.create(); describe('lib/viewers/BaseViewer', () => { @@ -38,6 +41,8 @@ describe('lib/viewers/BaseViewer', () => { describe('setup()', () => { it('should set options, a container, bind event listeners, and set timeout', () => { sandbox.stub(base, 'addCommonListeners'); + sandbox.stub(base, 'loadAssets').returns(Promise.resolve()); + base.options.showAnnotations = true; base.setup(); @@ -45,20 +50,37 @@ describe('lib/viewers/BaseViewer', () => { container: containerEl, file: { id: '0' - } + }, + showAnnotations: true }); expect(base.containerEl).to.have.class('bp'); expect(base.addCommonListeners).to.be.called; expect(base.loadTimeout).to.be.a.number; + expect(base.loadAssets).to.be.called; }); it('should add a mobile class to the container if on mobile', () => { sandbox.stub(Browser, 'isMobile').returns(true); + sandbox.stub(base, 'loadAssets').returns(Promise.resolve()); base.setup(); const container = document.querySelector('.bp'); expect(container).to.have.class('bp-is-mobile'); }); + + it('should not load annotations assets if global preview showAnnotations option is false', () => { + sandbox.stub(base, 'addCommonListeners'); + sandbox.stub(base, 'loadAssets'); + base.options.showAnnotations = false; + expect(base.loadAssets).to.not.be.called; + }); + + it('should not load annotations assets if expiring embed is a shared link', () => { + sandbox.stub(base, 'addCommonListeners'); + sandbox.stub(base, 'loadAssets'); + base.options.sharedLink = 'url'; + expect(base.loadAssets).to.not.be.called; + }); }); describe('debouncedResizeHandler()', () => { @@ -224,6 +246,7 @@ describe('lib/viewers/BaseViewer', () => { expect(fullscreen.addListener).to.be.calledWith('exit', sinon.match.func); expect(document.defaultView.addEventListener).to.be.calledWith('resize', base.debouncedResizeHandler); expect(base.addListener).to.be.calledWith('togglepointannotationmode', sinon.match.func); + expect(base.addListener).to.be.calledWith('load', sinon.match.func); }); }); @@ -265,6 +288,8 @@ describe('lib/viewers/BaseViewer', () => { }); it('should cleanup the base viewer', () => { + sandbox.stub(base, 'loadAssets').returns(Promise.resolve()); + sandbox.stub(base, 'loadAnnotator'); base.setup(); sandbox.mock(fullscreen).expects('removeAllListeners'); @@ -317,7 +342,6 @@ describe('lib/viewers/BaseViewer', () => { }); describe('Pinch to Zoom Handlers', () => { - let stubs = {}; let event = {}; beforeEach(() => { @@ -327,6 +351,8 @@ describe('lib/viewers/BaseViewer', () => { id: '123' } }); + sandbox.stub(base, 'loadAssets').returns(Promise.resolve()); + sandbox.stub(base, 'loadAnnotator'); base.setup(); event = { preventDefault: sandbox.stub(), @@ -562,4 +588,141 @@ describe('lib/viewers/BaseViewer', () => { expect(base.isRepresentationReady(representation)).to.be.false; }); }); + + describe('loadAnnotator()', () => { + beforeEach(() => { + base.options.viewer = { + NAME: Document + }; + + stubs.annotatorLoader = { + TYPE: ['point', 'highlight'] + }; + stubs.isAnnotatable = sandbox.stub(base, 'isAnnotatable').returns(true); + sandbox.stub(base, 'initAnnotations'); + sandbox.stub(base, 'showAnnotateButton'); + stubs.checkPermission = sandbox.stub(file, 'checkPermission').returns(false); + }); + + it('should load the appropriate annotator for the current viewer', () => { + class BoxAnnotations { + determineAnnotator() { + return stubs.annotatorLoader; + } + } + sandbox.stub(Browser, 'isMobile').returns(false); + stubs.checkPermission.returns(true); + + window.BoxAnnotations = BoxAnnotations; + base.loadAnnotator(); + expect(base.initAnnotations).to.be.called; + expect(base.showAnnotateButton).to.be.called; + }); + + it('should not display the point annotation button if the user does not have the appropriate permissions', () => { + class BoxAnnotations { + determineAnnotator() {} + } + window.BoxAnnotations = BoxAnnotations; + + base.loadAnnotator(); + expect(base.initAnnotations).to.not.be.called; + expect(base.showAnnotateButton).to.not.be.called; + }); + + it('should not load an annotator if no loader was found', () => { + class BoxAnnotations { + determineAnnotator() {} + } + window.BoxAnnotations = BoxAnnotations; + base.loadAnnotator(); + expect(base.initAnnotations).to.not.be.called; + expect(base.showAnnotateButton).to.not.be.called; + }); + + it('should not load an annotator if the viewer is not annotatable', () => { + class BoxAnnotations { + determineAnnotator() { + return stubs.annotatorLoader; + } + } + window.BoxAnnotations = BoxAnnotations; + stubs.isAnnotatable.returns(false); + base.loadAnnotator(); + expect(base.initAnnotations).to.not.be.called; + expect(base.showAnnotateButton).to.not.be.called; + }); + }); + + describe('initAnnotations()', () => { + it('should initialize the annotator', () => { + base.options = { + container: document, + file: { + file_version: { + id: 123 + } + }, + location: { + locale: 'en-US' + } + }; + base.annotator = { + init: sandbox.stub() + }; + base.annotatorLoader = { + CONSTRUCTOR: sandbox.stub().returns(base.annotator) + }; + base.initAnnotations(); + expect(base.annotator.init).to.be.called; + }); + }); + + describe('isAnnotatable()', () => { + beforeEach(() => { + stubs.getViewerOption = sandbox.stub(base, 'getViewerOption'); + stubs.getViewerOption.withArgs('annotations').returns(true); + base.annotationTypes = ['point', 'highlight']; + }); + + it('should return false if the type is not supported by the viewer', () => { + expect(base.isAnnotatable('drawing')).to.equal(false); + }); + + it('should return true if viewer option is set to true', () => { + expect(base.isAnnotatable('point')).to.equal(true); + stubs.getViewerOption.withArgs('annotations').returns(false); + expect(base.isAnnotatable('point')).to.equal(false); + }); + + it('should use the global show annotationsBoolean if the viewer param is not specified', () => { + base.annotationTypes = null; + stubs.getViewerOption.withArgs('annotations').returns(null); + base.options.showAnnotations = true; + expect(base.isAnnotatable()).to.equal(true); + + base.options.showAnnotations = false; + expect(base.isAnnotatable()).to.equal(false); + }); + }); + + describe('showAnnotateButton()', () => { + it('should set up and show annotate button', () => { + const buttonEl = document.createElement('div'); + buttonEl.classList.add('bp-btn-annotate'); + buttonEl.classList.add(constants.CLASS_HIDDEN); + base.options = { + container: document, + file: { + id: 123 + } + }; + containerEl.appendChild(buttonEl); + sandbox.mock(buttonEl).expects('addEventListener').withArgs('click', base.handler); + + base.showAnnotateButton(base.handler); + expect(buttonEl.title).to.equal('Point annotation mode'); + expect(buttonEl.classList.contains(constants.CLASS_HIDDEN)).to.be.false; + }); + }); }); diff --git a/src/lib/viewers/doc/DocBaseViewer.js b/src/lib/viewers/doc/DocBaseViewer.js index bfb3a9e44..aaaaeb37d 100644 --- a/src/lib/viewers/doc/DocBaseViewer.js +++ b/src/lib/viewers/doc/DocBaseViewer.js @@ -1,11 +1,9 @@ import autobind from 'autobind-decorator'; import throttle from 'lodash.throttle'; -import AnnotationService from '../../annotations/AnnotationService'; import BaseViewer from '../BaseViewer'; import Browser from '../../Browser'; import cache from '../../Cache'; import Controls from '../../Controls'; -import DocAnnotator from '../../annotations/doc/DocAnnotator'; import DocFindBar from './DocFindBar'; import fullscreen from '../../Fullscreen'; import Popup from '../../Popup'; @@ -14,7 +12,6 @@ import { CLASS_HIDDEN, CLASS_IS_SCROLLABLE, DOC_STATIC_ASSETS_VERSION, - PERMISSION_ANNOTATE, PERMISSION_DOWNLOAD, PRELOAD_REP_NAME } from '../../constants'; @@ -511,28 +508,6 @@ class DocBaseViewer extends BaseViewer { this.pdfViewer.currentScaleValue = scale; } - /** - * Returns whether or not viewer is annotatable. If an optional type is - * passed in, we check if that type of annotation is allowed. - * - * @param {string} [type] - Type of annotation - * @return {boolean} Whether or not viewer is annotatable - */ - isAnnotatable(type) { - if (typeof type === 'string' && type !== 'point' && type !== 'highlight') { - return false; - } - - // Respect viewer-specific annotation option if it is set - const viewerAnnotations = this.getViewerOption('annotations'); - if (typeof viewerAnnotations === 'boolean') { - return viewerAnnotations; - } - - // Otherwise, use global preview annotation option - return this.options.showAnnotations; - } - /** * Returns click handler for toggling point annotation mode. * @@ -685,47 +660,22 @@ class DocBaseViewer extends BaseViewer { /** * Initializes annotations. * + * @protected * @return {void} - * @private */ initAnnotations() { this.setupPageIds(); - - const { apiHost, file, location, token, sharedLink } = this.options; - const fileVersionID = file.file_version.id; - - // Do not initialize annotations for shared links - // TODO(@spramod): Determine the expected behavior on shared links - if (sharedLink) { - return; - } - - // Users can currently only view annotations on mobile - const canAnnotate = checkPermission(file, PERMISSION_ANNOTATE) && !Browser.isMobile(); - const annotationService = new AnnotationService({ - apiHost, - fileId: file.id, - token, - canAnnotate - }); - - // Construct and init annotator - this.annotator = new DocAnnotator({ - annotatedElement: this.docEl, - annotationService, - fileVersionID, - locale: location.locale - }); - this.annotator.init(); - this.annotator.setScale(this.pdfViewer.currentScale); + super.initAnnotations(); // Disable controls during point annotation mode + /* istanbul ignore next */ this.annotator.addListener('pointmodeenter', () => { if (this.controls) { this.controls.disable(); } }); + /* istanbul ignore next */ this.annotator.addListener('pointmodeexit', () => { if (this.controls) { this.controls.enable(); @@ -1057,11 +1007,6 @@ class DocBaseViewer extends BaseViewer { pagesinitHandler() { this.pdfViewer.currentScaleValue = 'auto'; - // Initialize annotations before other UI - if (this.isAnnotatable()) { - this.initAnnotations(); - } - this.loadUI(); this.checkPaginationButtons(); @@ -1090,17 +1035,6 @@ class DocBaseViewer extends BaseViewer { pagerenderedHandler(event) { const pageNumber = event.detail ? event.detail.pageNumber : undefined; - // Render annotations by page - if (this.annotator) { - // We should get a page number from pdfViewer most of the time - if (pageNumber) { - this.annotator.renderAnnotationsOnPage(pageNumber); - // If not, we re-render all annotations to be safe - } else { - this.annotator.renderAnnotations(); - } - } - // If text layer is disabled due to permissions, we still want to show annotations if (PDFJS.disableTextLayer) { this.textlayerrenderedHandler(); @@ -1126,13 +1060,7 @@ class DocBaseViewer extends BaseViewer { * @private */ textlayerrenderedHandler() { - if (!this.annotator || this.annotationsLoaded) { - return; - } - - // Show existing annotations after text layer is rendered - this.annotator.showAnnotations(); - this.annotationsLoaded = true; + this.emit('load'); } /** diff --git a/src/lib/viewers/doc/__tests__/DocBaseViewer-test.js b/src/lib/viewers/doc/__tests__/DocBaseViewer-test.js index e551ab045..e92239b14 100644 --- a/src/lib/viewers/doc/__tests__/DocBaseViewer-test.js +++ b/src/lib/viewers/doc/__tests__/DocBaseViewer-test.js @@ -12,7 +12,6 @@ import * as util from '../../../util'; import { CLASS_BOX_PREVIEW_FIND_BAR, CLASS_HIDDEN, - PERMISSION_ANNOTATE, PERMISSION_DOWNLOAD } from '../../../constants'; @@ -56,7 +55,7 @@ describe('src/lib/viewers/doc/DocBaseViewer', () => { id: '0' } }); - Object.defineProperty(BaseViewer.prototype, 'setup', { value: sandbox.mock() }); + Object.defineProperty(BaseViewer.prototype, 'setup', { value: sandbox.stub() }); docBase.containerEl = containerEl; docBase.setup(); stubs = {}; @@ -316,10 +315,11 @@ describe('src/lib/viewers/doc/DocBaseViewer', () => { Object.defineProperty(BaseViewer.prototype, 'load', { value: sandbox.mock() }); sandbox.stub(docBase, 'createContentUrlWithAuthParams'); sandbox.stub(docBase, 'postload'); - sandbox.stub(docBase, 'loadAssets').returns(Promise.resolve()); sandbox.stub(docBase, 'getRepStatus').returns({ getPromise: () => Promise.resolve() }); + sandbox.stub(docBase, 'loadAssets'); return docBase.load().then(() => { + expect(docBase.loadAssets).to.be.called; expect(docBase.setup).to.be.called; expect(docBase.createContentUrlWithAuthParams).to.be.calledWith('foo'); expect(docBase.postload).to.be.called; @@ -331,6 +331,9 @@ describe('src/lib/viewers/doc/DocBaseViewer', () => { it('should setup pdfjs, init viewer, print, and find', () => { const url = 'foo'; docBase.pdfUrl = url; + docBase.pdfViewer = { + currentScale: 1 + }; const setupPdfjsStub = sandbox.stub(docBase, 'setupPdfjs'); const initViewerStub = sandbox.stub(docBase, 'initViewer'); @@ -851,32 +854,6 @@ describe('src/lib/viewers/doc/DocBaseViewer', () => { }); }); - describe('isAnnotatable()', () => { - beforeEach(() => { - stubs.getViewerOption = sandbox.stub(docBase, 'getViewerOption'); - stubs.getViewerOption.withArgs('annotations').returns(true); - }); - - it('should return false if the type is not a point or a highlight', () => { - expect(docBase.isAnnotatable('drawing')).to.equal(false); - }); - - it('should return true if viewer option is set to true', () => { - expect(docBase.isAnnotatable('point')).to.equal(true); - stubs.getViewerOption.withArgs('annotations').returns(false); - expect(docBase.isAnnotatable('point')).to.equal(false); - }); - - it('should use the global show annotationsBoolean if the viewer param is not specified', () => { - stubs.getViewerOption.withArgs('annotations').returns(null); - docBase.options.showAnnotations = true; - expect(docBase.isAnnotatable('point')).to.equal(true); - - docBase.options.showAnnotations = false; - expect(docBase.isAnnotatable('highlight')).to.equal(false); - }); - }); - describe('getPointModeClickHandler()', () => { beforeEach(() => { stubs.isAnnotatable = sandbox.stub(docBase, 'isAnnotatable').returns(false); @@ -1127,54 +1104,24 @@ describe('src/lib/viewers/doc/DocBaseViewer', () => { }); describe('initAnnotations()', () => { - beforeEach(() => { - docBase.options = { - file: { - file_version: { - id: 0 - }, - permissions: { - can_annotate: true - } - }, - location: { - locale: 'en-US' - } - }; - docBase.pdfViewer = { - currentScale: 1 - }; - stubs.browser = sandbox.stub(Browser, 'isMobile').returns(false); - stubs.setupPageIds = sandbox.stub(docBase, 'setupPageIds'); - stubs.checkPermission = sandbox.stub(file, 'checkPermission'); - }); + const initFunc = BaseViewer.prototype.initAnnotations; - it('should set up page IDs', () => { - docBase.initAnnotations(); - expect(stubs.setupPageIds).to.be.called; - }); - - it('should do nothing if expiring embed is a shared link', () => { - stubs.checkPermission.withArgs(docBase.options.file, PERMISSION_ANNOTATE).returns(true); - docBase.options.sharedLink = 'url'; - docBase.initAnnotations(); - expect(docBase.annotator).to.be.undefined; + afterEach(() => { + Object.defineProperty(BaseViewer.prototype, 'initAnnotations', { value: initFunc }); }); - it('should allow annotations based on browser and permissions', () => { - stubs.checkPermission.withArgs(docBase.options.file, PERMISSION_ANNOTATE).returns(true); - docBase.initAnnotations(); - expect(docBase.annotator.annotationService.canAnnotate).to.be.true; - - stubs.browser.returns(true); - stubs.checkPermission.withArgs(docBase.options.file, PERMISSION_ANNOTATE).returns(true); - docBase.initAnnotations(); - expect(docBase.annotator.annotationService.canAnnotate).to.be.false; + it('should set up page IDs and initialize the annotator', () => { + docBase.pdfViewer = { + currentScale: 1 + }; + docBase.annotator = { + addListener: sandbox.stub() + }; + sandbox.stub(docBase, 'setupPageIds'); + Object.defineProperty(BaseViewer.prototype, 'initAnnotations', { value: sandbox.mock() }); - stubs.browser.returns(false); - stubs.checkPermission.withArgs(docBase.options.file, PERMISSION_ANNOTATE).returns(false); docBase.initAnnotations(); - expect(docBase.annotator.annotationService.canAnnotate).to.be.false; + expect(docBase.setupPageIds).to.be.called; }); }); @@ -1488,8 +1435,6 @@ describe('src/lib/viewers/doc/DocBaseViewer', () => { describe('pagesinitHandler()', () => { beforeEach(() => { - stubs.isAnnotatable = sandbox.stub(docBase, 'isAnnotatable'); - stubs.initAnnotations = sandbox.stub(docBase, 'initAnnotations'); stubs.loadUI = sandbox.stub(docBase, 'loadUI'); stubs.checkPaginationButtons = sandbox.stub(docBase, 'checkPaginationButtons'); stubs.setPage = sandbox.stub(docBase, 'setPage'); @@ -1497,24 +1442,12 @@ describe('src/lib/viewers/doc/DocBaseViewer', () => { stubs.emit = sandbox.stub(docBase, 'emit'); }); - it('should init annotations if annotatable', () => { - stubs.isAnnotatable.returns(true); - docBase.pdfViewer = { - currentScale: 'unknown' - }; - - docBase.pagesinitHandler(); - expect(stubs.initAnnotations).to.be.called; - }); - it('should load UI, check the pagination buttons, set the page, and make document scrollable', () => { - stubs.isAnnotatable.returns(false); docBase.pdfViewer = { currentScale: 'unknown' }; docBase.pagesinitHandler(); - expect(stubs.initAnnotations).to.not.be.called; expect(stubs.loadUI).to.be.called; expect(stubs.checkPaginationButtons).to.be.called; expect(stubs.setPage).to.be.called; @@ -1522,7 +1455,6 @@ describe('src/lib/viewers/doc/DocBaseViewer', () => { }); it('should broadcast that the preview is loaded if it hasn\'t already', () => { - stubs.isAnnotatable.returns(false); docBase.pdfViewer = { currentScale: 'unknown' }; @@ -1540,10 +1472,6 @@ describe('src/lib/viewers/doc/DocBaseViewer', () => { describe('pagerenderedHandler()', () => { beforeEach(() => { - docBase.annotator = { - renderAnnotationsOnPage: sandbox.stub(), - renderAnnotations: sandbox.stub() - }; docBase.event = { detail: { pageNumber: 1 @@ -1562,52 +1490,13 @@ describe('src/lib/viewers/doc/DocBaseViewer', () => { docBase.pagerenderedHandler(docBase.event); expect(stubs.emit).to.be.calledWith('progressend'); }); - - it('should render annotations on a page if the annotator and event page are specified', () => { - docBase.pagerenderedHandler(docBase.event); - expect(docBase.annotator.renderAnnotationsOnPage).to.be.calledWith(docBase.event.detail.pageNumber); - }); - - it('should render annotations all annotations if the annotator but not the page is specified', () => { - docBase.event.detail = undefined; - - docBase.pagerenderedHandler(docBase.event); - expect(docBase.annotator.renderAnnotations).to.be.called; - }); - - it('should show annotations even if the text layer is disabled', () => { - PDFJS.disableTextLayer = true; - - docBase.pagerenderedHandler(docBase.event); - expect(stubs.textlayerrenderedHandler).to.be.called; - }); }); describe('textlayerrenderedHandler()', () => { - beforeEach(() => { - docBase.annotator = { - showAnnotations: sandbox.stub() - }; - }); - it('should do nothing if the annotator does not exist or if the annotations are loaded', () => { - docBase.annotationsLoaded = true; - - docBase.textlayerrenderedHandler(); - expect(docBase.annotator.showAnnotations).to.not.be.called; - - docBase.annotationsLoaded = false; - docBase.annotator = false; - docBase.textlayerrenderedHandler(); - expect(docBase.annotationsLoaded).to.equal(false); - }); - - it('should show annotations and set them as loaded', () => { - docBase.annotationsLoaded = false; - + stubs.emit = sandbox.stub(docBase, 'emit'); docBase.textlayerrenderedHandler(); - expect(docBase.annotator.showAnnotations).to.be.called; - expect(docBase.annotationsLoaded).to.be.true; + expect(stubs.emit).to.be.calledWith('load'); }); }); diff --git a/src/lib/viewers/image/ImageViewer.js b/src/lib/viewers/image/ImageViewer.js index cb7afc897..44bd5f2a6 100644 --- a/src/lib/viewers/image/ImageViewer.js +++ b/src/lib/viewers/image/ImageViewer.js @@ -1,11 +1,8 @@ import autobind from 'autobind-decorator'; -import AnnotationService from '../../annotations/AnnotationService'; -import ImageAnnotator from '../../annotations/image/ImageAnnotator'; import Browser from '../../Browser'; import ImageBaseViewer from './ImageBaseViewer'; -import { checkPermission } from '../../file'; import { ICON_ROTATE_LEFT, ICON_FULLSCREEN_IN, ICON_FULLSCREEN_OUT } from '../../icons/icons'; -import { CLASS_INVISIBLE, PERMISSION_ANNOTATE } from '../../constants'; +import { CLASS_INVISIBLE } from '../../constants'; import { openContentInsideIframe } from '../../util'; import './Image.scss'; @@ -31,7 +28,6 @@ class ImageViewer extends ImageBaseViewer { // hides image tag until content is loaded this.imageEl.classList.add(CLASS_INVISIBLE); - this.initAnnotations(); this.currentRotationAngle = 0; } @@ -268,13 +264,6 @@ class ImageViewer extends ImageBaseViewer { this.controls.add(__('rotate_left'), this.rotateLeft, 'bp-image-rotate-left-icon', ICON_ROTATE_LEFT); this.controls.add(__('enter_fullscreen'), this.toggleFullscreen, 'bp-enter-fullscreen-icon', ICON_FULLSCREEN_IN); this.controls.add(__('exit_fullscreen'), this.toggleFullscreen, 'bp-exit-fullscreen-icon', ICON_FULLSCREEN_OUT); - - // Show existing annotations after image is rendered - if (!this.annotator || this.annotationsLoaded) { - return; - } - this.annotator.showAnnotations(); - this.annotationsLoaded = true; } /** @@ -303,43 +292,11 @@ class ImageViewer extends ImageBaseViewer { /** * Initializes annotations. * - * @private + * @protected * @return {void} */ initAnnotations() { - // Ignore if viewer/file type is not annotatable - if (!this.isAnnotatable()) { - return; - } - - // Users can currently only view annotations on mobile - const { apiHost, file, location, token, sharedLink } = this.options; - - // Do not initialize annotations for shared links - // TODO(@spramod): Determine the expected behavior on shared links - if (sharedLink) { - return; - } - - const canAnnotate = checkPermission(file, PERMISSION_ANNOTATE) && !Browser.isMobile(); - this.canAnnotate = canAnnotate; - - const fileVersionID = file.file_version.id; - const annotationService = new AnnotationService({ - apiHost, - fileId: file.id, - token, - canAnnotate - }); - - // Construct and init annotator - this.annotator = new ImageAnnotator({ - annotatedElement: this.wrapperEl, - annotationService, - fileVersionID, - locale: location.locale - }); - this.annotator.init(this); + super.initAnnotations(); // Disables controls during point annotation mode /* istanbul ignore next */ diff --git a/src/lib/viewers/image/__tests__/ImageBaseViewer-test.js b/src/lib/viewers/image/__tests__/ImageBaseViewer-test.js index 1ac85043a..1ce1d072d 100644 --- a/src/lib/viewers/image/__tests__/ImageBaseViewer-test.js +++ b/src/lib/viewers/image/__tests__/ImageBaseViewer-test.js @@ -487,7 +487,8 @@ describe('lib/viewers/image/ImageBaseViewer', () => { imageBase.finishLoading(); expect(imageBase.loaded).to.be.true; - expect(stubs.emit).to.have.been.called; + expect(stubs.emit).to.have.been.calledWith('load'); + expect(stubs.emit).to.have.been.calledWith('load'); expect(stubs.zoom).to.have.been.called; expect(stubs.loadUI).to.have.been.called; }); diff --git a/src/lib/viewers/image/__tests__/ImageViewer-test.js b/src/lib/viewers/image/__tests__/ImageViewer-test.js index debea696e..b16a2f563 100644 --- a/src/lib/viewers/image/__tests__/ImageViewer-test.js +++ b/src/lib/viewers/image/__tests__/ImageViewer-test.js @@ -2,9 +2,7 @@ import ImageViewer from '../ImageViewer'; import BaseViewer from '../../BaseViewer'; import Browser from '../../../Browser'; -import * as file from '../../../file'; import * as util from '../../../util'; -import { PERMISSION_ANNOTATE } from '../../../constants'; const CSS_CLASS_ZOOMABLE = 'zoomable'; const CSS_CLASS_PANNABLE = 'pannable'; @@ -70,21 +68,8 @@ describe('lib/viewers/image/ImageViewer', () => { describe('setup()', () => { it('should set up layout and init annotations', () => { - image = new ImageViewer({ - container: containerEl, - file: { - id: '1' - } - }); - sandbox.stub(image, 'initAnnotations'); - - Object.defineProperty(BaseViewer.prototype, 'setup', { value: sandbox.stub() }); - image.containerEl = containerEl; - image.setup(); - expect(image.wrapperEl).to.have.class('bp-image'); expect(image.imageEl).to.have.class('bp-is-invisible'); - expect(image.initAnnotations).to.be.called; }); }); @@ -412,16 +397,6 @@ describe('lib/viewers/image/ImageViewer', () => { expect(image.controls.buttonRefs.length).to.equal(5); expect(image.annotationsLoaded).to.be.false; }); - - it('should show annotations after image is rendered', () => { - image.annotator = { - showAnnotations: sandbox.stub() - }; - - image.loadUI(); - expect(image.annotator.showAnnotations).to.be.called; - expect(image.annotationsLoaded).to.be.true; - }); }); describe('print()', () => { @@ -466,70 +441,19 @@ describe('lib/viewers/image/ImageViewer', () => { }); describe('initAnnotations()', () => { - beforeEach(() => { - stubs.annotatable = sandbox.stub(image, 'isAnnotatable'); - stubs.isMobile = sandbox.stub(Browser, 'isMobile').returns(false); - stubs.checkPermission = sandbox.stub(file, 'checkPermission'); - image.options.location = { - locale: 'en-US' - }; - }); - - it('should not init annotations if image is not annotatable', () => { - stubs.annotatable.returns(false); - image.annotator = undefined; + const initFunc = BaseViewer.prototype.initAnnotations; - image.initAnnotations(); - expect(image.annotator).to.be.undefined; - }); - - it('should do nothing if expiring embed is a shared link', () => { - stubs.annotatable.returns(true); - image.options.sharedLink = 'url'; - image.initAnnotations(); - expect(image.annotator).to.be.undefined; + afterEach(() => { + Object.defineProperty(BaseViewer.prototype, 'initAnnotations', { value: initFunc }); }); it('should init annotations if user can annotate', () => { - stubs.checkPermission.withArgs(image.options.file, PERMISSION_ANNOTATE).returns(true); - stubs.annotatable.returns(true); - - image.initAnnotations(); - expect(image.canAnnotate).to.be.true; - expect(image.annotator).to.not.be.undefined; - }); - - it('should init annotations if user cannot annotate', () => { - stubs.checkPermission.withArgs(image.options.file, PERMISSION_ANNOTATE).returns(false); - stubs.annotatable.returns(true); - + Object.defineProperty(BaseViewer.prototype, 'initAnnotations', { value: sandbox.mock() }); + image.annotator = { + addListener: sandbox.stub() + }; image.initAnnotations(); - expect(image.canAnnotate).to.be.false; - expect(image.annotator).to.not.be.undefined; - }); - }); - - describe('isAnnotatable()', () => { - beforeEach(() => { - image.options.viewers = { Image: { annotations: true } }; - }); - - it('should return false if not using point annotations', () => { - const result = image.isAnnotatable('highlight'); - expect(result).to.be.false; - }); - - it('should return viewer permissions if set', () => { - expect(image.isAnnotatable('point')).to.be.true; - image.options.viewers.Image.annotations = false; - expect(image.isAnnotatable('point')).to.be.false; - }); - - it('should return global preview permissions if viewer permissions is not set', () => { - image.options.showAnnotations = true; - image.options.viewers.Image.annotations = 'notboolean'; - const result = image.isAnnotatable('point'); - expect(result).to.be.true; + expect(image.annotator.addListener).to.be.calledTwice; }); });