diff --git a/src/lib/annotations/Annotator.js b/src/lib/annotations/Annotator.js index 74b2593d8..d8899e19c 100644 --- a/src/lib/annotations/Annotator.js +++ b/src/lib/annotations/Annotator.js @@ -298,6 +298,10 @@ class Annotator extends EventEmitter { const pageThreads = this.getThreadsOnPage(pageNum); Object.values(pageThreads).forEach((thread) => { + if (!this.isModeAnnotatable(thread.type)) { + return; + } + thread.show(); }); } diff --git a/src/lib/annotations/BoxAnnotations.js b/src/lib/annotations/BoxAnnotations.js index 7f14f504d..0c2fe9098 100644 --- a/src/lib/annotations/BoxAnnotations.js +++ b/src/lib/annotations/BoxAnnotations.js @@ -8,7 +8,7 @@ const ANNOTATORS = [ NAME: 'Document', CONSTRUCTOR: DocAnnotator, VIEWER: ['Document', 'Presentation'], - TYPE: [TYPES.point, TYPES.highlight] + TYPE: [TYPES.point, TYPES.highlight, TYPES.highlight_comment] }, { NAME: 'Image', diff --git a/src/lib/annotations/MobileAnnotator.scss b/src/lib/annotations/MobileAnnotator.scss index f8410fa1a..aea37c45c 100644 --- a/src/lib/annotations/MobileAnnotator.scss +++ b/src/lib/annotations/MobileAnnotator.scss @@ -216,20 +216,21 @@ $tablet: "(min-width: 768px)"; z-index: 9999; .bp-annotation-highlight-btns { + display: flex; + justify-content: flex-start; padding: 0; button { - float: left; + flex-grow: 1; padding: 15px 0; - width: 50%; - &:first-child::after { + &:nth-child(2)::before { border-right: 1px solid $seesee; bottom: 17px; content: ""; height: 25px; - left: 50%; position: absolute; + right: 50%; } } } diff --git a/src/lib/annotations/__tests__/Annotator-test.js b/src/lib/annotations/__tests__/Annotator-test.js index 27cee03e2..9fe1ebef1 100644 --- a/src/lib/annotations/__tests__/Annotator-test.js +++ b/src/lib/annotations/__tests__/Annotator-test.js @@ -238,6 +238,21 @@ describe('lib/annotations/Annotator', () => { stubs.threadMock3.expects('show').never(); annotator.renderAnnotationsOnPage(1); }); + + it('should not call show() if the thread type is disabled', () => { + const badType = 'not_accepted'; + stubs.thread3.type = badType; + stubs.thread2.type = 'type'; + + stubs.threadMock3.expects('show').never(); + stubs.threadMock2.expects('show').once(); + + const isModeAnn = sandbox.stub(annotator, 'isModeAnnotatable'); + isModeAnn.withArgs(badType).returns(false); + isModeAnn.withArgs('type').returns(true); + + annotator.renderAnnotationsOnPage('2'); + }); }); describe('rotateAnnotations()', () => { diff --git a/src/lib/annotations/doc/CreateHighlightDialog.js b/src/lib/annotations/doc/CreateHighlightDialog.js index ea84fd90c..e9fced6ed 100644 --- a/src/lib/annotations/doc/CreateHighlightDialog.js +++ b/src/lib/annotations/doc/CreateHighlightDialog.js @@ -9,24 +9,20 @@ const TITLE_HIGHLIGHT_TOGGLE = __('annotation_highlight_toggle'); const TITLE_HIGHLIGHT_COMMENT = __('annotation_highlight_comment'); const DATA_TYPE_HIGHLIGHT = 'add-highlight-btn'; const DATA_TYPE_ADD_HIGHLIGHT_COMMENT = 'add-highlight-comment-btn'; -const CREATE_HIGHLIGHT_DIALOG_TEMPLATE = ` -
-
-
- - - - -
-
`.trim(); + +const CARAT_TEMPLATE = `
`; +const HIGHLIGHT_BUTTON_TEMPLATE = ` + `.trim(); +const COMMENT_BUTTON_TEMPLATE = ` + `.trim(); /** * Events emitted by this component. @@ -71,6 +67,12 @@ class CreateHighlightDialog extends EventEmitter { /** @property {boolean} - Whether or not this is visible. */ isVisible; + /** @property {boolean} - Whether or not to allow plain highlight interaction. */ + allowHighlight; + + /** @property {boolean} - Whether or not to allow comment interactions. */ + allowComment; + /** * A dialog used to create plain and comment highlights. * @@ -88,12 +90,19 @@ class CreateHighlightDialog extends EventEmitter { this.parentEl = parentEl; this.isMobile = config.isMobile || false; this.hasTouch = config.hasTouch || false; + this.allowHighlight = config.allowHighlight !== undefined ? !!config.allowHighlight : true; + this.allowComment = config.allowComment !== undefined ? !!config.allowComment : true; // Explicit scope binding for event listeners - this.onHighlightClick = this.onHighlightClick.bind(this); - this.onCommentClick = this.onCommentClick.bind(this); - this.onCommentPost = this.onCommentPost.bind(this); - this.onCommentCancel = this.onCommentCancel.bind(this); + if (this.allowHighlight) { + this.onHighlightClick = this.onHighlightClick.bind(this); + } + + if (this.allowComment) { + this.onCommentClick = this.onCommentClick.bind(this); + this.onCommentPost = this.onCommentPost.bind(this); + this.onCommentCancel = this.onCommentCancel.bind(this); + } } /** @@ -162,8 +171,10 @@ class CreateHighlightDialog extends EventEmitter { hideElement(this.containerEl); - this.commentBox.hide(); - this.commentBox.clear(); + if (this.commentBox) { + this.commentBox.hide(); + this.commentBox.clear(); + } } /** @@ -186,8 +197,6 @@ class CreateHighlightDialog extends EventEmitter { // Event listeners this.highlightCreateEl.removeEventListener('click', this.onHighlightClick); this.commentCreateEl.removeEventListener('click', this.onCommentClick); - this.commentBox.removeListener(CommentBox.CommentEvents.post, this.onCommentPost); - this.commentBox.removeListener(CommentBox.CommentEvents.cancel, this.onCommentCancel); if (this.hasTouch) { this.highlightCreateEl.removeEventListener('touchstart', this.stopPropagation); @@ -201,8 +210,12 @@ class CreateHighlightDialog extends EventEmitter { this.containerEl = null; this.parentEl = null; - this.commentBox.destroy(); - this.commentBox = null; + if (this.commentBox) { + this.commentBox.removeListener(CommentBox.CommentEvents.post, this.onCommentPost); + this.commentBox.removeListener(CommentBox.CommentEvents.cancel, this.onCommentCancel); + this.commentBox.destroy(); + this.commentBox = null; + } } //-------------------------------------------------------------------------- @@ -315,50 +328,77 @@ class CreateHighlightDialog extends EventEmitter { * @return {HTMLElement} The element containing Highlight creation UI */ createElement() { + const caretTemplate = this.isMobile ? '' : CARAT_TEMPLATE; + const highlightTemplate = this.allowHighlight ? HIGHLIGHT_BUTTON_TEMPLATE : ''; + const commentTemplate = this.allowComment ? COMMENT_BUTTON_TEMPLATE : ''; + + const createHighlightDialogTemplate = ` + ${caretTemplate} +
+
+ + ${highlightTemplate} + ${commentTemplate} + +
+
`.trim(); + const highlightDialogEl = document.createElement('div'); highlightDialogEl.classList.add(CLASS_CREATE_DIALOG); - highlightDialogEl.innerHTML = CREATE_HIGHLIGHT_DIALOG_TEMPLATE; + highlightDialogEl.innerHTML = createHighlightDialogTemplate; // Get rid of the caret if (this.isMobile) { highlightDialogEl.classList.add('bp-mobile-annotation-dialog'); highlightDialogEl.classList.add('bp-annotation-dialog'); - highlightDialogEl.querySelector('.bp-annotation-caret').remove(); } const containerEl = highlightDialogEl.querySelector(constants.SELECTOR_ANNOTATION_HIGHLIGHT_DIALOG); // Reference HTML - this.highlightCreateEl = containerEl.querySelector(constants.SELECTOR_ADD_HIGHLIGHT_BTN); - this.commentCreateEl = containerEl.querySelector(`.${constants.CLASS_ADD_HIGHLIGHT_COMMENT_BTN}`); this.buttonsEl = containerEl.querySelector(constants.SELECTOR_HIGHLIGHT_BTNS); - // Create comment box - this.commentBox = new CommentBox(containerEl); - // Stop interacting with this element from triggering outside actions highlightDialogEl.addEventListener('click', this.stopPropagation); highlightDialogEl.addEventListener('mouseup', this.stopPropagation); highlightDialogEl.addEventListener('dblclick', this.stopPropagation); - // Event listeners - this.highlightCreateEl.addEventListener('click', this.onHighlightClick); - this.commentCreateEl.addEventListener('click', this.onCommentClick); - this.commentBox.addListener(CommentBox.CommentEvents.post, this.onCommentPost); - this.commentBox.addListener(CommentBox.CommentEvents.cancel, this.onCommentCancel); + // Events for highlight button + if (this.allowHighlight) { + this.highlightCreateEl = containerEl.querySelector(constants.SELECTOR_ADD_HIGHLIGHT_BTN); + this.highlightCreateEl.addEventListener('click', this.onHighlightClick); + + if (this.hasTouch) { + this.highlightCreateEl.addEventListener('touchstart', this.stopPropagation); + this.highlightCreateEl.addEventListener('touchend', this.onHighlightClick); + } + } + + // Events for comment button + if (this.allowComment) { + // Create comment box + this.commentBox = new CommentBox(containerEl); + this.commentCreateEl = containerEl.querySelector(`.${constants.CLASS_ADD_HIGHLIGHT_COMMENT_BTN}`); + this.commentCreateEl.addEventListener('click', this.onCommentClick); + + // Event listeners + this.commentBox.addListener(CommentBox.CommentEvents.post, this.onCommentPost); + this.commentBox.addListener(CommentBox.CommentEvents.cancel, this.onCommentCancel); + + // Hide comment box, by default + this.commentBox.hide(); + + if (this.hasTouch) { + this.commentCreateEl.addEventListener('touchstart', this.stopPropagation); + this.commentCreateEl.addEventListener('touchend', this.onCommentClick); + } + } // touch events if (this.hasTouch) { - this.highlightCreateEl.addEventListener('touchstart', this.stopPropagation); - this.commentCreateEl.addEventListener('touchstart', this.stopPropagation); - this.highlightCreateEl.addEventListener('touchend', this.onHighlightClick); - this.commentCreateEl.addEventListener('touchend', this.onCommentClick); highlightDialogEl.addEventListener('touchend', this.stopPropagation); } - // Hide comment box, by default - this.commentBox.hide(); - return highlightDialogEl; } } diff --git a/src/lib/annotations/doc/DocAnnotator.js b/src/lib/annotations/doc/DocAnnotator.js index d74e27471..8a5c6c2db 100644 --- a/src/lib/annotations/doc/DocAnnotator.js +++ b/src/lib/annotations/doc/DocAnnotator.js @@ -46,7 +46,7 @@ const ANNOTATION_LAYER_CLASSES = [CLASS_ANNOTATION_LAYER_HIGHLIGHT, CLASS_ANNOTA */ function showFirstDialogFilter(thread, index) { if (index === 0) { - thread.show(); + thread.show(this.plainHighlightEnabled, this.commentHighlightEnabled); // TODO(@jholdstock): remove flags on refactor. } else { thread.hideDialog(); } @@ -86,6 +86,15 @@ class DocAnnotator extends Annotator { /** @property {Selection} - For tracking diffs in text selection, for mobile highlights creation. */ lastSelection; + /** @property {boolean} - True if regular highlights are allowed to be read/written */ + plainHighlightEnabled; + + /** @property {boolean} - True if comment highlights are allowed to be read/written */ + commentHighlightEnabled; + + /** @property {Function} - Reference to filter function that has been bound TODO(@jholdstock): remove on refactor. */ + showFirstDialogFilter; + /** * Creates and mananges plain highlight and comment highlight and point annotations * on document files. @@ -98,20 +107,38 @@ class DocAnnotator extends Annotator { constructor(data) { super(data); + this.plainHighlightEnabled = this.isModeAnnotatable(TYPES.highlight); + this.commentHighlightEnabled = this.isModeAnnotatable(TYPES.highlight_comment); + + // Don't bind to highlight specific handlers if we cannot highlight + if (!this.plainHighlightEnabled && !this.commentHighlightEnabled) { + return; + } + // Explicit scoping - this.highlightCurrentSelection = this.highlightCurrentSelection.bind(this); - this.createHighlightThread = this.createHighlightThread.bind(this); - this.createPlainHighlight = this.createPlainHighlight.bind(this); this.highlightCreateHandler = this.highlightCreateHandler.bind(this); this.drawingSelectionHandler = this.drawingSelectionHandler.bind(this); + this.showFirstDialogFilter = showFirstDialogFilter.bind(this); this.createHighlightDialog = new CreateHighlightDialog(this.container, { isMobile: this.isMobile, - hasTouch: this.hasTouch + hasTouch: this.hasTouch, + allowComment: this.commentHighlightEnabled, + allowHighlight: this.plainHighlightEnabled }); - this.createHighlightDialog.addListener(CreateEvents.plain, this.createPlainHighlight); - this.createHighlightDialog.addListener(CreateEvents.comment, this.highlightCurrentSelection); - this.createHighlightDialog.addListener(CreateEvents.commentPost, this.createHighlightThread); + + if (this.commentHighlightEnabled) { + this.highlightCurrentSelection = this.highlightCurrentSelection.bind(this); + this.createHighlightDialog.addListener(CreateEvents.comment, this.highlightCurrentSelection); + + this.createHighlightThread = this.createHighlightThread.bind(this); + this.createHighlightDialog.addListener(CreateEvents.commentPost, this.createHighlightThread); + } + + if (this.plainHighlightEnabled) { + this.createPlainHighlight = this.createPlainHighlight.bind(this); + this.createHighlightDialog.addListener(CreateEvents.plain, this.createPlainHighlight); + } } /** @@ -121,10 +148,19 @@ class DocAnnotator extends Annotator { */ destroy() { super.destroy(); + if (!this.createHighlightDialog) { + return; + } + + if (this.commentHighlightEnabled) { + this.createHighlightDialog.removeListener(CreateEvents.comment, this.highlightCurrentSelection); + this.createHighlightDialog.removeListener(CreateEvents.commentPost, this.createHighlightThread); + } + + if (this.plainHighlightEnabled) { + this.createHighlightDialog.removeListener(CreateEvents.plain, this.createPlainHighlight); + } - this.createHighlightDialog.removeListener(CreateEvents.plain, this.createPlainHighlight); - this.createHighlightDialog.removeListener(CreateEvents.comment, this.highlightCurrentSelection); - this.createHighlightDialog.removeListener(CreateEvents.commentPost, this.createHighlightThread); this.createHighlightDialog.destroy(); this.createHighlightDialog = null; } @@ -347,17 +383,22 @@ class DocAnnotator extends Annotator { if (commentText === '' || !this.lastHighlightEvent) { return null; } - this.createHighlightDialog.hide(); + + if (this.createHighlightDialog) { + this.createHighlightDialog.hide(); + } + this.isCreatingHighlight = false; - const location = this.getLocationFromEvent(this.lastHighlightEvent, TYPES.highlight); + const highlightType = commentText ? TYPES.highlight_comment : TYPES.highlight; + const location = this.getLocationFromEvent(this.lastHighlightEvent, highlightType); this.highlighter.removeAllHighlights(); if (!location) { return null; } const annotations = []; - const thread = this.createAnnotationThread(annotations, location, TYPES.highlight); + const thread = this.createAnnotationThread(annotations, location, highlightType); this.lastHighlightEvent = null; this.lastSelection = null; @@ -372,7 +413,7 @@ class DocAnnotator extends Annotator { } thread.state = STATES.hover; - thread.show(); + thread.show(this.plainHighlightEnabled, this.commentHighlightEnabled); thread.dialog.postAnnotation(commentText); this.bindCustomListenersOnThread(thread); @@ -381,7 +422,7 @@ class DocAnnotator extends Annotator { } /** - * Renders annotations from memory for a specified page. + * Override to factor in highlight types being filtered out, if disabled. Also scales annotation canvases. * * @override * @param {number} pageNum - Page number @@ -391,7 +432,19 @@ class DocAnnotator extends Annotator { // Scale existing canvases on re-render this.scaleAnnotationCanvases(pageNum); - super.renderAnnotationsOnPage(pageNum); + if (!this.threads) { + return; + } + + // TODO (@jholdstock|@spramod) remove this if statement, and make super call, upon refactor. + const pageThreads = this.getThreadsOnPage(pageNum); + Object.values(pageThreads).forEach((thread) => { + if (!this.isModeAnnotatable(thread.type)) { + return; + } + + thread.show(this.plainHighlightEnabled, this.commentHighlightEnabled); + }); // Destroy current pending highlight annotation const highlightThreads = this.getHighlightThreadsOnPage(pageNum); @@ -434,6 +487,10 @@ class DocAnnotator extends Annotator { setupAnnotations() { super.setupAnnotations(); + if (!this.plainHighlightEnabled && !this.commentHighlightEnabled) { + return; + } + // Init rangy and rangy highlight this.highlighter = rangy.createHighlighter(); this.highlighter.addClassApplier( @@ -456,6 +513,11 @@ class DocAnnotator extends Annotator { this.annotatedElement.addEventListener('mouseup', this.highlightMouseupHandler); + // Prevent all forms of highlight annotations if annotating (or plain AND comment highlights) is disabled + if (!this.permissions.canAnnotate || (!this.plainHighlightEnabled && !this.commentHighlightEnabled)) { + return; + } + if (this.hasTouch && this.isMobile) { document.addEventListener('selectionchange', this.onSelectionChange); this.annotatedElement.addEventListener('touchstart', this.drawingSelectionHandler); @@ -485,6 +547,10 @@ class DocAnnotator extends Annotator { this.highlightThrottleHandle = null; } + if (!this.permissions.canAnnotate) { + return; + } + Object.values(this.modeControllers).forEach((controller) => controller.removeSelection()); if (this.hasTouch && this.isMobile) { @@ -715,7 +781,7 @@ class DocAnnotator extends Annotator { // hovered over at the same time, only the top-most highlight // dialog will be displayed and the others will be hidden // without delay - delayThreads.forEach(showFirstDialogFilter); + delayThreads.forEach(this.showFirstDialogFilter); } /** @@ -769,14 +835,17 @@ class DocAnnotator extends Annotator { this.highlighter.removeAllHighlights(); } - this.createHighlightDialog.hide(); + if (this.createHighlightDialog) { + this.createHighlightDialog.hide(); + } + this.isCreatingHighlight = false; // Creating highlights is disabled on mobile for now since the // event we would listen to, selectionchange, fires continuously and // is unreliable. If the mouse moved or we double clicked text, // we trigger the create handler instead of the click handler - if (this.didMouseMove || event.type === 'dblclick') { + if (this.createHighlightDialog && (this.didMouseMove || event.type === 'dblclick')) { this.highlightCreateHandler(event); } else { this.highlightClickHandler(event); @@ -871,7 +940,7 @@ class DocAnnotator extends Annotator { // Show active thread last if (activeThread) { - activeThread.show(); + activeThread.show(this.plainHighlightEnabled, this.commentHighlightEnabled); } } @@ -933,7 +1002,7 @@ class DocAnnotator extends Annotator { } this.getHighlightThreadsOnPage(page).forEach((thread) => { - thread.show(); + thread.show(this.plainHighlightEnabled, this.commentHighlightEnabled); }); } diff --git a/src/lib/annotations/doc/DocHighlightDialog.js b/src/lib/annotations/doc/DocHighlightDialog.js index dc393ed9d..0c7f5fec9 100644 --- a/src/lib/annotations/doc/DocHighlightDialog.js +++ b/src/lib/annotations/doc/DocHighlightDialog.js @@ -100,6 +100,33 @@ class DocHighlightDialog extends AnnotationDialog { this.toggleHighlight(); } + /** TEMPORARY override to hide or show UI based on enabled annotation types. + * + * @param {boolean} [showPlain] - Whether or not show plain highlight UI + * @param {boolean} [showComment] - Whether or not show comment highlight UI + * @return {void} + */ + show(showPlain = true, showComment = true) { + const plainButtonEl = this.highlightDialogEl.querySelector(`button.${constants.CLASS_ADD_HIGHLIGHT_BTN}`); + const commentButtonEl = this.highlightDialogEl.querySelector( + `button.${constants.CLASS_ADD_HIGHLIGHT_COMMENT_BTN}` + ); + + if (showPlain) { + annotatorUtil.showElement(plainButtonEl); + } else { + annotatorUtil.hideElement(plainButtonEl); + } + + if (showComment) { + annotatorUtil.showElement(commentButtonEl); + } else { + annotatorUtil.hideElement(commentButtonEl); + } + + super.show(); + } + //-------------------------------------------------------------------------- // Abstract Implementations //-------------------------------------------------------------------------- diff --git a/src/lib/annotations/doc/DocHighlightThread.js b/src/lib/annotations/doc/DocHighlightThread.js index 386b9fabc..0e20d81e5 100644 --- a/src/lib/annotations/doc/DocHighlightThread.js +++ b/src/lib/annotations/doc/DocHighlightThread.js @@ -239,12 +239,14 @@ class DocHighlightThread extends AnnotationThread { * the highlight in active state and show the 'delete' button. * * @override + * @param {boolean} [showPlain] - Whether or not plain highlight ui is shown (TEMPORARY UNTIL REFACTOR) + * @param {boolean} [showComment] - Whether or not comment highlight ui is shown (TEMPORARY UNTIL REFACTOR) * @return {void} */ - show() { + show(showPlain, showComment) { switch (this.state) { case STATES.pending: - this.showDialog(); + this.showDialog(showPlain, showComment); break; case STATES.inactive: this.hideDialog(); @@ -252,7 +254,7 @@ class DocHighlightThread extends AnnotationThread { break; case STATES.hover: case STATES.pending_active: - this.showDialog(); + this.showDialog(showPlain, showComment); this.draw(HIGHLIGHT_FILL.active); break; default: @@ -260,6 +262,24 @@ class DocHighlightThread extends AnnotationThread { } } + /** Overridden to hide UI elements depending on whether or not comments or plain + * are allowed. Note: This will be deprecated upon proper refactor or comment highlight + * and plain highlights. + * + * @override + * @param {boolean} [showPlain] - Whether or not plain highlight ui is shown + * @param {boolean} [showComment] - Whether or not comment highlight ui is shown + * @return {void} + */ + showDialog(showPlain, showComment) { + // Prevents the annotations dialog from being created each mousemove + if (!this.dialog.element) { + this.dialog.setup(this.annotations); + } + + this.dialog.show(showPlain, showComment); + } + /** * Creates the document highlight annotation dialog for the thread. * diff --git a/src/lib/annotations/doc/__tests__/CreateHighlightDialog-test.js b/src/lib/annotations/doc/__tests__/CreateHighlightDialog-test.js index 86667db8f..ddcc093dc 100644 --- a/src/lib/annotations/doc/__tests__/CreateHighlightDialog-test.js +++ b/src/lib/annotations/doc/__tests__/CreateHighlightDialog-test.js @@ -1,6 +1,11 @@ /* eslint-disable no-unused-expressions */ import CreateHighlightDialog, { CreateEvents } from '../CreateHighlightDialog'; -import { CLASS_HIDDEN } from '../../annotationConstants'; +import { + CLASS_ADD_HIGHLIGHT_BTN, + CLASS_ADD_HIGHLIGHT_COMMENT_BTN, + CLASS_ANNOTATION_CARET, + CLASS_HIDDEN +} from '../../annotationConstants'; import CommentBox from '../../CommentBox'; import * as annotatorUtil from '../../annotatorUtil'; @@ -22,6 +27,24 @@ describe('lib/annotations/doc/CreateHighlightDialog', () => { parentEl = null; }); + describe('contructor()', () => { + it('should default to enable highlights and comments if no config passed in', () => { + const instance = new CreateHighlightDialog(document.createElement('div')); + expect(instance.allowHighlight).to.be.true; + expect(instance.allowComment).to.be.true; + }); + + it('should take config falsey value to disable highlights and comments, when passed in', () => { + const config = { + allowHighlight: 'this will falsey to true', + allowComment: false + }; + const instance = new CreateHighlightDialog(document.createElement('div'), config); + expect(instance.allowHighlight).to.be.true; + expect(instance.allowComment).to.be.false; + }); + }); + describe('setParentEl()', () => { it('should assign the new parent reference', () => { const newParent = document.createElement('span'); @@ -111,6 +134,13 @@ describe('lib/annotations/doc/CreateHighlightDialog', () => { dialog.hide(); expect(clearComment).to.be.called; }); + + it('should do nothing with the comment box if it does not exist', () => { + const clearComment = sandbox.stub(dialog.commentBox, 'clear'); + dialog.commentBox = null; + dialog.hide(); + expect(clearComment).to.not.be.called; + }); }); describe('destroy()', () => { @@ -348,27 +378,48 @@ describe('lib/annotations/doc/CreateHighlightDialog', () => { }); describe('createElement()', () => { - let containerEl; - - beforeEach(() => { - containerEl = dialog.createElement(); - }); it('should create a div with the proper create highlight class', () => { + let containerEl = dialog.createElement(); expect(containerEl.nodeName).to.equal('DIV'); expect(containerEl.classList.contains(CLASS_CREATE_DIALOG)).to.be.true; }); it('should make a reference to the highlight button', () => { + let containerEl = dialog.createElement(); expect(dialog.highlightCreateEl).to.exist; }); it('should make a reference to the comment button', () => { + let containerEl = dialog.createElement(); expect(dialog.commentCreateEl).to.exist; }); it('should create a comment box', () => { + let containerEl = dialog.createElement(); expect(dialog.commentBox).to.be.an.instanceof(CommentBox); }); + + it('should not create the caret if on a mobile device', () => { + dialog.isMobile = true; + let containerEl = dialog.createElement(); + + expect(containerEl.querySelector(`.${CLASS_ANNOTATION_CARET}`)).to.not.exist; + }); + + it('should not create a highlight button if highlights are disabled', () => { + dialog.allowHighlight = false; + let containerEl = dialog.createElement(); + + expect(containerEl.querySelector(`.${CLASS_ADD_HIGHLIGHT_BTN}`)).to.not.exist; + }); + + it('should not create a comment box or button if comments are disabled', () => { + dialog.allowComment = false; + let containerEl = dialog.createElement(); + + expect(containerEl.querySelector(`.${CLASS_ADD_HIGHLIGHT_COMMENT_BTN}`)).to.not.exist; + expect(dialog.commentBox).to.not.exist; + }); }); }); diff --git a/src/lib/annotations/doc/__tests__/DocAnnotator-test.js b/src/lib/annotations/doc/__tests__/DocAnnotator-test.js index ef2470d47..ffb851a50 100644 --- a/src/lib/annotations/doc/__tests__/DocAnnotator-test.js +++ b/src/lib/annotations/doc/__tests__/DocAnnotator-test.js @@ -7,6 +7,7 @@ import DocAnnotator from '../DocAnnotator'; import DocHighlightThread from '../DocHighlightThread'; import DocDrawingThread from '../DocDrawingThread'; import DocPointThread from '../DocPointThread'; +import { CreateEvents } from '../CreateHighlightDialog'; import * as annotatorUtil from '../../annotatorUtil'; import * as docAnnotatorUtil from '../docAnnotatorUtil'; import { @@ -31,13 +32,20 @@ describe('lib/annotations/doc/DocAnnotator', () => { fixture.load('annotations/doc/__tests__/DocAnnotator-test.html'); annotator = new DocAnnotator({ - canAnnotate: true, + permissions: { + canAnnotate: true + }, container: document, annotationService: {}, fileVersionId: 1, isMobile: false, options: {}, - modeButtons: {} + modeButtons: {}, + options: { + annotator: { + TYPE: ['highlight', 'highlight-comment'] + } + } }); annotator.annotatedElement = annotator.getAnnotatedEl(document); annotator.annotationService = {}; @@ -55,6 +63,56 @@ describe('lib/annotations/doc/DocAnnotator', () => { stubs = {}; }); + describe('constructor()', () => { + it('should not bind any plain highlight functions if they are disabled', () => { + const docAnno = new DocAnnotator({ + permissions: { + canAnnotate: true + }, + container: document, + annotationService: {}, + fileVersionId: 1, + isMobile: false, + options: {}, + modeButtons: {}, + options: { + annotator: { + TYPE: ['highlight-comment'] + } + } + }); + + const createPlain = sandbox.stub(docAnno, 'createPlainHighlight'); + docAnno.createHighlightDialog.emit(CreateEvents.plain); + + expect(createPlain).to.not.be.called; + }); + + it('should not bind any comment highlight functions if they are disabled', () => { + const docAnno = new DocAnnotator({ + permissions: { + canAnnotate: true + }, + container: document, + annotationService: {}, + fileVersionId: 1, + isMobile: false, + options: {}, + modeButtons: {}, + options: { + annotator: { + TYPE: ['highlight'] + } + } + }); + + const createComment = sandbox.stub(docAnno, 'createHighlightThread'); + docAnno.createHighlightDialog.emit(CreateEvents.commentPost); + + expect(createComment).to.not.be.called; + }); + }); + describe('getAnnotatedEl()', () => { it('should return the annotated element as the document', () => { expect(annotator.annotatedElement).to.not.be.null; @@ -384,7 +442,7 @@ describe('lib/annotations/doc/DocAnnotator', () => { stubs.createAnnotationThread.returns(thread); annotator.createHighlightThread('some text with severe passive agression'); - expect(stubs.createAnnotationThread).to.be.calledWith([], location, TYPES.highlight); + expect(stubs.createAnnotationThread).to.be.calledWith([], location, TYPES.highlight_comment); }); it('should bail out of making an annotation if thread is null', () => { @@ -460,17 +518,11 @@ describe('lib/annotations/doc/DocAnnotator', () => { }); describe('renderAnnotationsOnPage()', () => { - const renderFunc = Annotator.prototype.renderAnnotationsOnPage; beforeEach(() => { - Object.defineProperty(Annotator.prototype, 'renderAnnotationsOnPage', { value: sandbox.mock() }); sandbox.stub(annotator, 'scaleAnnotationCanvases'); }); - afterEach(() => { - Object.defineProperty(Annotator.prototype, 'renderAnnotationsOnPage', { value: renderFunc }); - }); - it('should destroy any pending highlight annotations on the page', () => { const pendingThread = { state: 'pending', destroy: () => {} }; stubs.pendingMock = sandbox.mock(pendingThread); @@ -489,6 +541,24 @@ describe('lib/annotations/doc/DocAnnotator', () => { annotator.renderAnnotationsOnPage(1); expect(annotator.scaleAnnotationCanvases).to.be.called; }); + + it('should call show on ONLY enabled annotation types', () => { + const plain = { state: 'I do not care', type: 'highlight', show: sandbox.stub() }; + const comment = { state: 'I do not care', type: 'highlight-comment', show: sandbox.stub() }; + const point = { state: 'I do not care', type: 'point', show: sandbox.stub() }; + const threads = [plain, comment, point]; + annotator.threads = { 1: threads }; + sandbox.stub(annotator, 'getHighlightThreadsOnPage').returns(threads); + annotator.options.annotator.TYPE = ['highlight', 'point']; + + annotator.renderAnnotationsOnPage(1); + + expect(plain.show).to.be.called; + expect(point.show).to.be.called; + expect(comment.show).to.not.be.called; + + annotator.threads = {}; + }); }); describe('scaleAnnotationCanvases()', () => { @@ -526,6 +596,16 @@ describe('lib/annotations/doc/DocAnnotator', () => { expect(rangy.createHighlighter).to.have.been.called; expect(stubs.highlighter.addClassApplier).to.have.been.called; }); + + it('should not create a highlighter if all forms of highlight are disabled', () => { + sandbox.stub(rangy, 'createHighlighter'); + annotator.plainHighlightEnabled = false; + annotator.commentHighlightEnabled = false; + + annotator.setupAnnotations(); + + expect(rangy.createHighlighter).to.not.be.called; + }); }); describe('bindDOMListeners()', () => { @@ -553,6 +633,7 @@ describe('lib/annotations/doc/DocAnnotator', () => { annotator.permissions.canAnnotate = true; annotator.isMobile = true; annotator.hasTouch = true; + const docListen = sandbox.spy(document, 'addEventListener'); const annotatedElementListen = sandbox.spy(annotator.annotatedElement, 'addEventListener'); diff --git a/src/lib/annotations/doc/__tests__/DocHighlightDialog-test.js b/src/lib/annotations/doc/__tests__/DocHighlightDialog-test.js index c29e9faf7..632f526da 100644 --- a/src/lib/annotations/doc/__tests__/DocHighlightDialog-test.js +++ b/src/lib/annotations/doc/__tests__/DocHighlightDialog-test.js @@ -198,8 +198,36 @@ describe('lib/annotations/doc/DocHighlightDialog', () => { expect(dialog.highlightDialogEl.classList.remove).to.be.calledWith(constants.CLASS_HIDDEN); }); + }); + + describe('show()', () => { + beforeEach(() => { + Object.defineProperty(AnnotationDialog.prototype, 'show', { value: sandbox.stub() }); + }); + + it('should show the highlight button if it has been enabled', () => { + dialog.show(true, true); + const button = dialog.highlightDialogEl.querySelector(`button.${constants.CLASS_ADD_HIGHLIGHT_BTN}`); + expect(button.classList.contains(constants.CLASS_HIDDEN)).to.be.false; + }); - it('should set hasComments to false'); + it('should hide the highlight button if it has been disabled', () => { + dialog.show(false, true); + const button = dialog.highlightDialogEl.querySelector(`button.${constants.CLASS_ADD_HIGHLIGHT_BTN}`); + expect(button.classList.contains(constants.CLASS_HIDDEN)).to.be.true; + }); + + it('should show the comment button if it has been enabled', () => { + dialog.show(true, true); + const button = dialog.highlightDialogEl.querySelector(`button.${constants.CLASS_ADD_HIGHLIGHT_COMMENT_BTN}`); + expect(button.classList.contains(constants.CLASS_HIDDEN)).to.be.false; + }); + + it('should hide the comment button if it has been disabled', () => { + dialog.show(true, false); + const button = dialog.highlightDialogEl.querySelector(`button.${constants.CLASS_ADD_HIGHLIGHT_COMMENT_BTN}`); + expect(button.classList.contains(constants.CLASS_HIDDEN)).to.be.true; + }); }); describe('position()', () => {