From fa7855edaaf7d44ed128a5462f2b9c5849f62617 Mon Sep 17 00:00:00 2001 From: Sumedha Pramod Date: Mon, 30 Oct 2017 17:38:22 -0700 Subject: [PATCH] Chore: Removing annotations from Box-Content-Preview (#451) * Chore: Remove annotations from Preview * Chore: Remove annotations icon * Chore: Leaving README.md in Preview until box-annotations repo is public --- src/lib/annotations/Annotation.js | 51 - src/lib/annotations/AnnotationDialog.js | 779 --------- .../annotations/AnnotationModeController.js | 179 -- src/lib/annotations/AnnotationService.js | 321 ---- src/lib/annotations/AnnotationThread.js | 655 ------- src/lib/annotations/Annotator.js | 1025 ----------- src/lib/annotations/Annotator.scss | 586 ------- src/lib/annotations/BoxAnnotations.js | 134 -- src/lib/annotations/CommentBox.js | 267 --- src/lib/annotations/MobileAnnotator.scss | 240 --- .../__tests__/AnnotationDialog-test.html | 7 - .../__tests__/AnnotationDialog-test.js | 980 ----------- .../AnnotationModeController-test.js | 194 --- .../__tests__/AnnotationService-test.js | 475 ------ .../__tests__/AnnotationThread-test.html | 1 - .../__tests__/AnnotationThread-test.js | 783 --------- .../annotations/__tests__/Annotator-test.html | 2 - .../annotations/__tests__/Annotator-test.js | 1275 -------------- .../__tests__/BoxAnnotations-test.js | 237 --- .../annotations/__tests__/CommentBox-test.js | 275 --- .../__tests__/annotatorUtil-test.html | 17 - .../__tests__/annotatorUtil-test.js | 690 -------- src/lib/annotations/annotationConstants.js | 147 -- src/lib/annotations/annotationsShell.html | 23 - src/lib/annotations/annotatorUtil.js | 719 -------- .../annotations/doc/CreateHighlightDialog.js | 419 ----- src/lib/annotations/doc/DocAnnotator.js | 1082 ------------ src/lib/annotations/doc/DocDrawingDialog.js | 282 --- src/lib/annotations/doc/DocDrawingThread.js | 323 ---- src/lib/annotations/doc/DocHighlightDialog.js | 601 ------- src/lib/annotations/doc/DocHighlightThread.js | 554 ------ src/lib/annotations/doc/DocPointAnnotator.js | 62 - src/lib/annotations/doc/DocPointDialog.js | 60 - src/lib/annotations/doc/DocPointThread.js | 86 - .../__tests__/CreateHighlightDialog-test.html | 1 - .../__tests__/CreateHighlightDialog-test.js | 440 ----- .../doc/__tests__/DocAnnotator-test.html | 1 - .../doc/__tests__/DocAnnotator-test.js | 1513 ----------------- .../doc/__tests__/DocDrawingDialog-test.html | 13 - .../doc/__tests__/DocDrawingDialog-test.js | 299 ---- .../doc/__tests__/DocDrawingThread-test.html | 1 - .../doc/__tests__/DocDrawingThread-test.js | 470 ----- .../__tests__/DocHighlightDialog-test.html | 13 - .../doc/__tests__/DocHighlightDialog-test.js | 721 -------- .../__tests__/DocHighlightThread-test.html | 6 - .../doc/__tests__/DocHighlightThread-test.js | 537 ------ .../doc/__tests__/DocPointDialog-test.html | 1 - .../doc/__tests__/DocPointDialog-test.js | 52 - .../doc/__tests__/DocPointThread-test.html | 1 - .../doc/__tests__/DocPointThread-test.js | 121 -- .../doc/__tests__/docAnnotatorUtil-test.html | 11 - .../doc/__tests__/docAnnotatorUtil-test.js | 318 ---- src/lib/annotations/doc/docAnnotatorUtil.js | 393 ----- .../annotations/drawing/DrawingContainer.js | 146 -- .../drawing/DrawingModeController.js | 329 ---- src/lib/annotations/drawing/DrawingPath.js | 174 -- src/lib/annotations/drawing/DrawingThread.js | 428 ----- .../__tests__/DrawingContainer-test.js | 194 --- .../__tests__/DrawingModeController-test.js | 389 ----- .../drawing/__tests__/DrawingPath-test.js | 227 --- .../drawing/__tests__/DrawingThread-test.js | 384 ----- src/lib/annotations/image/ImageAnnotator.js | 175 -- src/lib/annotations/image/ImagePointDialog.js | 55 - src/lib/annotations/image/ImagePointThread.js | 65 - .../image/__tests__/ImageAnnotator-test.html | 4 - .../image/__tests__/ImageAnnotator-test.js | 209 --- .../__tests__/ImagePointDialog-test.html | 6 - .../image/__tests__/ImagePointDialog-test.js | 56 - .../__tests__/ImagePointThread-test.html | 4 - .../image/__tests__/ImagePointThread-test.js | 107 -- .../__tests__/imageAnnotatorUtil-test.html | 5 - .../__tests__/imageAnnotatorUtil-test.js | 157 -- .../annotations/image/imageAnnotatorUtil.js | 116 -- src/lib/icons/annotation_24px.svg | 3 - .../annotation_highlight_comment_24px.svg | 6 - src/lib/icons/draw_delete.svg | 1 - src/lib/icons/draw_save.svg | 1 - src/lib/icons/highlight_24px.svg | 6 - src/lib/icons/icons.js | 12 - src/lib/icons/placed_annotation_24px.svg | 9 - 80 files changed, 21711 deletions(-) delete mode 100644 src/lib/annotations/Annotation.js delete mode 100644 src/lib/annotations/AnnotationDialog.js delete mode 100644 src/lib/annotations/AnnotationModeController.js delete mode 100644 src/lib/annotations/AnnotationService.js delete mode 100644 src/lib/annotations/AnnotationThread.js delete mode 100644 src/lib/annotations/Annotator.js delete mode 100644 src/lib/annotations/Annotator.scss delete mode 100644 src/lib/annotations/BoxAnnotations.js delete mode 100644 src/lib/annotations/CommentBox.js delete mode 100644 src/lib/annotations/MobileAnnotator.scss delete mode 100644 src/lib/annotations/__tests__/AnnotationDialog-test.html delete mode 100644 src/lib/annotations/__tests__/AnnotationDialog-test.js delete mode 100644 src/lib/annotations/__tests__/AnnotationModeController-test.js delete mode 100644 src/lib/annotations/__tests__/AnnotationService-test.js delete mode 100644 src/lib/annotations/__tests__/AnnotationThread-test.html delete mode 100644 src/lib/annotations/__tests__/AnnotationThread-test.js delete mode 100644 src/lib/annotations/__tests__/Annotator-test.html delete mode 100644 src/lib/annotations/__tests__/Annotator-test.js delete mode 100644 src/lib/annotations/__tests__/BoxAnnotations-test.js delete mode 100644 src/lib/annotations/__tests__/CommentBox-test.js delete mode 100644 src/lib/annotations/__tests__/annotatorUtil-test.html delete mode 100644 src/lib/annotations/__tests__/annotatorUtil-test.js delete mode 100644 src/lib/annotations/annotationConstants.js delete mode 100644 src/lib/annotations/annotationsShell.html delete mode 100644 src/lib/annotations/annotatorUtil.js delete mode 100644 src/lib/annotations/doc/CreateHighlightDialog.js delete mode 100644 src/lib/annotations/doc/DocAnnotator.js delete mode 100644 src/lib/annotations/doc/DocDrawingDialog.js delete mode 100644 src/lib/annotations/doc/DocDrawingThread.js delete mode 100644 src/lib/annotations/doc/DocHighlightDialog.js delete mode 100644 src/lib/annotations/doc/DocHighlightThread.js delete mode 100644 src/lib/annotations/doc/DocPointAnnotator.js delete mode 100644 src/lib/annotations/doc/DocPointDialog.js delete mode 100644 src/lib/annotations/doc/DocPointThread.js delete mode 100644 src/lib/annotations/doc/__tests__/CreateHighlightDialog-test.html delete mode 100644 src/lib/annotations/doc/__tests__/CreateHighlightDialog-test.js delete mode 100644 src/lib/annotations/doc/__tests__/DocAnnotator-test.html delete mode 100644 src/lib/annotations/doc/__tests__/DocAnnotator-test.js delete mode 100644 src/lib/annotations/doc/__tests__/DocDrawingDialog-test.html delete mode 100644 src/lib/annotations/doc/__tests__/DocDrawingDialog-test.js delete mode 100644 src/lib/annotations/doc/__tests__/DocDrawingThread-test.html delete mode 100644 src/lib/annotations/doc/__tests__/DocDrawingThread-test.js delete mode 100644 src/lib/annotations/doc/__tests__/DocHighlightDialog-test.html delete mode 100644 src/lib/annotations/doc/__tests__/DocHighlightDialog-test.js delete mode 100644 src/lib/annotations/doc/__tests__/DocHighlightThread-test.html delete mode 100644 src/lib/annotations/doc/__tests__/DocHighlightThread-test.js delete mode 100644 src/lib/annotations/doc/__tests__/DocPointDialog-test.html delete mode 100644 src/lib/annotations/doc/__tests__/DocPointDialog-test.js delete mode 100644 src/lib/annotations/doc/__tests__/DocPointThread-test.html delete mode 100644 src/lib/annotations/doc/__tests__/DocPointThread-test.js delete mode 100644 src/lib/annotations/doc/__tests__/docAnnotatorUtil-test.html delete mode 100644 src/lib/annotations/doc/__tests__/docAnnotatorUtil-test.js delete mode 100644 src/lib/annotations/doc/docAnnotatorUtil.js delete mode 100644 src/lib/annotations/drawing/DrawingContainer.js delete mode 100644 src/lib/annotations/drawing/DrawingModeController.js delete mode 100644 src/lib/annotations/drawing/DrawingPath.js delete mode 100644 src/lib/annotations/drawing/DrawingThread.js delete mode 100644 src/lib/annotations/drawing/__tests__/DrawingContainer-test.js delete mode 100644 src/lib/annotations/drawing/__tests__/DrawingModeController-test.js delete mode 100644 src/lib/annotations/drawing/__tests__/DrawingPath-test.js delete mode 100644 src/lib/annotations/drawing/__tests__/DrawingThread-test.js delete mode 100644 src/lib/annotations/image/ImageAnnotator.js delete mode 100644 src/lib/annotations/image/ImagePointDialog.js delete mode 100644 src/lib/annotations/image/ImagePointThread.js delete mode 100644 src/lib/annotations/image/__tests__/ImageAnnotator-test.html delete mode 100644 src/lib/annotations/image/__tests__/ImageAnnotator-test.js delete mode 100644 src/lib/annotations/image/__tests__/ImagePointDialog-test.html delete mode 100644 src/lib/annotations/image/__tests__/ImagePointDialog-test.js delete mode 100644 src/lib/annotations/image/__tests__/ImagePointThread-test.html delete mode 100644 src/lib/annotations/image/__tests__/ImagePointThread-test.js delete mode 100644 src/lib/annotations/image/__tests__/imageAnnotatorUtil-test.html delete mode 100644 src/lib/annotations/image/__tests__/imageAnnotatorUtil-test.js delete mode 100644 src/lib/annotations/image/imageAnnotatorUtil.js delete mode 100644 src/lib/icons/annotation_24px.svg delete mode 100644 src/lib/icons/annotation_highlight_comment_24px.svg delete mode 100644 src/lib/icons/draw_delete.svg delete mode 100644 src/lib/icons/draw_save.svg delete mode 100644 src/lib/icons/highlight_24px.svg delete mode 100755 src/lib/icons/placed_annotation_24px.svg diff --git a/src/lib/annotations/Annotation.js b/src/lib/annotations/Annotation.js deleted file mode 100644 index f7983fe46..000000000 --- a/src/lib/annotations/Annotation.js +++ /dev/null @@ -1,51 +0,0 @@ -import autobind from 'autobind-decorator'; - -@autobind -class Annotation { - //-------------------------------------------------------------------------- - // Typedef - //-------------------------------------------------------------------------- - - /** - * The data object for constructing an annotation. - * - * @typedef {Object} AnnotationData - * @property {string} annotationID Annotation ID - * @property {string} fileVersionId File version ID for this annotation - * @property {string} threadID Thread ID - * @property {string} thread Thread number - * @property {string} type Annotation type, e.g. 'point' or 'highlight' - * @property {string} text Annotation text - * @property {Object} location Location object - * @property {Object} user User creating/that created this annotation - * @property {Object} permissions Permissions user has - * @property {number} created Created timestamp - * @property {number} modified Modified timestamp - */ - - //-------------------------------------------------------------------------- - // Public - //-------------------------------------------------------------------------- - - /** - * [constructor] - * - * @param {AnnotationData} data - Data for constructing annotation - * @return {Annotation} Instance of annotation - */ - constructor(data) { - this.annotationID = data.annotationID; - this.fileVersionId = data.fileVersionId; - this.threadID = data.threadID; - this.threadNumber = data.threadNumber; - this.type = data.type; - this.text = data.text; - this.location = data.location; - this.user = data.user; - this.permissions = data.permissions; - this.created = data.created; - this.modified = data.modified; - } -} - -export default Annotation; diff --git a/src/lib/annotations/AnnotationDialog.js b/src/lib/annotations/AnnotationDialog.js deleted file mode 100644 index 6633d62b3..000000000 --- a/src/lib/annotations/AnnotationDialog.js +++ /dev/null @@ -1,779 +0,0 @@ -import autobind from 'autobind-decorator'; -import EventEmitter from 'events'; -import * as annotatorUtil from './annotatorUtil'; -import * as constants from './annotationConstants'; -import { ICON_CLOSE, ICON_DELETE } from '../icons/icons'; - -const POINT_ANNOTATION_ICON_HEIGHT = 31; -const POINT_ANNOTATION_ICON_DOT_HEIGHT = 8; -const CLASS_FLIPPED_DIALOG = 'bp-annotation-dialog-flipped'; - -const CLASS_BUTTON_DELETE_COMMENT = 'delete-comment-btn'; -const CLASS_CANCEL_DELETE = 'cancel-delete-btn'; -const CLASS_CANNOT_ANNOTATE = 'cannot-annotate'; -const CLASS_COMMENT = 'annotation-comment'; -const CLASS_COMMENTS_CONTAINER = 'annotation-comments'; -const CLASS_REPLY_CONTAINER = 'reply-container'; -const CLASS_REPLY_TEXTAREA = 'reply-textarea'; -const CLASS_DELETE_CONFIRMATION = 'delete-confirmation'; -const CLASS_BUTTON_DELETE_CONFIRM = 'confirm-delete-btn'; - -@autobind -class AnnotationDialog extends EventEmitter { - //-------------------------------------------------------------------------- - // Typedef - //-------------------------------------------------------------------------- - - /** - * The data object for constructing a dialog. - * - * @typedef {Object} AnnotationDialogData - * @property {HTMLElement} annotatedElement HTML element being annotated on - * @property {Annotation[]} annotations Annotations in dialog, can be an - * empty array for a new thread - * @property {Object} location Location object - * @property {boolean} canAnnotate Whether or not user can annotate - */ - - //-------------------------------------------------------------------------- - // Public - //-------------------------------------------------------------------------- - - /** - * [constructor] - * - * @param {AnnotationDialogData} data - Data for constructing thread - * @return {AnnotationDialog} Annotation dialog instance - */ - constructor(data) { - super(); - - this.annotatedElement = data.annotatedElement; - this.container = data.container; - this.location = data.location; - this.hasAnnotations = data.annotations.length > 0; - this.canAnnotate = data.canAnnotate; - this.locale = data.locale; - this.isMobile = data.isMobile; - } - - /** - * [destructor] - * - * @return {void} - */ - destroy() { - if (this.element) { - this.unbindDOMListeners(); - - if (this.element.parentNode) { - this.element.parentNode.removeChild(this.element); - } - - this.element = null; - } - } - - /** - * Positions and shows the dialog. - * - * @return {void} - */ - show() { - // Populate mobile annotations dialog with annotations information - if (this.isMobile) { - this.element = this.container.querySelector(`.${constants.CLASS_MOBILE_ANNOTATION_DIALOG}`); - annotatorUtil.showElement(this.element); - this.element.appendChild(this.dialogEl); - - const commentEls = this.element.querySelectorAll(`.${CLASS_COMMENT}`); - if (this.highlightDialogEl && !commentEls.length) { - this.element.classList.add(constants.CLASS_ANNOTATION_PLAIN_HIGHLIGHT); - - const headerEl = this.element.querySelector(constants.SELECTOR_MOBILE_DIALOG_HEADER); - headerEl.classList.add(constants.CLASS_HIDDEN); - } - - const dialogCloseButtonEl = this.element.querySelector(constants.SELECTOR_DIALOG_CLOSE); - dialogCloseButtonEl.addEventListener('click', this.hideMobileDialog); - - this.element.classList.add(constants.CLASS_ANIMATE_DIALOG); - - this.bindDOMListeners(); - } - - const textAreaEl = this.hasAnnotations - ? this.element.querySelector(`.${CLASS_REPLY_TEXTAREA}`) - : this.element.querySelector(constants.SELECTOR_ANNOTATION_TEXTAREA); - - // Don't re-position if reply textarea is already active - const textareaIsActive = textAreaEl.classList.contains(constants.CLASS_ACTIVE); - if (textareaIsActive && this.element.parentNode) { - return; - } - - // Position and show - we need to reposition every time since the DOM - // could have changed from zooming - if (!this.isMobile) { - this.position(); - } - - // Activate appropriate textarea - if (this.hasAnnotations) { - this.activateReply(); - } else { - textAreaEl.classList.add(constants.CLASS_ACTIVE); - } - - // Move cursor to end of text area - if (textAreaEl.selectionStart) { - textAreaEl.selectionEnd = textAreaEl.value.length; - textAreaEl.selectionStart = textAreaEl.selectionEnd; - } - - // If user cannot annotate, hide reply/edit/delete UI - if (!this.canAnnotate) { - this.element.classList.add(CLASS_CANNOT_ANNOTATE); - } - - // Focus the textarea if visible - if (annotatorUtil.isElementInViewport(textAreaEl)) { - textAreaEl.focus(); - } - } - - /** - * Hides and resets the shared mobile dialog. - * - * @return {void} - */ - hideMobileDialog() { - if (!this.element) { - return; - } - - if (this.dialogEl && this.dialogEl.parentNode) { - this.dialogEl.parentNode.removeChild(this.dialogEl); - } - - this.element.classList.remove(constants.CLASS_ANIMATE_DIALOG); - - // Clear annotations from dialog - this.element.innerHTML = ` -
- -
`.trim(); - this.element.classList.remove(constants.CLASS_ANNOTATION_PLAIN_HIGHLIGHT); - - const dialogCloseButtonEl = this.element.querySelector(constants.SELECTOR_DIALOG_CLOSE); - dialogCloseButtonEl.removeEventListener('click', this.hideMobileDialog); - - annotatorUtil.hideElement(this.element); - this.unbindDOMListeners(); - - // Cancel any unsaved annotations - this.cancelAnnotation(); - } - - /** - * Hides the dialog. - * - * @return {void} - */ - hide() { - if (this.element && this.element.classList.contains(constants.CLASS_HIDDEN)) { - return; - } - - if (this.isMobile) { - this.hideMobileDialog(); - } - - annotatorUtil.hideElement(this.element); - this.deactivateReply(); - - // Make sure entire thread icon displays for flipped dialogs - this.toggleFlippedThreadEl(); - } - - /** - * Adds an annotation to the dialog. - * - * @param {Annotation} annotation - Annotation to add - * @return {void} - */ - addAnnotation(annotation) { - // Show new section if needed - if (!this.hasAnnotations) { - const createSectionEl = this.element.querySelector(constants.SECTION_CREATE); - const showSectionEl = this.element.querySelector(constants.SECTION_SHOW); - annotatorUtil.hideElement(createSectionEl); - annotatorUtil.showElement(showSectionEl); - this.hasAnnotations = true; - } - - this.addAnnotationElement(annotation); - this.deactivateReply(true); // Deactivate reply area and focus - } - - /** - * Removes an annotation from the dialog. - * - * @param {string} annotationID - ID of annotation to remove - * @return {void} - */ - removeAnnotation(annotationID) { - const annotationEl = this.element.querySelector(`[data-annotation-id="${annotationID}"]`); - if (annotationEl) { - annotationEl.parentNode.removeChild(annotationEl); - this.deactivateReply(); // Deactivate reply area and focus - } - } - - /** - * Posts an annotation in the dialog. - * - * @param {string} [textInput] - Annotation text to post - * @return {void} - */ - postAnnotation(textInput) { - const annotationTextEl = this.element.querySelector(constants.SELECTOR_ANNOTATION_TEXTAREA); - const text = textInput || annotationTextEl.value; - if (text.trim() === '') { - return; - } - - this.emit('annotationcreate', { text }); - annotationTextEl.value = ''; - } - - //-------------------------------------------------------------------------- - // Abstract - //-------------------------------------------------------------------------- - - /** - * Must be implemented to position the dialog on the preview. - * - * @return {void} - */ - position() {} - - //-------------------------------------------------------------------------- - // Protected - //-------------------------------------------------------------------------- - - /** - * Sets up the dialog element. - * - * @param {Annotation[]} annotations - Annotations to show in the dialog - * @param {HTMLElement} threadEl - Annotation icon element - * @return {void} - * @protected - */ - setup(annotations, threadEl) { - this.threadEl = threadEl; - - // Generate HTML of dialog - this.dialogEl = this.generateDialogEl(annotations.length); - this.dialogEl.classList.add(constants.CLASS_ANNOTATION_CONTAINER); - - // Setup shared mobile annotations dialog - if (!this.isMobile) { - this.element = document.createElement('div'); - this.element.setAttribute('data-type', constants.DATA_TYPE_ANNOTATION_DIALOG); - this.element.classList.add(constants.CLASS_ANNOTATION_DIALOG); - this.element.innerHTML = `
`; - this.element.appendChild(this.dialogEl); - - // Adding thread number to dialog - if (annotations.length > 0) { - this.element.dataset.threadNumber = annotations[0].threadNumber; - } - - this.bindDOMListeners(); - } - - // Add annotation elements - annotations.forEach((annotation) => { - this.addAnnotationElement(annotation); - }); - } - - /** - * Binds DOM event listeners. - * - * @protected - * @return {void} - */ - bindDOMListeners() { - this.element.addEventListener('keydown', this.keydownHandler); - this.element.addEventListener('click', this.clickHandler); - this.element.addEventListener('mouseup', this.stopPropagation); - this.element.addEventListener('wheel', this.stopPropagation); - - if (!this.isMobile) { - this.element.addEventListener('mouseenter', this.mouseenterHandler); - this.element.addEventListener('mouseleave', this.mouseleaveHandler); - } - } - - /** - * Unbinds DOM event listeners. - * - * @protected - * @return {void} - */ - unbindDOMListeners() { - this.element.removeEventListener('keydown', this.keydownHandler); - this.element.removeEventListener('click', this.clickHandler); - this.element.removeEventListener('mouseup', this.stopPropagation); - this.element.removeEventListener('wheel', this.stopPropagation); - - if (!this.isMobile) { - this.element.removeEventListener('mouseenter', this.mouseenterHandler); - this.element.removeEventListener('mouseleave', this.mouseleaveHandler); - } - } - - /** - * Mouseenter handler. Clears hide timeout. - * - * @protected - * @return {void} - */ - mouseenterHandler() { - if (this.element.classList.contains(constants.CLASS_HIDDEN)) { - annotatorUtil.showElement(this.element); - - const replyTextArea = this.element.querySelector(`.${CLASS_REPLY_TEXTAREA}`); - const commentsTextArea = this.element.querySelector(constants.SELECTOR_ANNOTATION_TEXTAREA); - if (replyTextArea.textContent !== '' || commentsTextArea.textContent !== '') { - this.emit('annotationcommentpending'); - } - - // Ensure textarea stays open - this.activateReply(); - } - } - - /** - * Mouseleave handler. Hides dialog if we aren't creating the first one. - * - * @protected - * @return {void} - */ - mouseleaveHandler() { - if (this.hasAnnotations) { - this.hide(); - } - } - - //-------------------------------------------------------------------------- - // Private - //-------------------------------------------------------------------------- - - /** - * Keydown handler for dialog. - * - * @private - * @param {Event} event - DOM event - * @return {void} - */ - keydownHandler(event) { - event.stopPropagation(); - - const key = annotatorUtil.decodeKeydown(event); - if (key === 'Escape') { - this.hide(); - } else { - const dataType = annotatorUtil.findClosestDataType(event.target); - if (dataType === CLASS_REPLY_TEXTAREA) { - this.activateReply(); - } - } - } - - /** - * Stops propagation of DOM event. - * - * @private - * @param {Event} event - DOM event - * @return {void} - */ - stopPropagation(event) { - event.stopPropagation(); - } - - /** - * Click handler on dialog. - * - * @private - * @param {Event} event - DOM event - * @return {void} - */ - clickHandler(event) { - event.stopPropagation(); - - const eventTarget = event.target; - const dataType = annotatorUtil.findClosestDataType(eventTarget); - const annotationID = annotatorUtil.findClosestDataType(eventTarget, 'data-annotation-id'); - - switch (dataType) { - // Clicking 'Post' button to create an annotation - case constants.DATA_TYPE_POST: - this.postAnnotation(); - break; - // Clicking 'Cancel' button to cancel the annotation - case constants.DATA_TYPE_CANCEL: - if (this.isMobile) { - this.hide(); - } else { - // Cancels + destroys the annotation thread - this.cancelAnnotation(); - } - - this.deactivateReply(true); - break; - // Clicking inside reply text area - case constants.DATA_TYPE_REPLY_TEXTAREA: - this.activateReply(); - break; - // Canceling a reply - case constants.DATA_TYPE_CANCEL_REPLY: - this.deactivateReply(true); - break; - // Clicking 'Post' button to create a reply annotation - case constants.DATA_TYPE_POST_REPLY: - this.postReply(); - break; - // Clicking trash icon to initiate deletion - case constants.DATA_TYPE_DELETE: - this.showDeleteConfirmation(annotationID); - break; - // Clicking 'Cancel' button to cancel deletion - case constants.DATA_TYPE_CANCEL_DELETE: - this.hideDeleteConfirmation(annotationID); - break; - // Clicking 'Delete' button to confirm deletion - case constants.DATA_TYPE_CONFIRM_DELETE: { - this.deleteAnnotation(annotationID); - break; - } - - default: - break; - } - } - - /** - * Adds an annotation to the dialog. - * - * @private - * @param {Annotation} annotation - Annotation to add - * @return {void} - */ - addAnnotationElement(annotation) { - const userId = annotatorUtil.htmlEscape(annotation.user.id || '0'); - - // Temporary until annotation user API is available - let userName; - if (userId === '0') { - userName = this.localized.posting; - } else { - userName = annotatorUtil.htmlEscape(annotation.user.name) || this.localized.anonymousUserName; - } - - const avatarUrl = annotatorUtil.htmlEscape(annotation.user.avatarUrl || ''); - const avatarHtml = annotatorUtil.getAvatarHtml(avatarUrl, userId, userName, this.localized.profileAlt); - const created = new Date(annotation.created).toLocaleString(this.locale, { - month: '2-digit', - day: '2-digit', - year: 'numeric', - hour: '2-digit', - minute: '2-digit' - }); - const text = annotatorUtil.htmlEscape(annotation.text); - - const annotationEl = document.createElement('div'); - annotationEl.classList.add(CLASS_COMMENT); - annotationEl.setAttribute('data-annotation-id', annotation.annotationID); - annotationEl.innerHTML = ` -
${avatarHtml}
-
-
${userName}
-
${created}
-
-
${text}
- -
-
- ${this.localized.deleteConfirmation} -
-
- - -
-
`.trim(); - - const annotationContainerEl = this.dialogEl.querySelector(`.${CLASS_COMMENTS_CONTAINER}`); - annotationContainerEl.appendChild(annotationEl); - } - - /** - * Cancels posting an annotation. - * - * @private - * @return {void} - */ - cancelAnnotation() { - this.emit('annotationcancel'); - } - - /** - * Activates reply textarea. - * - * @private - * @return {void} - */ - activateReply() { - if (!this.dialogEl) { - return; - } - - const replyTextEl = this.dialogEl.querySelector(`.${CLASS_REPLY_TEXTAREA}`); - - // Don't activate if reply textarea is already active - const isActive = replyTextEl.classList.contains(constants.CLASS_ACTIVE); - if (isActive) { - return; - } - - const replyButtonEls = replyTextEl.parentNode.querySelector(constants.SELECTOR_BUTTON_CONTAINER); - replyTextEl.classList.add(constants.CLASS_ACTIVE); - annotatorUtil.showElement(replyButtonEls); - - // Auto scroll annotations dialog to bottom where new comment was added - const annotationsEl = this.dialogEl.querySelector(constants.SELECTOR_ANNOTATION_CONTAINER); - if (annotationsEl) { - annotationsEl.scrollTop = annotationsEl.scrollHeight - annotationsEl.clientHeight; - } - } - - /** - * Deactivate reply textarea. - * - * @private - * @param {boolean} clearText - Whether or not text in text area should be cleared - * @return {void} - */ - deactivateReply(clearText) { - if (!this.dialogEl) { - return; - } - - const replyContainerEl = this.dialogEl.querySelector(`.${CLASS_REPLY_CONTAINER}`); - const replyTextEl = replyContainerEl.querySelector(`.${CLASS_REPLY_TEXTAREA}`); - const replyButtonEls = replyContainerEl.querySelector(constants.SELECTOR_BUTTON_CONTAINER); - annotatorUtil.resetTextarea(replyTextEl, clearText); - annotatorUtil.hideElement(replyButtonEls); - - // Only focus on textarea if dialog is visible - if (annotatorUtil.isElementInViewport(replyTextEl)) { - replyTextEl.focus(); - } - - // Auto scroll annotations dialog to bottom where new comment was added - const annotationsEl = this.dialogEl.querySelector(constants.SELECTOR_ANNOTATION_CONTAINER); - if (annotationsEl) { - annotationsEl.scrollTop = annotationsEl.scrollHeight - annotationsEl.clientHeight; - } - } - - /** - * Posts a reply in the dialog. - * - * @private - * @return {void} - */ - postReply() { - const replyTextEl = this.element.querySelector(`.${CLASS_REPLY_TEXTAREA}`); - const text = replyTextEl.value; - if (text.trim() === '') { - return; - } - - this.emit('annotationcreate', { text }); - replyTextEl.value = ''; - } - - /** - * Shows delete confirmation. - * - * @private - * @param {string} annotationID - ID of annotation to delete - * @return {void} - */ - showDeleteConfirmation(annotationID) { - const annotationEl = this.element.querySelector(`[data-annotation-id="${annotationID}"]`); - const deleteConfirmationEl = annotationEl.querySelector(`.${CLASS_DELETE_CONFIRMATION}`); - const cancelDeleteButtonEl = annotationEl.querySelector(`.${CLASS_CANCEL_DELETE}`); - const deleteButtonEl = annotationEl.querySelector(`.${CLASS_BUTTON_DELETE_COMMENT}`); - annotatorUtil.hideElement(deleteButtonEl); - annotatorUtil.showElement(deleteConfirmationEl); - cancelDeleteButtonEl.focus(); - } - - /** - * Hides delete confirmation. - * - * @private - * @param {string} annotationID - ID of annotation to delete - * @return {void} - */ - hideDeleteConfirmation(annotationID) { - const annotationEl = this.element.querySelector(`[data-annotation-id="${annotationID}"]`); - const deleteConfirmationEl = annotationEl.querySelector(`.${CLASS_DELETE_CONFIRMATION}`); - const deleteButtonEl = annotationEl.querySelector(`.${CLASS_BUTTON_DELETE_COMMENT}`); - annotatorUtil.showElement(deleteButtonEl); - annotatorUtil.hideElement(deleteConfirmationEl); - deleteButtonEl.focus(); - } - - /** - * Broadcasts message to delete an annotation. - * - * @private - * @param {string} annotationID - ID of annotation to delete - * @return {void} - */ - deleteAnnotation(annotationID) { - this.emit('annotationdelete', { annotationID }); - } - - /** - * Generates the annotation dialog DOM element - * - * @private - * @param {number} numAnnotations - length of annotations array - * @return {HTMLElement} Annotation dialog DOM element - */ - generateDialogEl(numAnnotations) { - const dialogEl = document.createElement('div'); - dialogEl.innerHTML = ` -
- -
- - -
-
-
-
-
- -
- - -
-
-
`.trim(); - return dialogEl; - } - - /** - * Flip the annotations dialog if the dialog would appear in the lower - * half of the viewer - * - * @private - * @param {number} yPos - y coordinate for the top of the dialog - * @param {number} containerHeight - height of the current annotation - * container/page - * @return {void} - */ - flipDialog(yPos, containerHeight) { - let top = ''; - let bottom = ''; - const iconPadding = POINT_ANNOTATION_ICON_HEIGHT - POINT_ANNOTATION_ICON_DOT_HEIGHT / 2; - const annotationCaretEl = this.element.querySelector(constants.SELECTOR_ANNOTATION_CARET); - - if (yPos <= containerHeight / 2) { - // Keep dialog below the icon if in the top half of the viewport - top = `${yPos - POINT_ANNOTATION_ICON_DOT_HEIGHT}px`; - bottom = ''; - - this.element.classList.remove(CLASS_FLIPPED_DIALOG); - - annotationCaretEl.style.bottom = ''; - } else { - // Flip dialog to above the icon if in the lower half of the viewport - const flippedY = containerHeight - yPos - iconPadding; - top = ''; - bottom = `${flippedY}px`; - - this.element.classList.add(CLASS_FLIPPED_DIALOG); - - // Adjust dialog caret - annotationCaretEl.style.top = ''; - annotationCaretEl.style.bottom = '0px'; - } - - this.fitDialogHeightInPage(); - this.toggleFlippedThreadEl(); - return { top, bottom }; - } - - /** - * Show/hide the top portion of the annotations icon based on if the - * entire dialog is flipped - * - * @private - * @return {void} - */ - toggleFlippedThreadEl() { - if (!this.element || !this.threadEl) { - return; - } - - const isDialogFlipped = this.element.classList.contains(CLASS_FLIPPED_DIALOG); - if (!isDialogFlipped) { - return; - } - - if (this.element.classList.contains(constants.CLASS_HIDDEN)) { - this.threadEl.classList.remove(CLASS_FLIPPED_DIALOG); - } else { - this.threadEl.classList.add(CLASS_FLIPPED_DIALOG); - } - } - - /** - * Set max height for dialog to prevent the dialog from being cut off - * - * @private - * @return {void} - */ - fitDialogHeightInPage() { - this.dialogEl.style.maxHeight = `${this.container.clientHeight / 2}px`; - } -} - -export default AnnotationDialog; diff --git a/src/lib/annotations/AnnotationModeController.js b/src/lib/annotations/AnnotationModeController.js deleted file mode 100644 index abaa7382f..000000000 --- a/src/lib/annotations/AnnotationModeController.js +++ /dev/null @@ -1,179 +0,0 @@ -import EventEmitter from 'events'; -import { insertTemplate } from './annotatorUtil'; - -class AnnotationModeController extends EventEmitter { - /** @property {Array} - The array of annotation threads */ - threads = []; - - /** @property {Array} - The array of annotation handlers */ - handlers = []; - - /** - * Register the annotator and any information associated with the annotator - * - * @public - * @param {Annotator} annotator - The annotator to be associated with the controller - * @return {void} - */ - registerAnnotator(annotator) { - // TODO (@minhnguyen): remove the need to register an annotator. Ideally, the annotator should know about the - // controller and the controller does not know about the annotator. - this.annotator = annotator; - } - - /** - * Bind the mode listeners and store each handler for future unbinding - * - * @public - * @return {void} - */ - bindModeListeners() { - const currentHandlerIndex = this.handlers.length; - this.setupHandlers(); - - for (let index = currentHandlerIndex; index < this.handlers.length; index++) { - const handler = this.handlers[index]; - const types = handler.type instanceof Array ? handler.type : [handler.type]; - - types.forEach((eventName) => handler.eventObj.addEventListener(eventName, handler.func)); - } - } - - /** - * Unbind the previously bound mode listeners - * - * @public - * @return {void} - */ - unbindModeListeners() { - while (this.handlers.length > 0) { - const handler = this.handlers.pop(); - const types = handler.type instanceof Array ? handler.type : [handler.type]; - - types.forEach((eventName) => { - handler.eventObj.removeEventListener(eventName, handler.func); - }); - } - } - - /** - * Register a thread with the controller so that the controller can keep track of relevant threads - * - * @public - * @param {AnnotationThread} thread - The thread to register with the controller - * @return {void} - */ - registerThread(thread) { - this.threads.push(thread); - } - - /** - * Unregister a previously registered thread - * - * @public - * @param {AnnotationThread} thread - The thread to unregister with the controller - * @return {void} - */ - unregisterThread(thread) { - this.threads = this.threads.filter((item) => item !== thread); - } - - /** - * Clean up any selected annotations - * - * @return {void} - */ - removeSelection() {} - - /** - * Binds custom event listeners for a thread. - * - * @protected - * @param {AnnotationThread} thread - Thread to bind events to - * @return {void} - */ - bindCustomListenersOnThread(thread) { - if (!thread) { - return; - } - - // TODO (@minhnguyen): Move annotator.bindCustomListenersOnThread logic to AnnotationModeController - this.annotator.bindCustomListenersOnThread(thread); - thread.addListener('threadevent', (data) => { - this.handleAnnotationEvent(thread, data); - }); - } - - /** - * Unbinds custom event listeners for the thread. - * - * @protected - * @param {AnnotationThread} thread - Thread to unbind events from - * @return {void} - */ - unbindCustomListenersOnThread(thread) { - if (!thread) { - return; - } - - thread.removeAllListeners('threadevent'); - } - - /** - * Set up and return the necessary handlers for the annotation mode - * - * @protected - * @return {Array} An array where each element is an object containing the object that will emit the event, - * the type of events to listen for, and the callback - */ - setupHandlers() {} - - /** - * Handle an annotation event. - * - * @protected - * @param {AnnotationThread} thread - The thread that emitted the event - * @param {Object} data - Extra data related to the annotation event - * @return {void} - */ - /* eslint-disable no-unused-vars */ - handleAnnotationEvent(thread, data = {}) {} - /* eslint-enable no-unused-vars */ - - /** - * Creates a handler description object and adds its to the internal handler container. - * Useful for setupAndGetHandlers. - * - * @protected - * @param {HTMLElement} element - The element to bind the listener to - * @param {Array|string} type - An array of event types to listen for or the event name to listen for - * @param {Function} handlerFn - The callback to be invoked when the element emits a specified eventname - * @return {void} - */ - pushElementHandler(element, type, handlerFn) { - if (!element) { - return; - } - - this.handlers.push({ - eventObj: element, - func: handlerFn, - type - }); - } - - /** - * Setups the header for the annotation mode - * - * @protected - * @param {HTMLElement} container - Container element - * @param {HTMLElement} header - Header to add to DOM - * @return {void} - */ - setupHeader(container, header) { - const baseHeaderEl = container.firstElementChild; - insertTemplate(container, header, baseHeaderEl); - } -} - -export default AnnotationModeController; diff --git a/src/lib/annotations/AnnotationService.js b/src/lib/annotations/AnnotationService.js deleted file mode 100644 index 47a647bf2..000000000 --- a/src/lib/annotations/AnnotationService.js +++ /dev/null @@ -1,321 +0,0 @@ -import 'whatwg-fetch'; -import EventEmitter from 'events'; -import autobind from 'autobind-decorator'; -import Annotation from './Annotation'; -import { getHeaders } from './annotatorUtil'; - -@autobind -class AnnotationService extends EventEmitter { - //-------------------------------------------------------------------------- - // Static - //-------------------------------------------------------------------------- - - /** - * Generates a rfc4122v4-compliant GUID, from - * http://stackoverflow.com/questions/105034/create-guid-uuid-in-javascript. - * - * @return {string} UUID for annotation - */ - static generateID() { - /* eslint-disable */ - return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, c => { - var r = (Math.random() * 16) | 0, - v = c == 'x' ? r : (r & 0x3) | 0x8; - return v.toString(16); - }); - /* eslint-enable */ - } - - //-------------------------------------------------------------------------- - // Typedef - //-------------------------------------------------------------------------- - - /** - * The data object for constructing an Annotation Service. - * @typedef {Object} AnnotationServiceData - * @property {string} apiHost API root - * @property {string} fileId File ID - * @property {string} token Access token - * @property {boolean} canAnnotate Can user annotate - */ - - //-------------------------------------------------------------------------- - // Public - //-------------------------------------------------------------------------- - - /** - * [constructor] - * - * @param {AnnotationServiceData} data - Annotation Service data - * @return {AnnotationService} AnnotationService instance - */ - constructor(data) { - super(); - this.api = data.apiHost; - this.fileId = data.fileId; - this.headers = getHeaders({}, data.token); - this.canAnnotate = data.canAnnotate; - this.user = { - id: '0', - name: this.anonymousUserName - }; - } - - /** - * Create an annotation. - * - * @param {Annotation} annotation - Annotation to save - * @return {Promise} Promise that resolves with created annotation - */ - create(annotation) { - return new Promise((resolve, reject) => { - fetch(`${this.api}/2.0/annotations`, { - method: 'POST', - headers: this.headers, - body: JSON.stringify({ - item: { - type: 'file_version', - id: annotation.fileVersionId - }, - details: { - type: annotation.type, - drawingPaths: annotation.drawingPaths, - location: annotation.location, - threadID: annotation.threadID - }, - message: annotation.text, - thread: annotation.threadNumber - }) - // NOTE: Ensure that threadNumbers are sent to the API as - // thread, else the API created annotation will have an - // incremented threadNumber. This is due to the naming system - // in the annotations API - }) - .then((response) => response.json()) - .then((data) => { - if (data.type !== 'error' && data.id) { - const tempData = data; - tempData.permissions = { - can_edit: true, - can_delete: true - }; - const createdAnnotation = this.createAnnotation(tempData); - - // Set user if not set already - if (this.user.id === '0') { - this.user = createdAnnotation.user; - } - - resolve(createdAnnotation); - } else { - const error = new Error('Could not create annotation'); - reject(error); - this.emit('annotationerror', { - reason: 'create', - error: error.toString() - }); - } - }) - /* istanbul ignore next */ - .catch((error) => { - reject(new Error('Could not create annotation due to invalid or expired token')); - this.emit('annotationerror', { - reason: 'authorization', - error: error.toString() - }); - }); - }); - } - - /** - * Reads annotations from file version ID. - * - * @param {string} fileVersionId - File version ID to fetch annotations for - * @return {Promise} Promise that resolves with fetched annotations - */ - read(fileVersionId) { - this.annotations = []; - let resolve; - let reject; - const promise = new Promise((success, failure) => { - resolve = success; - reject = failure; - }); - - this.readFromMarker(resolve, reject, fileVersionId); - return promise; - } - - /** - * Delete an annotation. - * - * @param {string} annotationID - Id of annotation to delete - * @return {Promise} Promise to delete annotation - */ - delete(annotationID) { - return new Promise((resolve, reject) => { - fetch(`${this.api}/2.0/annotations/${annotationID}`, { - method: 'DELETE', - headers: this.headers - }) - .then((response) => { - if (response.status === 204) { - resolve(); - } else { - const error = new Error(`Could not delete annotation with ID ${annotationID}`); - reject(error); - this.emit('annotationerror', { - reason: 'delete', - error: error.toString() - }); - } - }) - /* istanbul ignore next */ - .catch((error) => { - reject(new Error('Could not delete annotation due to invalid or expired token')); - this.emit('annotationerror', { - reason: 'authorization', - error: error.toString() - }); - }); - }); - } - - /** - * Gets a map of thread ID to annotations in that thread. - * - * @param {string} fileVersionId - File version ID to fetch annotations for - * @return {Promise} Promise that resolves with thread map - */ - getThreadMap(fileVersionId) { - return this.read(fileVersionId).then(this.createThreadMap); - } - - //-------------------------------------------------------------------------- - // Private - //-------------------------------------------------------------------------- - - /** - * Generates a map of thread ID to annotations in thread. - * - * @private - * @param {Annotation[]} annotations - Annotations to generate map from - * @return {Object} Map of thread ID to annotations in that thread - */ - createThreadMap(annotations) { - const threadMap = {}; - - // Construct map of thread ID to annotations - annotations.forEach((annotation) => { - const threadID = annotation.threadID; - threadMap[threadID] = threadMap[threadID] || []; - threadMap[threadID].push(annotation); - }); - - // Sort annotations by date created - Object.keys(threadMap).forEach((threadID) => { - threadMap[threadID].sort((a, b) => { - return new Date(a.created) - new Date(b.created); - }); - }); - - return threadMap; - } - - /** - * Generates an Annotation object from an API response. - * - * @private - * @param {Object} data - API response data - * @return {Annotation} Created annotation - */ - createAnnotation(data) { - return new Annotation({ - annotationID: data.id, - fileVersionId: data.item.id, - threadID: data.details.threadID, - type: data.details.type, - threadNumber: data.thread, - text: data.message, - location: data.details.location, - user: { - id: data.created_by.id, - name: data.created_by.name, - avatarUrl: data.created_by.profile_image - }, - permissions: data.permissions, - created: data.created_at, - modified: data.modified_at - }); - } - - /** - * Construct the URL to read annotations with a marker or limit added - * - * @private - * @param {string} fileVersionId - File version ID to fetch annotations for - * @param {string} marker - Marker to use if there are more than limit annotations - * @param {int} limit - The amout of annotations the API will return per call - * @return {Promise} Promise that resolves with fetched annotations - */ - getReadUrl(fileVersionId, marker = null, limit = null) { - let apiUrl = `${this.api}/2.0/files/${this - .fileId}/annotations?version=${fileVersionId}&fields=item,thread,details,message,created_by,created_at,modified_at,permissions`; - if (marker) { - apiUrl += `&marker=${marker}`; - } - - if (limit) { - apiUrl += `&limit=${limit}`; - } - - return apiUrl; - } - - /** - * Reads annotations from file version ID starting at a marker. The default - * limit is 100 annotations per API call. - * - * @private - * @param {Function} resolve - Promise resolution handler - * @param {Function} reject - Promise rejection handler - * @param {string} fileVersionId - File version ID to fetch annotations for - * @param {string} marker - Marker to use if there are more than limit annotations - * @param {int} limit - The amout of annotations the API will return per call - * @return {void} - */ - readFromMarker(resolve, reject, fileVersionId, marker = null, limit = null) { - fetch(this.getReadUrl(fileVersionId, marker, limit), { - headers: this.headers - }) - .then((response) => response.json()) - .then((data) => { - if (data.type === 'error' || !Array.isArray(data.entries)) { - const error = new Error(`Could not read annotations from file version with ID ${fileVersionId}`); - reject(error); - this.emit('annotationerror', { - reason: 'read', - error: error.toString() - }); - } else { - data.entries.forEach((annotationData) => { - this.annotations.push(this.createAnnotation(annotationData)); - }); - - if (data.next_marker) { - this.readFromMarker(resolve, reject, fileVersionId, data.next_marker, limit); - } else { - resolve(this.annotations); - } - } - }) - .catch((error) => { - reject(new Error('Could not read annotations from file due to invalid or expired token')); - this.emit('annotationerror', { - reason: 'authorization', - error: error.toString() - }); - }); - } -} -export default AnnotationService; diff --git a/src/lib/annotations/AnnotationThread.js b/src/lib/annotations/AnnotationThread.js deleted file mode 100644 index 5b89810ac..000000000 --- a/src/lib/annotations/AnnotationThread.js +++ /dev/null @@ -1,655 +0,0 @@ -import EventEmitter from 'events'; -import autobind from 'autobind-decorator'; -import Annotation from './Annotation'; -import AnnotationService from './AnnotationService'; -import * as annotatorUtil from './annotatorUtil'; -import { ICON_PLACED_ANNOTATION } from '../icons/icons'; -import { - STATES, - TYPES, - CLASS_ANNOTATION_POINT_MARKER, - DATA_TYPE_ANNOTATION_INDICATOR, - THREAD_EVENT -} from './annotationConstants'; - -@autobind -class AnnotationThread extends EventEmitter { - //-------------------------------------------------------------------------- - // Typedef - //-------------------------------------------------------------------------- - - /** - * The data object for constructing a thread. - * @typedef {Object} AnnotationThreadData - * @property {HTMLElement} annotatedElement HTML element being annotated on - * @property {Annotation[]} [annotations] Annotations in thread - none if - * this is a new thread - * @property {AnnotationService} annotationService Annotations CRUD service - * @property {string} fileVersionId File version ID - * @property {Object} location Location object - * @property {string} threadID Thread ID - * @property {string} thread Thread number - * @property {string} type Type of thread - */ - - //-------------------------------------------------------------------------- - // Public - //-------------------------------------------------------------------------- - - /** - * [constructor] - * - * @param {AnnotationThreadData} data - Data for constructing thread - * @return {AnnotationThread} Annotation thread instance - */ - constructor(data) { - super(); - - this.annotatedElement = data.annotatedElement; - this.annotations = data.annotations || []; - this.annotationService = data.annotationService; - this.container = data.container; - this.fileVersionId = data.fileVersionId; - this.location = data.location; - this.threadID = data.threadID || AnnotationService.generateID(); - this.threadNumber = data.threadNumber || ''; - this.type = data.type; - this.locale = data.locale; - this.isMobile = data.isMobile; - this.hasTouch = data.hasTouch; - this.permissions = data.permissions; - this.localized = data.localized; - - this.setup(); - } - - /** - * [destructor] - * - * @return {void} - */ - destroy() { - if (this.dialog && !this.isMobile) { - this.unbindCustomListenersOnDialog(); - this.dialog.destroy(); - } - - if (this.element) { - this.unbindDOMListeners(); - - if (this.element.parentNode) { - this.element.parentNode.removeChild(this.element); - } - - this.element = null; - } - - this.emit(THREAD_EVENT.threadDelete); - } - - /** - * Hides the annotation indicator. - * - * @return {void} - */ - hide() { - annotatorUtil.hideElement(this.element); - } - - /** - * Reset state to inactive. - * - * @return {void} - */ - reset() { - this.state = STATES.inactive; - } - - /** - * Shows the appropriate annotation dialog for this thread. - * - * @return {void} - */ - showDialog() { - // Prevents the annotations dialog from being created each mousemove - if (!this.dialog.element) { - this.dialog.setup(this.annotations, this.element); - } - - this.dialog.show(); - } - - /** - * Hides the appropriate annotation dialog for this thread. - * - * @return {void} - */ - hideDialog() { - if (this.dialog) { - this.state = STATES.inactive; - this.dialog.hide(); - } - } - - /** - * Saves an annotation. - * - * @param {string} type - Type of annotation - * @param {string} text - Text of annotation to save - * @return {Promise} - Annotation create promise - */ - saveAnnotation(type, text) { - const annotationData = this.createAnnotationData(type, text); - - // Save annotation on client - const tempAnnotationID = AnnotationService.generateID(); - const tempAnnotationData = annotationData; - tempAnnotationData.annotationID = tempAnnotationID; - tempAnnotationData.permissions = { - can_edit: true, - can_delete: true - }; - tempAnnotationData.created = new Date().getTime(); - tempAnnotationData.modified = tempAnnotationData.created; - const tempAnnotation = new Annotation(tempAnnotationData); - this.saveAnnotationToThread(tempAnnotation); - - // Changing state from pending - this.state = STATES.hover; - - // Save annotation on server - return this.annotationService - .create(annotationData) - .then((savedAnnotation) => this.updateTemporaryAnnotation(tempAnnotation, savedAnnotation)) - .catch((error) => this.handleThreadSaveError(error, tempAnnotationID)); - } - - /** - * Deletes an annotation. - * - * @param {string} annotationID - ID of annotation to delete - * @param {boolean} [useServer] - Whether or not to delete on server, default true - * @return {Promise} - Annotation delete promise - */ - deleteAnnotation(annotationID, useServer = true) { - // Ignore if no corresponding annotation exists in thread or user doesn't have permissions - const annotation = this.annotations.find((annot) => annot.annotationID === annotationID); - if (!annotation) { - // Broadcast error - this.emit(THREAD_EVENT.deleteError); - /* eslint-disable no-console */ - console.error( - THREAD_EVENT.deleteError, - `Annotation with ID ${annotation.threadNumber} could not be found.` - ); - /* eslint-enable no-console */ - return Promise.reject(); - } - - if (annotation.permissions && !annotation.permissions.can_delete) { - // Broadcast error - this.emit(THREAD_EVENT.deleteError); - /* eslint-disable no-console */ - console.error( - THREAD_EVENT.deleteError, - `User does not have the correct permissions to delete annotation with ID ${annotation.threadNumber}.` - ); - /* eslint-enable no-console */ - return Promise.reject(); - } - - // Delete annotation on client - this.annotations = this.annotations.filter((annot) => annot.annotationID !== annotationID); - - // If the user doesn't have permission to delete the entire highlight - // annotation, display the annotation as a plain highlight - let canDeleteAnnotation = - this.annotations.length > 0 && - this.annotations[0].permissions && - this.annotations[0].permissions.can_delete; - if (annotatorUtil.isPlainHighlight(this.annotations) && !canDeleteAnnotation) { - this.cancelFirstComment(); - - // If this annotation was the last one in the thread, destroy the thread - } else if (this.annotations.length === 0 || annotatorUtil.isPlainHighlight(this.annotations)) { - if (this.isMobile && this.dialog) { - this.dialog.hideMobileDialog(); - this.dialog.removeAnnotation(annotationID); - } - this.destroy(); - - // Otherwise, remove deleted annotation from dialog - } else if (this.dialog) { - this.dialog.removeAnnotation(annotationID); - } - - if (!useServer) { - /* eslint-disable no-console */ - console.error( - THREAD_EVENT.deleteError, - `Annotation with ID ${annotation.threadNumber} not deleted from server` - ); - /* eslint-enable no-console */ - return Promise.resolve(); - } - - // Delete annotation on server - return this.annotationService - .delete(annotationID) - .then(() => { - // Ensures that blank highlight comment is also deleted when removing - // the last comment on a highlight - canDeleteAnnotation = - this.annotations.length > 0 && - this.annotations[0].permissions && - this.annotations[0].permissions.can_delete; - if (annotatorUtil.isPlainHighlight(this.annotations) && canDeleteAnnotation) { - this.annotationService.delete(this.annotations[0].annotationID); - } - - // Broadcast thread cleanup if needed - if (this.annotations.length === 0) { - this.emit(THREAD_EVENT.threadCleanup); - } - - // Broadcast annotation deletion event - this.emit(THREAD_EVENT.delete); - }) - .catch((error) => { - // Broadcast error - this.emit(THREAD_EVENT.deleteError); - /* eslint-disable no-console */ - console.error(THREAD_EVENT.deleteError, error.toString()); - /* eslint-enable no-console */ - }); - } - - //-------------------------------------------------------------------------- - // Abstract - //-------------------------------------------------------------------------- - - /** - * Cancels the first comment on the thread - * - * @return {void} - */ - cancelFirstComment() {} - - /** - * Must be implemented to show the annotation indicator. - * - * @return {void} - */ - show() {} - - /** - * Must be implemented to create the appropriate annotation dialog and save - * as a property on the thread. - * - * @return {void} - */ - createDialog() {} - - //-------------------------------------------------------------------------- - // Protected - //-------------------------------------------------------------------------- - - /** - * Sets up the thread. Creates HTML for annotation indicator, sets - * appropriate dialog, and binds event listeners. - * - * @protected - * @return {void} - */ - setup() { - if (this.annotations.length === 0) { - this.state = STATES.pending; - } else { - this.state = STATES.inactive; - } - - this.createDialog(); - this.bindCustomListenersOnDialog(); - - if (this.dialog) { - this.dialog.isMobile = this.isMobile; - this.dialog.localized = this.localized; - } - - this.setupElement(); - } - - /** - * Sets up indicator element. - * - * @protected - * @return {void} - */ - setupElement() { - this.element = this.createElement(); - this.bindDOMListeners(); - } - - /** - * Binds DOM event listeners for the thread. - * - * @protected - * @return {void} - */ - bindDOMListeners() { - if (!this.element) { - return; - } - - this.element.addEventListener('click', this.showDialog); - this.element.addEventListener('mouseenter', this.showDialog); - - if (!this.isMobile) { - this.element.addEventListener('mouseleave', this.mouseoutHandler); - } - } - - /** - * Unbinds DOM event listeners for the thread. - * - * @protected - * @return {void} - */ - unbindDOMListeners() { - if (!this.element) { - return; - } - - this.element.removeEventListener('click', this.showDialog); - this.element.removeEventListener('mouseenter', this.showDialog); - - if (!this.isMobile) { - this.element.removeEventListener('mouseleave', this.mouseoutHandler); - } - } - - /** - * Binds custom event listeners for the dialog. - * - * @protected - * @return {void} - */ - bindCustomListenersOnDialog() { - if (!this.dialog) { - return; - } - - this.dialog.addListener('annotationcreate', this.createAnnotation); - this.dialog.addListener('annotationcancel', this.cancelUnsavedAnnotation); - this.dialog.addListener('annotationdelete', this.deleteAnnotationWithID); - } - - /** - * Unbinds custom event listeners for the dialog. - * - * @protected - * @return {void} - */ - unbindCustomListenersOnDialog() { - if (!this.dialog) { - return; - } - - this.dialog.removeAllListeners('annotationcreate'); - this.dialog.removeAllListeners('annotationcancel'); - this.dialog.removeAllListeners('annotationdelete'); - } - - /** - * Destroys mobile and pending/pending-active annotation threads - * - * @protected - * @return {void} - */ - cancelUnsavedAnnotation() { - if (!annotatorUtil.isPending(this.state)) { - return; - } - - this.emit(THREAD_EVENT.cancel); - this.destroy(); - } - - //-------------------------------------------------------------------------- - // Private - //-------------------------------------------------------------------------- - - /** - * Scroll annotation into the center of the viewport, if possible - * - * @private - * @return {void} - */ - scrollIntoView() { - const yPos = parseInt(this.location.y, 10); - this.scrollToPage(); - this.centerAnnotation(this.annotatedElement.scrollTop + yPos); - } - - /** - * Scroll to the annotation's page - * - * @private - * @return {void} - */ - scrollToPage() { - // Ignore if annotation does not have a location or page - if (!this.location || !this.location.page) { - return; - } - - const pageEl = this.annotatedElement.querySelector(`[data-page-number="${this.location.page}"]`); - pageEl.scrollIntoView(); - } - - /** - * Adjust page scroll position so annotation is centered in viewport - * - * @private - * @param {number} scrollVal - scroll value to adjust so annotation is - centered in the viewport - * @return {void} - */ - centerAnnotation(scrollVal) { - if (scrollVal < this.annotatedElement.scrollHeight) { - this.annotatedElement.scrollTop = scrollVal; - } else { - this.annotatedElement.scrollTop = this.annotatedElement.scrollBottom; - } - } - - /** - * Update a temporary annotation with the annotation saved on the backend. Set the threadNumber if it has not - * yet been set. Propogate the threadnumber to an attached dialog if applicable. - * - * @private - * @param {Annotation} tempAnnotation - The locally stored placeholder for the server validated annotation - * @param {Annotation} savedAnnotation - The annotation determined by the backend to be used as the source of truth - * @return {void} - */ - updateTemporaryAnnotation(tempAnnotation, savedAnnotation) { - const tempIdx = this.annotations.indexOf(tempAnnotation); - if (tempIdx === -1) { - // If no temporary annotation is found, save to thread normally - this.saveAnnotationToThread(savedAnnotation); - } else { - // Otherwise, replace temporary annotation with annotation saved to server - this.annotations[tempIdx] = savedAnnotation; - } - - // Set threadNumber if the savedAnnotation is the first annotation of the thread - if (!this.threadNumber && savedAnnotation && savedAnnotation.threadNumber) { - this.threadNumber = savedAnnotation.threadNumber; - } - - if (this.dialog) { - // Add thread number to associated dialog and thread - if (this.dialog.element && this.dialog.element.dataset) { - this.dialog.element.dataset.threadNumber = this.threadNumber; - } - - // Remove temporary annotation and replace it with the saved annotation - this.dialog.addAnnotation(savedAnnotation); - this.dialog.removeAnnotation(tempAnnotation.annotationID); - } - - this.showDialog(); - this.emit(THREAD_EVENT.save); - } - - /** - * Creates the HTML for the annotation indicator. - * - * @private - * @return {HTMLElement} HTML element - */ - createElement() { - const indicatorEl = document.createElement('button'); - indicatorEl.classList.add(CLASS_ANNOTATION_POINT_MARKER); - indicatorEl.setAttribute('data-type', DATA_TYPE_ANNOTATION_INDICATOR); - indicatorEl.innerHTML = ICON_PLACED_ANNOTATION; - return indicatorEl; - } - - /** - * Mouseout handler. Hides dialog if we aren't creating the first one. - * - * @private - * @param {HTMLEvent} event - DOM event - * @return {void} - */ - mouseoutHandler(event) { - if (!event) { - return; - } - - const mouseInDialog = annotatorUtil.isInDialog(event, this.dialog.element); - - if (this.annotations.length !== 0 && !mouseInDialog) { - this.hideDialog(); - } - } - - /** - * Saves the provided annotation to the thread and dialog if appropriate - * and resets state to inactive. - * - * @private - * @param {Annotation} annotation - Annotation to save - * @return {void} - */ - saveAnnotationToThread(annotation) { - this.annotations.push(annotation); - - if (this.dialog) { - this.dialog.addAnnotation(annotation); - } - } - - /** - * Create an annotation data object to pass to annotation service. - * - * @private - * @param {string} type - Type of annotation - * @param {string} text - Annotation text - * @return {Object} Annotation data - */ - createAnnotationData(type, text) { - return { - fileVersionId: this.fileVersionId, - type, - text, - location: this.location, - user: this.annotationService.user, - threadID: this.threadID, - threadNumber: this.threadNumber - }; - } - - /** - * Creates a new point annotation - * - * @private - * @param {Object} data - Annotation data - * @return {void} - */ - createAnnotation(data) { - this.saveAnnotation(TYPES.point, data.text); - } - - /** - * Deletes annotation with annotationID from thread - * - * @private - * @param {Object} data - Annotation data - * @return {void} - */ - deleteAnnotationWithID(data) { - this.deleteAnnotation(data.annotationID); - } - - /** - * Deletes the temporary annotation if the annotation failed to save on the server - * - * @private - * @param {error} error - error thrown while saving the annotation - * @param {string} tempAnnotationID - ID of temporary annotation to be updated with annotation from server - * @return {void} - */ - handleThreadSaveError(error, tempAnnotationID) { - // Remove temporary annotation - this.deleteAnnotation(tempAnnotationID, /* useServer */ false); - - // Broadcast error - this.emit(THREAD_EVENT.createError); - - /* eslint-disable no-console */ - console.error(THREAD_EVENT.createError, error.toString()); - /* eslint-enable no-console */ - } - - /** - * Generate threadData with relevant information to be emitted with an - * annotation thread event - * - * @private - * @return {Object} threadData - Annotation event thread data - */ - getThreadEventData() { - const threadData = { - type: this.type, - threadID: this.threadID - }; - - if (this.annotationService.user.id > 0) { - threadData.userId = this.annotationService.user.id; - } - if (this.threadNumber) { - threadData.threadNumber = this.threadNumber; - } - - return threadData; - } - - /** - * Emits a generic viewer event - * - * @private - * @emits viewerevent - * @param {string} event - Event name - * @param {Object} eventData - Event data - * @return {void} - */ - emit(event, eventData) { - const threadData = this.getThreadEventData(); - super.emit(event, { data: threadData, eventData }); - super.emit('threadevent', { event, data: threadData, eventData }); - } -} - -export default AnnotationThread; diff --git a/src/lib/annotations/Annotator.js b/src/lib/annotations/Annotator.js deleted file mode 100644 index 6d3d0efa6..000000000 --- a/src/lib/annotations/Annotator.js +++ /dev/null @@ -1,1025 +0,0 @@ -import EventEmitter from 'events'; -import autobind from 'autobind-decorator'; -import AnnotationService from './AnnotationService'; -import * as annotatorUtil from './annotatorUtil'; -import { ICON_CLOSE } from '../icons/icons'; -import './Annotator.scss'; -import { - CLASS_ACTIVE, - CLASS_HIDDEN, - SELECTOR_BOX_PREVIEW_BASE_HEADER, - DATA_TYPE_ANNOTATION_DIALOG, - CLASS_MOBILE_ANNOTATION_DIALOG, - CLASS_ANNOTATION_DIALOG, - CLASS_ANNOTATION_MODE, - CLASS_ANNNOTATION_DRAWING_BACKGROUND, - CLASS_MOBILE_DIALOG_HEADER, - CLASS_DIALOG_CLOSE, - ID_MOBILE_ANNOTATION_DIALOG, - SELECTOR_ANNOTATION_DRAWING_HEADER, - TYPES, - THREAD_EVENT, - ANNOTATOR_EVENT -} from './annotationConstants'; - -@autobind -class Annotator extends EventEmitter { - //-------------------------------------------------------------------------- - // Typedef - //-------------------------------------------------------------------------- - - /** - * The data object for constructing an Annotator. - * @typedef {Object} AnnotatorData - * @property {HTMLElement} annotatedElement HTML element to annotate on - * @property {AnnotationService} [annotationService] Annotations CRUD service - * @property {string} fileVersionId File version ID - */ - - //-------------------------------------------------------------------------- - // Public - //-------------------------------------------------------------------------- - - /** - * [constructor] - * - * @param {Object} options - Options for constructing an Annotator - * @return {Annotator} Annotator instance - */ - constructor(options) { - super(); - - this.options = options; - this.locale = options.location.locale || 'en-US'; - this.validationErrorEmitted = false; - this.isMobile = options.isMobile || false; - this.hasTouch = options.hasTouch || false; - this.annotationModeHandlers = []; - this.localized = options.localizedStrings; - - const { file } = this.options; - this.fileVersionId = file.file_version.id; - this.fileId = file.id; - } - - /** - * [destructor] - * - * @return {void} - */ - destroy() { - this.unbindModeListeners(); - - if (this.threads) { - Object.keys(this.threads).forEach((page) => { - const pageThreads = this.getThreadsOnPage(page); - - Object.keys(pageThreads).forEach((threadID) => { - const thread = pageThreads[threadID]; - this.unbindCustomListenersOnThread(thread); - }); - }); - } - - // Destroy all annotate buttons - Object.keys(this.modeButtons).forEach((type) => { - const handler = this.getAnnotationModeClickHandler(type); - const buttonEl = this.container.querySelector(this.modeButtons[type].selector); - - if (buttonEl) { - buttonEl.removeEventListener('click', handler); - } - }); - - this.unbindDOMListeners(); - this.unbindCustomListenersOnService(); - this.removeListener(ANNOTATOR_EVENT.scale, this.scaleAnnotations); - } - - /** - * Initializes annotator. - * - * @param {number} [initialScale] - The initial scale factor to render the annotations - * @return {void} - */ - init(initialScale = 1) { - // Get the container dom element if selector was passed, in tests - this.container = this.options.container; - if (typeof this.options.container === 'string') { - this.container = document.querySelector(this.options.container); - } - - // Get annotated element from container - this.annotatedElement = this.getAnnotatedEl(this.container); - - this.getAnnotationPermissions(this.options.file); - const { apiHost, file, token } = this.options; - this.annotationService = new AnnotationService({ - apiHost, - fileId: file.id, - token, - canAnnotate: this.permissions.canAnnotate, - anonymousUserName: this.localized.anonymousUserName - }); - - // Set up mobile annotations dialog - if (this.isMobile) { - this.setupMobileDialog(); - } - - // Get applicable annotation mode controllers - const { CONTROLLERS } = this.options.annotator || {}; - this.modeControllers = CONTROLLERS || {}; - - // Show the annotate button for all enabled types for the - // current viewer - this.modeButtons = this.options.modeButtons; - Object.keys(this.modeButtons).forEach((type) => { - this.showModeAnnotateButton(type); - }); - - this.setScale(initialScale); - this.setupAnnotations(); - this.showAnnotations(); - } - - /** - * Returns whether or not the current annotation mode is enabled for - * the current viewer/annotator. - * - * @param {string} type - Type of annotation - * @return {boolean} Whether or not the annotation mode is enabled - */ - isModeAnnotatable(type) { - if (!this.options.annotator) { - return false; - } - - const { TYPE: annotationTypes } = this.options.annotator; - if (type && annotationTypes) { - if (!annotationTypes.some((annotationType) => type === annotationType)) { - return false; - } - } - - return true; - } - - /** - * Shows the annotate button for the specified mode - * - * @param {string} currentMode - Annotation mode - * @return {void} - */ - showModeAnnotateButton(currentMode) { - const mode = this.modeButtons[currentMode]; - if (!mode || !this.permissions.canAnnotate || !this.isModeAnnotatable(currentMode)) { - return; - } - - const annotateButtonEl = this.container.querySelector(mode.selector); - if (annotateButtonEl) { - annotateButtonEl.title = mode.title; - annotateButtonEl.classList.remove(CLASS_HIDDEN); - - const handler = this.getAnnotationModeClickHandler(currentMode); - annotateButtonEl.addEventListener('click', handler); - - if (this.modeControllers[currentMode]) { - this.modeControllers[currentMode].registerAnnotator(this); - } - } - } - - /** - * Gets the annotation button element. - * - * @param {string} annotatorSelector - Class selector for a custom annotation button. - * @return {HTMLElement|null} Annotate button element or null if the selector did not find an element. - */ - getAnnotateButton(annotatorSelector) { - return this.container.querySelector(annotatorSelector); - } - - /** - * Fetches and shows saved annotations. - * - * @return {void} - */ - showAnnotations() { - // Show annotations after we've generated an in-memory map - this.fetchAnnotations().then(this.renderAnnotations); - } - - /** - * Hides annotations. - * - * @return {void} - */ - hideAnnotations() { - Object.keys(this.threads).forEach((pageNum) => { - this.hideAnnotationsOnPage(pageNum); - }); - } - - /** - * Hides annotations on a specified page. - * - * @param {number} pageNum - Page number - * @return {void} - */ - hideAnnotationsOnPage(pageNum) { - if (!this.threads) { - return; - } - - const pageThreads = this.getThreadsOnPage(pageNum); - Object.keys(pageThreads).forEach((threadID) => { - const thread = pageThreads[threadID]; - thread.hide(); - }); - } - - /** - * Sets the zoom scale. - * - * @param {number} scale - current zoom scale - * @return {void} - */ - setScale(scale) { - this.annotatedElement.setAttribute('data-scale', scale); - } - - /** - * Toggles annotation modes on and off. When an annotation mode is - * on, annotation threads will be created at that location. - * - * @param {string} mode - Current annotation mode - * @param {HTMLEvent} event - DOM event - * @return {void} - */ - toggleAnnotationHandler(mode, event = {}) { - if (!this.isModeAnnotatable(mode)) { - return; - } - - this.destroyPendingThreads(); - - if (this.createHighlightDialog.isVisible) { - document.getSelection().removeAllRanges(); - this.createHighlightDialog.hide(); - } - - // No specific mode available for annotation type - if (!(mode in this.modeButtons)) { - return; - } - - const buttonSelector = this.modeButtons[mode].selector; - const buttonEl = event.target || this.getAnnotateButton(buttonSelector); - - // Exit any other annotation mode - this.exitAnnotationModesExcept(mode); - - // If in annotation mode, turn it off - if (this.isInAnnotationMode(mode)) { - this.disableAnnotationMode(mode, buttonEl); - - // Remove annotation mode - this.currentAnnotationMode = null; - } else { - this.enableAnnotationMode(mode, buttonEl); - - // Update annotation mode - this.currentAnnotationMode = mode; - } - } - - /** - * Disables the specified annotation mode - * - * @param {string} mode - Current annotation mode - * @param {HTMLElement} buttonEl - Annotation button element - * @return {void} - */ - disableAnnotationMode(mode, buttonEl) { - if (!this.isModeAnnotatable(mode)) { - return; - } else if (this.isInAnnotationMode(mode)) { - this.currentAnnotationMode = null; - this.emit(ANNOTATOR_EVENT.modeExit, { mode, headerSelector: SELECTOR_BOX_PREVIEW_BASE_HEADER }); - } - - this.annotatedElement.classList.remove(CLASS_ANNOTATION_MODE); - if (buttonEl) { - buttonEl.classList.remove(CLASS_ACTIVE); - - if (mode === TYPES.draw) { - this.annotatedElement.classList.remove(CLASS_ANNNOTATION_DRAWING_BACKGROUND); - } - } - - this.unbindModeListeners(mode); // Disable mode - this.bindDOMListeners(); // Re-enable other annotations - } - - /** - * Enables the specified annotation mode - * - * @param {string} mode - Current annotation mode - * @param {HTMLElement} buttonEl - Annotation button element - * @return {void} - */ - enableAnnotationMode(mode, buttonEl) { - this.emit(ANNOTATOR_EVENT.modeEnter, { mode, headerSelector: SELECTOR_ANNOTATION_DRAWING_HEADER }); - - this.annotatedElement.classList.add(CLASS_ANNOTATION_MODE); - if (buttonEl) { - buttonEl.classList.add(CLASS_ACTIVE); - - if (mode === TYPES.draw) { - this.annotatedElement.classList.add(CLASS_ANNNOTATION_DRAWING_BACKGROUND); - } - } - - this.unbindDOMListeners(); // Disable other annotations - this.bindModeListeners(mode); // Enable mode - } - - //-------------------------------------------------------------------------- - // Abstract - //-------------------------------------------------------------------------- - - /** - * Must be implemented to return an annotation location object from the DOM - * event. - * - * @param {Event} event - DOM event - * @param {string} annotationType - Type of annotation - * @return {Object} Location object - */ - /* eslint-disable no-unused-vars */ - getLocationFromEvent(event, annotationType) {} - /* eslint-enable no-unused-vars */ - - /** - * Must be implemented to create the appropriate new thread, add it to the - * in-memory map, and return the thread. - * - * @param {Annotation[]} annotations - Annotations in thread - * @param {Object} location - Location object - * @param {string} type - Annotation type - * @return {AnnotationThread} Created annotation thread - */ - /* eslint-disable no-unused-vars */ - 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 - //-------------------------------------------------------------------------- - - /** - * Annotations setup. - * - * @protected - * @return {void} - */ - setupAnnotations() { - // Map of page => {threads on page} - this.threads = {}; - this.bindDOMListeners(); - this.bindCustomListenersOnService(this.annotationService); - this.addListener(ANNOTATOR_EVENT.scale, this.scaleAnnotations); - } - - /** - * Sets up the shared mobile dialog element. - * - * @protected - * @return {void} - */ - setupMobileDialog() { - // Generate HTML of dialog - const mobileDialogEl = document.createElement('div'); - mobileDialogEl.setAttribute('data-type', DATA_TYPE_ANNOTATION_DIALOG); - mobileDialogEl.classList.add(CLASS_MOBILE_ANNOTATION_DIALOG); - mobileDialogEl.classList.add(CLASS_ANNOTATION_DIALOG); - mobileDialogEl.classList.add(CLASS_HIDDEN); - mobileDialogEl.id = ID_MOBILE_ANNOTATION_DIALOG; - - mobileDialogEl.innerHTML = ` -
- -
`.trim(); - - this.container.appendChild(mobileDialogEl); - } - - /** - * Fetches persisted annotations, creates threads as needed, and generates - * an in-memory map of page to threads. - * - * @protected - * @return {Promise} Promise for fetching saved annotations - */ - fetchAnnotations() { - this.threads = {}; - - // Do not load any pre-existing annotations if the user does not have - // the correct permissions - if (!this.permissions.canViewAllAnnotations && !this.permissions.canViewOwnAnnotations) { - return Promise.resolve(this.threads); - } - - return this.annotationService.getThreadMap(this.fileVersionId).then((threadMap) => { - // Generate map of page to threads - Object.keys(threadMap).forEach((threadID) => { - const annotations = threadMap[threadID]; - const firstAnnotation = annotations[0]; - - if (!firstAnnotation || !this.isModeAnnotatable(firstAnnotation.type)) { - return; - } - - // Bind events on valid annotation thread - const thread = this.createAnnotationThread(annotations, firstAnnotation.location, firstAnnotation.type); - this.bindCustomListenersOnThread(thread); - - const { annotator } = this.options; - if (!annotator) { - return; - } - - if (this.modeControllers[firstAnnotation.type]) { - const controller = this.modeControllers[firstAnnotation.type]; - controller.bindCustomListenersOnThread(thread); - controller.registerThread(thread); - } - }); - - this.emit(ANNOTATOR_EVENT.fetch); - }); - } - - /** - * Binds DOM event listeners. Can be overridden by any annotator that - * needs to bind event listeners to the DOM in the normal state (ie not - * in any annotation mode). - * - * @protected - * @return {void} - */ - bindDOMListeners() {} - - /** - * Unbinds DOM event listeners. Can be overridden by any annotator that - * needs to bind event listeners to the DOM in the normal state (ie not - * in any annotation mode). - * - * @protected - * @return {void} - */ - unbindDOMListeners() {} - - /** - * Binds custom event listeners for the Annotation Service. - * - * @protected - * @return {void} - */ - bindCustomListenersOnService() { - const service = this.annotationService; - if (!service || !(service instanceof AnnotationService)) { - return; - } - - /* istanbul ignore next */ - service.addListener(ANNOTATOR_EVENT.error, this.handleServiceEvents); - } - - /** - * Unbinds custom event listeners for the Annotation Service. - * - * @protected - * @return {void} - */ - unbindCustomListenersOnService() { - const service = this.annotationService; - if (!service || !(service instanceof AnnotationService)) { - return; - } - service.removeListener(ANNOTATOR_EVENT.error, this.handleServiceEvents); - } - - /** - * Binds custom event listeners for a thread. - * - * @protected - * @param {AnnotationThread} thread - Thread to bind events to - * @return {void} - */ - bindCustomListenersOnThread(thread) { - if (!thread) { - return; - } - - thread.addListener('threadevent', this.handleAnnotationThreadEvents); - } - - /** - * Unbinds custom event listeners for the thread. - * - * @protected - * @param {AnnotationThread} thread - Thread to bind events to - * @return {void} - */ - unbindCustomListenersOnThread(thread) { - thread.removeListener('threadevent', this.handleAnnotationThreadEvents); - } - - /** - * Binds event listeners for annotation modes. - * - * @protected - * @param {string} mode - Current annotation mode - * @return {void} - */ - bindModeListeners(mode) { - const handlers = []; - - if (mode === TYPES.point) { - handlers.push( - { - type: 'mousedown', - func: this.pointClickHandler, - eventObj: this.annotatedElement - }, - { - type: 'touchstart', - func: this.pointClickHandler, - eventObj: this.annotatedElement - } - ); - } else if (mode === TYPES.draw && this.modeControllers[mode]) { - this.modeControllers[mode].bindModeListeners(); - } - - handlers.forEach((handler) => { - handler.eventObj.addEventListener(handler.type, handler.func, false); - this.annotationModeHandlers.push(handler); - }); - } - - /** - * Event handler for adding a point annotation. Creates a point annotation - * thread at the clicked location. - * - * @protected - * @param {Event} event - DOM event - * @return {void} - */ - pointClickHandler(event) { - event.stopPropagation(); - event.preventDefault(); - - // Determine if a point annotation dialog is already open and close the - // current open dialog - const hasPendingThreads = this.destroyPendingThreads(); - if (hasPendingThreads) { - return; - } - - // Exits point annotation mode on first click - const buttonSelector = this.modeButtons[TYPES.point].selector; - const buttonEl = this.getAnnotateButton(buttonSelector); - this.disableAnnotationMode(TYPES.point, buttonEl); - - // Get annotation location from click event, ignore click if location is invalid - const location = this.getLocationFromEvent(event, TYPES.point); - if (!location) { - return; - } - - // Create new thread with no annotations, show indicator, and show dialog - const thread = this.createAnnotationThread([], location, TYPES.point); - - if (thread) { - thread.show(); - - // Bind events on thread - this.bindCustomListenersOnThread(thread); - } - - this.emit(THREAD_EVENT.pending, thread.getThreadEventData()); - } - - /** - * Unbinds event listeners for annotation modes. - * - * @protected - * @param {string} mode - Annotation mode to be unbound - * @return {void} - */ - unbindModeListeners(mode) { - while (this.annotationModeHandlers.length > 0) { - const handler = this.annotationModeHandlers.pop(); - handler.eventObj.removeEventListener(handler.type, handler.func); - } - - if (this.modeControllers[mode]) { - this.modeControllers[mode].unbindModeListeners(); - } - } - - /** - * Adds thread to in-memory map. - * - * @protected - * @param {AnnotationThread} thread - Thread to add - * @return {void} - */ - addThreadToMap(thread) { - // Add thread to in-memory map - const page = thread.location.page || 1; // Defaults to page 1 if thread has no page' - const pageThreads = this.getThreadsOnPage(page); - pageThreads[thread.threadID] = thread; - } - - /** - * Removes thread to in-memory map. - * - * @protected - * @param {AnnotationThread} thread - Thread to bind events to - * @return {void} - */ - removeThreadFromMap(thread) { - const page = thread.location.page || 1; - delete this.threads[page][thread.threadID]; - } - - /** - * Returns whether or not annotator is in the specified annotation mode. - * - * @protected - * @param {string} mode - Current annotation mode - * @return {boolean} Whether or not in the specified annotation mode - */ - isInAnnotationMode(mode) { - return this.currentAnnotationMode === mode; - } - - //-------------------------------------------------------------------------- - // Private - //-------------------------------------------------------------------------- - - /** - * Renders annotations from memory. - * - * @private - * @return {void} - */ - renderAnnotations() { - Object.keys(this.threads).forEach((pageNum) => { - this.renderAnnotationsOnPage(pageNum); - }); - } - - /** - * Renders annotations from memory for a specified page. - * - * @private - * @param {number} pageNum - Page number - * @return {void} - */ - renderAnnotationsOnPage(pageNum) { - if (!this.threads) { - return; - } - - const pageThreads = this.getThreadsOnPage(pageNum); - Object.keys(pageThreads).forEach((threadID) => { - const thread = pageThreads[threadID]; - if (!this.isModeAnnotatable(thread.type)) { - return; - } - - thread.show(); - }); - } - - /** - * Rotates annotations. Hides point annotation mode button if rotated - * - * @private - * @param {number} [rotationAngle] - current angle image is rotated - * @param {number} [pageNum] - Page number - * @return {void} - */ - rotateAnnotations(rotationAngle = 0, pageNum = 0) { - // Only render a specific page's annotations unless no page number - // is specified - if (pageNum) { - this.renderAnnotationsOnPage(pageNum); - } else { - this.renderAnnotations(); - } - - // Only show/hide point annotation button if user has the - // appropriate permissions - if (!this.permissions.canAnnotate) { - return; - } - - // Hide create annotations button if image is rotated - const pointButtonSelector = this.modeButtons[TYPES.point].selector; - const pointAnnotateButton = this.getAnnotateButton(pointButtonSelector); - - if (rotationAngle !== 0) { - annotatorUtil.hideElement(pointAnnotateButton); - } else { - annotatorUtil.showElement(pointAnnotateButton); - } - } - - /** - * Returns whether or not the current annotation mode is enabled for - * the current viewer/annotator. - * - * @private - * @param {Object} file - File - * @return {boolean} Whether or not the annotation mode is enabled - */ - getAnnotationPermissions(file) { - const permissions = file.permissions || {}; - this.permissions = { - canAnnotate: permissions.can_annotate || false, - canViewAllAnnotations: permissions.can_view_annotations_all || false, - canViewOwnAnnotations: permissions.can_view_annotations_self || false - }; - } - - /** - * Returns click handler for toggling annotation mode. - * - * @private - * @param {string} mode - Target annotation mode - * @return {Function|null} Click handler - */ - getAnnotationModeClickHandler(mode) { - if (!mode || !this.isModeAnnotatable(mode)) { - return null; - } - - return () => { - this.toggleAnnotationHandler(mode); - }; - } - - /** - * Orient annotations to the correct scale and orientation of the annotated document. - * - * @private - * @param {Object} data - Scale and orientation values needed to orient annotations. - * @return {void} - */ - scaleAnnotations(data) { - this.setScale(data.scale); - this.rotateAnnotations(data.rotationAngle, data.pageNum); - } - - /** - * Exits all annotation modes except the specified mode - * - * @param {string} mode - Current annotation mode - * @return {void} - */ - exitAnnotationModesExcept(mode) { - Object.keys(this.modeButtons).forEach((type) => { - if (mode === type) { - return; - } - - const buttonSelector = this.modeButtons[type].selector; - if (!this.modeButtons[type].button) { - this.modeButtons[type].button = this.getAnnotateButton(buttonSelector); - } - - this.disableAnnotationMode(type, this.modeButtons[type].button); - }); - } - - /** - * Gets threads on page - * - * @private - * @param {number} page - Current page number - * @return {Map|[]} Threads on page - */ - getThreadsOnPage(page) { - if (!(page in this.threads)) { - this.threads[page] = {}; - } - - return this.threads[page]; - } - - /** - * Gets thread specified by threadID - * - * @private - * @param {number} threadID - Thread ID - * @return {AnnotationThread} Annotation thread specified by threadID - */ - getThreadByID(threadID) { - let thread = null; - Object.keys(this.threads).forEach((page) => { - const pageThreads = this.getThreadsOnPage(page); - if (threadID in pageThreads) { - thread = pageThreads[threadID]; - } - }); - - return thread; - } - - /** - * Scrolls specified annotation into view - * - * @private - * @param {Object} threadID - annotation threadID for thread that should scroll into view - * @return {void} - */ - scrollToAnnotation(threadID) { - if (!threadID) { - return; - } - - Object.values(this.threads).forEach((pageThreads) => { - if (threadID in pageThreads) { - const thread = pageThreads[threadID]; - thread.scrollIntoView(); - } - }); - } - - /** - * Destroys pending threads. - * - * @private - * @return {boolean} Whether or not any pending threads existed on the - * current file - */ - destroyPendingThreads() { - let hasPendingThreads = false; - - Object.keys(this.threads).forEach((page) => { - const pageThreads = this.getThreadsOnPage(page); - - Object.keys(pageThreads).forEach((threadID) => { - const thread = pageThreads[threadID]; - if (annotatorUtil.isPending(thread.state)) { - hasPendingThreads = true; - thread.destroy(); - } - }); - }); - return hasPendingThreads; - } - - /** - * Displays annotation validation error notification once on load. Does - * nothing if notification was already displayed once. - * - * @private - * @return {void} - */ - handleValidationError() { - if (this.validationErrorEmitted) { - return; - } - - this.emit(ANNOTATOR_EVENT.error, this.localized.loadError); - /* eslint-disable no-console */ - console.error('Annotation could not be created due to invalid params'); - /* eslint-enable no-console */ - this.validationErrorEmitted = true; - } - - /** - * Handle events emitted by the annotaiton service - * - * @private - * @param {Object} [data] - Annotation service event data - * @param {string} [data.event] - Annotation service event - * @param {string} [data.data] - - * @return {void} - */ - handleServiceEvents(data) { - let errorMessage = ''; - switch (data.reason) { - case 'read': - errorMessage = this.localized.loadError; - break; - case 'create': - errorMessage = this.localized.createError; - this.showAnnotations(); - break; - case 'delete': - errorMessage = this.localized.deleteError; - this.showAnnotations(); - break; - case 'authorization': - errorMessage = this.localized.authError; - break; - default: - } - - if (data.error) { - /* eslint-disable no-console */ - console.error(ANNOTATOR_EVENT.error, data.error); - /* eslint-enable no-console */ - } - - if (errorMessage) { - this.emit(ANNOTATOR_EVENT.error, errorMessage); - } - } - - /** - * Handles annotation thread events and emits them to the viewer - * - * @private - * @param {Object} [data] - Annotation thread event data - * @param {string} [data.event] - Annotation thread event - * @param {string} [data.data] - Annotation thread event data - * @return {void} - */ - handleAnnotationThreadEvents(data) { - if (!data.data || !data.data.threadID) { - return; - } - - const thread = this.getThreadByID(data.data.threadID); - if (!thread) { - return; - } - - switch (data.event) { - case THREAD_EVENT.threadCleanup: - // Thread should be cleaned up, unbind listeners - we - // don't do this in annotationdelete listener since thread - // may still need to respond to error messages - this.unbindCustomListenersOnThread(thread); - break; - case THREAD_EVENT.threadDelete: - // Thread was deleted, remove from thread map - this.removeThreadFromMap(thread); - this.emit(data.event, data.data); - break; - case THREAD_EVENT.deleteError: - this.emit(ANNOTATOR_EVENT.error, this.localized.deleteError); - this.emit(data.event, data.data); - break; - case THREAD_EVENT.createError: - this.emit(ANNOTATOR_EVENT.error, this.localized.createError); - this.emit(data.event, data.data); - break; - default: - this.emit(data.event, data.data); - } - } - - /** - * Emits a generic annotator event - * - * @private - * @emits annotatorevent - * @param {string} event - Event name - * @param {Object} data - Event data - * @return {void} - */ - emit(event, data) { - const { annotator } = this.options; - super.emit(event, data); - super.emit('annotatorevent', { - event, - data, - annotatorName: annotator ? annotator.NAME : '', - fileVersionId: this.fileVersionId, - fileId: this.fileId - }); - } -} - -export default Annotator; diff --git a/src/lib/annotations/Annotator.scss b/src/lib/annotations/Annotator.scss deleted file mode 100644 index 1baaefe9e..000000000 --- a/src/lib/annotations/Annotator.scss +++ /dev/null @@ -1,586 +0,0 @@ -@import '../boxuiVariables'; - -$highlight-yellow: #fed94e; -$highlight-yellow-active-hover: #ffc900; -$highlight-yellow-hover: #f5b31b; - -// Taken from Box React UI avatar -$avatar-color-1: #18bbf7; -$avatar-color-2: #0d67c7; -$avatar-color-3: #052e5c; -$avatar-color-4: #747679; -$avatar-color-5: #fda308; -$avatar-color-6: #98c332; -$avatar-color-7: #159f45; -$avatar-color-8: #b800b2; -$avatar-color-9: #f22c44; - -$tablet: 'max-width: 768px'; - -.bp-annotate-draw-background { - background-color: $fours; -} - -.bp-annotate-draw-header { - background-color: $twos; - border: none; - position: relative; -} - -.bp-btn-annotate-draw-cancel { - background-color: $twos; - border: solid $off-white 1px; - color: $off-white; - - &:hover { - background-color: $black; - } -} - -.bp-btn-annotate-draw-post { - background-color: $off-white; - color: $twos; - - &:hover { - background-color: $white; - } -} - -.bp-annotate-draw-post-cancel-container { - margin-right: 20px; - position: absolute; - right: 0; - top: 0; - - button { - height: 32px; - margin: 8px; - } -} - -.bp-annotate-draw-undo-redo-container { - margin: 0 auto; - - .bp-btn-annotate-draw-undo, - .bp-btn-annotate-draw-redo { - background: none; - border: none; - margin: 5px; - - svg { - fill: $off-white; - vertical-align: middle; - } - } -} - -@media (max-width: 768px) { - .bp-annotate-draw-undo-redo-container { - margin: 0; - } -} - -.bp-annotation-caret { - left: 50%; - position: absolute; - - // CSS arrow for caret above container - &::after, - &::before { - border: solid transparent; - content: ' '; - height: 0; - left: 50%; - pointer-events: none; - position: absolute; - top: 0; - transform: rotate(-225deg); - width: 0; - } - - &::after { - border-color: transparent transparent $white $white; - border-width: 6px; - margin: -4px 0 0 -6px; - } - - &::before { - border-color: transparent transparent $sf-fog $sf-fog; - border-width: 6px; - margin: -5px 0 0 -6px; - } -} - -// Icon buttons -.delete-comment-btn { - svg { - fill: lighten($better-black, 10%); - } - - &:hover svg { - fill: $better-black; - } -} - -.bp-annotation-drawing-dialog, -.bp-annotation-highlight-dialog .bp-btn-plain, -.bp-annotation-highlight-dialog .bp-btn-plain:hover { - padding-left: 5px; - padding-right: 5px; - - .icon, - svg { - fill: lighten($better-black, 10%); - } - - &:hover .icon, - &:hover svg { - fill: $better-black; - } -} - -.bp-highlight-comment-btn:hover .icon { - fill: $white; -} - -.bp-add-highlight-btn:hover svg { - fill: $highlight-yellow; -} - -.bp-is-text-highlighted .bp-annotation-highlight-dialog .bp-btn-plain { - svg { - fill: $highlight-yellow; - } - - &.bp-is-active svg { - fill: $highlight-yellow-active-hover; - } - - &:hover svg { - fill: $highlight-yellow-hover; - } -} - -.bp-btn-plain.bp-btn-annotate-point.bp-is-active svg { - fill: $box-blue; -} - -//------------------------------------------------------------------------------ -// CSS for points and dialogs -//------------------------------------------------------------------------------ - -.bp-annotation-dialog, -.bp-create-annotation-dialog { - border-top: 20px solid transparent; // Transparent border for hover detection - cursor: default; // Overrides cursor: none from annotation mode - position: absolute; - text-align: left; - z-index: 9999; // Annotation dialog should be above other content - - .annotation-container { - background-color: $white; - border: 1px solid $seesee; - border-radius: 4px; - overflow-x: hidden; - overflow-y: auto; - padding: 15px; - white-space: normal; - width: 282px; // Hard-coded width to solve layout issues - } - - .bp-textarea { - display: block; - font-size: 13px; - height: 34px; - line-height: 13px; - max-width: 235px; - min-height: 34px; - padding: 7px; - resize: vertical; - transition: border-color linear .15s, box-shadow linear .1s, min-height .1s; - width: 235px; - - &.bp-is-active { - min-height: 68px; - } - } - - .annotation-comment { - border-bottom: 1px solid $seesee; - margin-top: 15px; - padding-bottom: 15px; - position: relative; - - &:first-child { - margin-top: 0; - } - - &:last-child { - border-bottom: 0; - } - } - - &.cannot-annotate { - .annotation-comment:last-child { - padding-bottom: 0; - } - - .delete-comment-btn, - .reply-container { - display: none; - } - } - - .profile-image-container { - display: inline-block; - padding-right: 5px; - vertical-align: top; - width: 37px; - - img { - border-radius: 50%; - display: block; // Remove padding from inline display - height: 32px; - width: 32px; - } - } - - .profile-container { - display: inline-block; - - .user-name { - color: $fours; - font-size: 13px; - font-weight: bold; - max-width: 175px; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - } - - .comment-date { - color: lighten($fours, 20%); - font-size: 11px; - padding-top: 2px; - } - } - - .comment-text { - color: $fours; - font-size: 12px; - padding-top: 10px; - width: 250px; - word-wrap: break-word; - } - - .delete-confirmation { - background-color: fade-out($white, .05); - height: 100%; - left: 0; - position: absolute; - top: 0; - width: 100%; - z-index: 2; // Above the delete button - - .button-container { - margin-top: 0; - text-align: center; - } - } - - .delete-confirmation-message { - color: $fours; - font-size: 12px; - padding: 0 0 5px; - text-align: center; - } - - .button-container { - margin-top: 15px; - position: relative; - text-align: right; - width: 100%; - - .bp-btn:last-child { - margin-right: 0; - } - } - - .delete-comment-btn { - position: absolute; - right: 0; - top: 0; - - svg { - height: 18px; - width: 18px; - } - - &:focus { - box-shadow: 0 0 2px 1px fade-out($black, .8); - } - } -} - -.bp-point-annotation-marker { - background-color: transparent; - border-style: none; - cursor: pointer; - padding: 0; - position: absolute; - width: 24px; - - &:hover { - z-index: 10000; // Ensure activated point annotation icon is above dialog - } - - svg { - fill: fade-out($box-blue, .35); - transition: fill .5s; - } - - &:hover svg { - fill: $box-blue; - } -} - -.bp-point-annotation-marker.bp-annotation-dialog-flipped { - path, - rect { - display: none; - } -} - -.bp-annotation-dialog-flipped { - .bp-annotation-caret { - transform: rotate(180deg); - } -} - -.bp-annotation-profile { - background-color: $tendemob-grey; - border-radius: 16px; // Circle - color: $white; - font-size: 12px; - height: 32px; - line-height: 32px; - text-align: center; - width: 32px; - - &.avatar-color-0 { - background-color: $avatar-color-1; - } - - &.avatar-color-1 { - background-color: $avatar-color-2; - } - - &.avatar-color-2 { - background-color: $avatar-color-3; - } - - &.avatar-color-3 { - background-color: $avatar-color-4; - } - - &.avatar-color-4 { - background-color: $avatar-color-5; - } - - &.avatar-color-5 { - background-color: $avatar-color-6; - } - - &.avatar-color-6 { - background-color: $avatar-color-7; - } - - &.avatar-color-7 { - background-color: $avatar-color-8; - } - - &.avatar-color-8 { - background-color: $avatar-color-9; - } -} - -//------------------------------------------------------------------------------ -// CSS for highlights -//------------------------------------------------------------------------------ -.bp-annotation-layer-highlight { - cursor: text; - left: 0; - mix-blend-mode: multiply; - position: absolute; - top: 15px; // Match 15px padding top on page -} - -.bp-highlight-dialog { - border-top: 20px solid transparent; // Transparent border for hover detection - color: $fours; - display: table; - position: absolute; - z-index: 9999; - - &.cannot-annotate { - .bp-add-highlight-btn, - .bp-highlight-comment-btn { - display: none; - } - } -} - -.bp-annotation-drawing-dialog, -.bp-annotation-highlight-dialog, -.bp-annotation-highlight-dialog:hover { - background-color: $white; - border: 1px solid $seesee; - border-radius: 2px; - padding: 5px 6px; - - // Center buttons - .bp-add-highlight-btn { - padding-top: 1px; - } - - .bp-btn-annotate-draw-add, - .bp-btn-annotate-draw-delete { - color: lighten($better-black, 10%); - padding: 8px; - - &:hover { - color: $better-black; - } - } - - .bp-btn-annotate-draw-add { - padding-right: 4px; - } - - .bp-btn-annotate-draw-delete { - padding-left: 4px; - } - - .bp-highlight-comment-btn { - padding-top: 3px; - } -} - -.bp-annotation-drawing-dialog { - padding: 0; -} - -.bp-use-default-cursor { - cursor: default; - - // Overrides pdf_viewer.css - .textLayer > div { - cursor: default; - } -} - -// Quad point positioning - the helper divs are positioned relative to the Rangy-created element -.bp-doc .rangy-highlight { - background-color: #ff0; - position: relative; -} - -// These helper divs allow us to calculate the quad points of an element -.bp-quad-corner-container { - bottom: 0; - left: 0; - position: absolute; - right: 0; - top: 0; -} - -.bp-quad-corner { - background: none; - height: 0; - position: absolute; - width: 0; - - &.corner1 { - bottom: 0; - left: 0; - } - - &.corner2 { - bottom: 0; - right: 0; - } - - &.corner3 { - right: 0; - top: 0; - } - - &.corner4 { - left: 0; - top: 0; - } -} - -.bp-annotation-drawing-label, -.bp-annotation-highlight-label { - padding: 8px 0 8px 8px; - width: 100%; -} - -//------------------------------------------------------------------------------ -// Draw annotation mode -//------------------------------------------------------------------------------ -.bp-annotation-draw-boundary { - animation: dash 1s linear infinite; - fill: none; - stroke: rgb(0, 0, 0); - stroke-dasharray: 5; - stroke-width: 3px; -} - -@keyframes dash { - from { - stroke-dashoffset: 10; - } - - to { - stroke-dashoffset: 0; - } -} - -.bp-annotation-draw .textLayer { - pointer-events: none; - - > div { - pointer-events: auto; - } -} - -.bp-annotation-layer-draw, -.bp-annotation-layer-draw-in-progress { - left: 0; - mix-blend-mode: multiply; - position: absolute; - top: 15px; // Match 15px padding top on page -} - -//------------------------------------------------------------------------------ -// Annotation mode -//------------------------------------------------------------------------------ -.bp-annotation-mode .page, -.bp-annotation-mode .bp-annotation-layer-highlight, -.bp-annotation-mode .textLayer > div, -.bp-annotation-mode > img, -.bp-annotation-mode img { - cursor: crosshair; -} - -// Needed to allow point annotations since PDF.js adds a funky mousedown handler -// that helps with text selection - we need to disable this since it interacts -// badly with point annotations on non-text divs in non-Chrome browsers -.bp-annotation-mode .textLayer > div, -.bp-annotation-mode .textLayer .endOfContent { - pointer-events: none; -} - -@import './MobileAnnotator'; diff --git a/src/lib/annotations/BoxAnnotations.js b/src/lib/annotations/BoxAnnotations.js deleted file mode 100644 index 7351ef108..000000000 --- a/src/lib/annotations/BoxAnnotations.js +++ /dev/null @@ -1,134 +0,0 @@ -import DocAnnotator from './doc/DocAnnotator'; -import ImageAnnotator from './image/ImageAnnotator'; -import DrawingModeController from './drawing/DrawingModeController'; -import { TYPES } from './annotationConstants'; -import { canLoadAnnotations } from './annotatorUtil'; - -/** - * NAME: The name of the annotator. - * CONSTRUCTOR: Constructor for the annotator. - * VIEWER: The kinds of viewers that can be annotated by this. - * TYPE: The types of annotations that can be used by this annotator. - * DEFAULT_TYPES: The default annotation types enabled if none provided. - */ -const ANNOTATORS = [ - { - NAME: 'Document', - CONSTRUCTOR: DocAnnotator, - VIEWER: ['Document', 'Presentation'], - TYPE: [TYPES.point, TYPES.highlight, TYPES.highlight_comment, TYPES.draw], - DEFAULT_TYPES: [TYPES.point, TYPES.highlight, TYPES.highlight_comment] - }, - { - NAME: 'Image', - CONSTRUCTOR: ImageAnnotator, - VIEWER: ['Image', 'MultiImage'], - TYPE: [TYPES.point], - DEFAULT_TYPES: [TYPES.point] - } -]; - -const ANNOTATOR_TYPE_CONTROLLERS = { - [TYPES.draw]: { - CONSTRUCTOR: DrawingModeController - } -}; - -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 : []; - } - - /** - * Get all annotators for a given viewer. - * - * @param {string} viewerName - Name of the viewer to get annotators for - * @param {Array} [disabledAnnotators] - List of disabled annotators - * @return {Object} Annotator for the viewer - */ - getAnnotatorsForViewer(viewerName, disabledAnnotators = []) { - const annotators = this.getAnnotators(); - const annotatorConfig = annotators.find( - (annotator) => !disabledAnnotators.includes(annotator.NAME) && annotator.VIEWER.includes(viewerName) - ); - this.instantiateControllers(annotatorConfig); - - return annotatorConfig; - } - - /** - * Instantiates and attaches controller instances to an annotator configuration. Does nothing if controller - * has already been instantiated or the config is invalid. - * - * @private - * @param {Object} annotatorConfig - The config where annotation type controller instances should be attached - * @return {void} - */ - instantiateControllers(annotatorConfig) { - if (!annotatorConfig || !annotatorConfig.TYPE || annotatorConfig.CONTROLLERS) { - return; - } - - /* eslint-disable no-param-reassign */ - annotatorConfig.CONTROLLERS = {}; - annotatorConfig.TYPE.forEach((type) => { - if (type in ANNOTATOR_TYPE_CONTROLLERS) { - annotatorConfig.CONTROLLERS[type] = new ANNOTATOR_TYPE_CONTROLLERS[type].CONSTRUCTOR(); - } - }); - /* eslint-enable no-param-reassign */ - } - - /** - * Chooses an annotator based on viewer. - * - * @param {Object} options - Viewer options - * @param {Object} [viewerConfig] - Viewer-specific annotations configs - * @param {Array} [disabledAnnotators] - List of disabled annotators - * @return {Object|null} A copy of the annotator to use, if available - */ - determineAnnotator(options, viewerConfig = {}, disabledAnnotators = []) { - let modifiedAnnotator = null; - - const hasAnnotationPermissions = canLoadAnnotations(options.file.permissions); - const annotator = this.getAnnotatorsForViewer(options.viewer.NAME, disabledAnnotators); - if (!hasAnnotationPermissions || !annotator || viewerConfig.enabled === false) { - return modifiedAnnotator; - } - - modifiedAnnotator = Object.assign({}, annotator); - - const enabledTypes = viewerConfig.enabledTypes || [...modifiedAnnotator.DEFAULT_TYPES]; - - // Keeping disabledTypes for backwards compatibility - const disabledTypes = viewerConfig.disabledTypes || []; - - const annotatorTypes = enabledTypes.filter((type) => { - return ( - !disabledTypes.some((disabled) => disabled === type) && - modifiedAnnotator.TYPE.some((allowed) => allowed === type) - ); - }); - - modifiedAnnotator.TYPE = annotatorTypes; - - return modifiedAnnotator; - } -} - -global.BoxAnnotations = BoxAnnotations; -export default BoxAnnotations; diff --git a/src/lib/annotations/CommentBox.js b/src/lib/annotations/CommentBox.js deleted file mode 100644 index 4318c28cb..000000000 --- a/src/lib/annotations/CommentBox.js +++ /dev/null @@ -1,267 +0,0 @@ -import EventEmitter from 'events'; -import * as constants from './annotationConstants'; -import { hideElement, showElement } from './annotatorUtil'; - -class CommentBox extends EventEmitter { - /** - * Text displayed in the Cancel button element. - * - * @property {string} - */ - cancelText; - - /** - * Text displayed in the Post button element. - * - * @property {string} - */ - postText; - - /** - * Placeholder text displayed in the text area element. - * - * @property {string} - */ - placeholderText; - - /** - * Reference to the comment box element. Contains buttons and text area. - * - * @property {HTMLElement} - */ - containerEl; - - /** - * Reference to the cancel button element in the comment box. - * - * @property {HTMLElement} - */ - cancelEl; - - /** - * Reference to the post button element in the comment box. - * - * @property {HTMLElement} - */ - postEl; - - /** - * Reference to the text area element in the comment box. - * - * @property {HTMLElement} - */ - textAreaEl; - - /** - * Reference to parent element that the comment box should be nested inside. - * - * @property {HTMLElement} - */ - parentEl; - - /** Whether or not we should use touch events */ - hasTouch; - - /* Events that the comment box can emit. */ - static CommentEvents = { - cancel: 'comment_cancel', - post: 'comment_post' - }; - - /** - * Creates an element for text entry, submission and cancellation. - * - * @param {HTMLElement} parentEl - Parent element - * @param {Object} [config] - Object containing text values to be displayed to the user. - * @param {Object} config.localized - Translated strings for UI - */ - constructor(parentEl, config = {}) { - super(); - - this.parentEl = parentEl; - - this.hasTouch = config.hasTouch; - this.cancelText = config.localized.cancelButton; - this.postText = config.localized.postButton; - this.placeholderText = config.localized.addCommentPlaceholder; - - // Explicit scope binding for event listeners - this.onCancel = this.onCancel.bind(this); - this.onPost = this.onPost.bind(this); - } - - /** - * Focus on the text box. - * - * @return {void} - */ - focus() { - if (this.textAreaEl) { - this.textAreaEl.focus(); - } - } - - /** - * Unfocus the text box. - * - * @return {void} - */ - blur() { - if (document.activeElement) { - document.activeElement.blur(); - } - } - - /** - * Clear out the text box. - * - * @return {void} - */ - clear() { - if (this.textAreaEl) { - this.textAreaEl.value = ''; - } - } - - /** - * Hide the element. - * - * @return {void} - */ - hide() { - if (this.containerEl) { - hideElement(this.containerEl); - } - } - - /** - * Show the element. - * - * @return {void} - */ - show() { - if (!this.containerEl) { - this.containerEl = this.createCommentBox(); - this.parentEl.appendChild(this.containerEl); - } - - showElement(this.containerEl); - } - - /** - * [destructor] - * - * @return {void} - */ - destroy() { - if (!this.containerEl) { - return; - } - - this.containerEl.remove(); - this.parentEl = null; - this.containerEl = null; - this.cancelEl.removeEventListener('click', this.onCancel); - this.postEl.removeEventListener('click', this.onPost); - if (this.hasTouch) { - this.cancelEl.removeEventListener('touchstart', this.onCancel); - this.postEl.removeEventListener('touchstart', this.onPost); - } - } - - //-------------------------------------------------------------------------- - // Private - //-------------------------------------------------------------------------- - - /** - * Create HTML containing the UI for the comment box. - * - * @private - * @return {HTMLElement} HTML containing UI for the comment box. - */ - createHTML() { - const containerEl = document.createElement('section'); - containerEl.classList.add('bp-create-highlight-comment'); - containerEl.innerHTML = ` - -
- - -
`.trim(); - - return containerEl; - } - - /** - * Stop default behaviour of an element. - * - * @param {Event} event Event created by an input event. - * @return {void} - */ - preventDefaultAndPropagation(event) { - event.preventDefault(); - event.stopPropagation(); - } - - /** - * Clear the current text in the textarea element and notify listeners. - * - * @private - * @param {Event} event Event created by input event - * @return {void} - */ - onCancel(event) { - // stops touch propogating to a click event - this.preventDefaultAndPropagation(event); - this.clear(); - this.emit(CommentBox.CommentEvents.cancel); - } - - /** - * Notify listeners of submit event and then clear textarea element. - * - * @private - * @param {Event} event Event created by input event - * @return {void} - */ - onPost(event) { - // stops touch propogating to a click event - this.preventDefaultAndPropagation(event); - this.emit(CommentBox.CommentEvents.post, this.textAreaEl.value); - this.clear(); - } - - /** - * Create HTML for the comment box. Assigns references to elements, attach event listeners. - * ie) Post button, cancel button - * - * @private - * @return {HTMLElement} The HTML to append to this.parentElement - */ - createCommentBox() { - const containerEl = this.createHTML(); - - // Reference HTML - this.textAreaEl = containerEl.querySelector(constants.SELECTOR_ANNOTATION_TEXTAREA); - this.cancelEl = containerEl.querySelector(constants.SELECTOR_ANNOTATION_BUTTON_CANCEL); - this.postEl = containerEl.querySelector(constants.SELECTOR_ANNOTATION_BUTTON_POST); - - // Add event listeners - this.cancelEl.addEventListener('click', this.onCancel); - this.postEl.addEventListener('click', this.onPost); - if (this.hasTouch) { - containerEl.addEventListener('touchend', this.preventDefaultAndPropagation.bind(this)); - this.cancelEl.addEventListener('touchend', this.onCancel); - this.postEl.addEventListener('touchend', this.onPost); - } - - return containerEl; - } -} - -export default CommentBox; diff --git a/src/lib/annotations/MobileAnnotator.scss b/src/lib/annotations/MobileAnnotator.scss deleted file mode 100644 index b2ea2ffe0..000000000 --- a/src/lib/annotations/MobileAnnotator.scss +++ /dev/null @@ -1,240 +0,0 @@ -@import '../_boxuiVariables'; - -$tablet: "(min-width: 768px)"; - -@mixin border-top-bottom { - border-color: $seesee; - border-style: solid; - border-width: 1px 0; -} - -.bp-mobile-annotation-dialog { - background: white; - border-top: 0; - height: 100%; - position: absolute; - top: 0; - width: 100%; - - &.bp-animate-show-dialog { - &:not(.bp-plain-highlight) { - animation: show-dialog-small; - animation-duration: .2s; - animation-fill-mode: forwards; - - @media #{$tablet} { - animation: show-dialog-tablet; - animation-duration: .2s; - animation-fill-mode: forwards; - border-left: solid 1px $seesee; - width: 450px; - } - } - - &.bp-plain-highlight { - animation: show-highlight-dialog; - animation-duration: .2s; - animation-fill-mode: forwards; - } - } -} - -.bp-create-annotation-dialog.bp-mobile-annotation-dialog { - height: auto; - - .bp-annotation-highlight-dialog { - bottom: 0; - } -} - -@keyframes show-dialog-small { - 0% { - top: 100%; - } - - 100% { - top: 0%; - } -} - -@keyframes show-dialog-tablet { - 0% { - right: -50%; - } - - 100% { - right: 0%; - } -} - -@keyframes show-highlight-dialog { - 0% { - top: -48px; - } - - 100% { - top: 0; - } -} - -.bp-mobile-annotation-dialog.bp-annotation-dialog { - .annotation-container { - background-color: $white; - border: 0; - border-radius: 4px; - height: 100%; - overflow-x: hidden; - overflow-y: auto; - padding: 15px; - position: absolute; - width: 100%; - } - - .bp-annotation-mobile-header { - align-items: center; - background-color: $white; - display: flex; - height: 48px; - justify-content: space-between; - padding: 0; - - @include border-top-bottom; - } - - .bp-annotation-dialog-close { - background: none; - border: 0; - display: inline-block; - height: 48px; - line-height: 1; - margin-top: -1px; - padding: 3px; - vertical-align: middle; - width: 48px; - - .icon { - fill: fade-out($better-black, .25); - } - - &:hover .icon { - fill: $better-black; - } - } - - .bp-textarea { - font-size: 16px; - line-height: 16px; - min-width: 100%; - } - - .comment-text { - width: 100%; - } - - .profile-image-container { - width: 43px; - - img { - height: 40px; - width: 40px; - } - } - - .profile-container { - .user-name { - font-size: 16px; - } - - .comment-date { - font-size: 14px; - } - } - - .bp-btn { - font-size: 16; - } - - .comment-text, - .delete-confirmation-message { - font-size: 15px; - } - - .delete-comment-btn { - svg { - height: 24px; - width: 24px; - } - } - - .reply-container { - padding-bottom: 45px; - } -} - -/* Highlight dialog */ -.bp-mobile-annotation-dialog.bp-plain-highlight { - height: 47px; // includes mobile header & highlight dialog - top: auto; - - @include border-top-bottom; -} - -.bp-mobile-annotation-dialog .bp-annotation-highlight-btns, -.bp-mobile-annotation-dialog .bp-create-highlight-comment, -.bp-mobile-annotation-dialog .annotation-container section[data-section="create"] { - background: white; - bottom: 0; - left: 0; - padding: 15px; - position: fixed; - width: 100%; - - @include border-top-bottom; -} - -.bp-mobile-annotation-dialog .annotation-container { - @media #{$tablet} { - left: auto; - padding: 15px 0; - width: 415px; - } -} - -.bp-mobile-annotation-dialog .bp-annotation-highlight-dialog { - border: none; - color: $fours; - font-size: 16px; - line-height: 16px; - min-width: 100%; - padding: 15px; - position: absolute; - text-align: center; - z-index: 9999; - - .bp-annotation-highlight-btns { - display: flex; - justify-content: flex-start; - padding: 0; - - button { - flex-grow: 1; - padding: 15px 0; - - &:nth-child(2)::before { - border-right: 1px solid $seesee; - bottom: 17px; - content: ""; - height: 25px; - position: absolute; - right: 50%; - } - } - } - - &.cannot-annotate { - .bp-add-highlight-btn, - .bp-highlight-comment-btn { - display: none; - } - } -} diff --git a/src/lib/annotations/__tests__/AnnotationDialog-test.html b/src/lib/annotations/__tests__/AnnotationDialog-test.html deleted file mode 100644 index 2d4a1caa3..000000000 --- a/src/lib/annotations/__tests__/AnnotationDialog-test.html +++ /dev/null @@ -1,7 +0,0 @@ -
-
-
-
-
-
-
diff --git a/src/lib/annotations/__tests__/AnnotationDialog-test.js b/src/lib/annotations/__tests__/AnnotationDialog-test.js deleted file mode 100644 index db0dae489..000000000 --- a/src/lib/annotations/__tests__/AnnotationDialog-test.js +++ /dev/null @@ -1,980 +0,0 @@ -/* eslint-disable no-unused-expressions */ -import Annotation from '../Annotation'; -import AnnotationDialog from '../AnnotationDialog'; -import * as annotatorUtil from '../annotatorUtil'; -import * as constants from '../annotationConstants'; - -const CLASS_FLIPPED_DIALOG = 'bp-annotation-dialog-flipped'; -const CLASS_CANCEL_DELETE = 'cancel-delete-btn'; -const CLASS_CANNOT_ANNOTATE = 'cannot-annotate'; -const CLASS_REPLY_TEXTAREA = 'reply-textarea'; -const CLASS_ANIMATE_DIALOG = 'bp-animate-show-dialog'; -const CLASS_BUTTON_DELETE_COMMENT = 'delete-comment-btn'; -const SELECTOR_DELETE_CONFIRMATION = '.delete-confirmation'; - -let dialog; -const sandbox = sinon.sandbox.create(); -let stubs = {}; - -describe('lib/annotations/AnnotationDialog', () => { - before(() => { - fixture.setBase('src/lib'); - }); - - beforeEach(() => { - fixture.load('annotations/__tests__/AnnotationDialog-test.html'); - - dialog = new AnnotationDialog({ - annotatedElement: document.querySelector('.annotated-element'), - container: document, - location: {}, - annotations: [], - canAnnotate: true - }); - dialog.localized = { - addCommentPlaceholder: 'add comment placeholder', - posting: 'posting' - }; - dialog.setup([]); - document.querySelector('.annotated-element').appendChild(dialog.element); - - stubs.emit = sandbox.stub(dialog, 'emit'); - dialog.isMobile = false; - }); - - afterEach(() => { - const dialogEl = document.querySelector('.annotated-element'); - if (dialogEl && dialogEl.parentNode) { - dialogEl.parentNode.removeChild(dialogEl); - } - - sandbox.verifyAndRestore(); - if (typeof dialog.destroy === 'function') { - dialog.destroy(); - dialog = null; - } - - stubs = {}; - }); - - describe('destroy()', () => { - it('should unbind DOM listeners and cleanup its HTML', () => { - const unbindStub = sandbox.stub(dialog, 'unbindDOMListeners'); - - dialog.destroy(); - expect(unbindStub).to.be.called; - expect(dialog.element).to.be.null; - }); - }); - - describe('show()', () => { - beforeEach(() => { - stubs.position = sandbox.stub(dialog, 'position'); - }); - - it('should not re-show dialog if already shown on page', () => { - dialog.hasAnnotations = true; - dialog.activateReply(); - - dialog.show(); - expect(stubs.position).to.not.be.called; - }); - - it('should not re-position dialog if already shown on page', () => { - dialog.hasAnnotations = true; - - // Deactivates dialog textarea - dialog.deactivateReply(); - const commentsTextArea = dialog.element.querySelector(constants.SELECTOR_ANNOTATION_TEXTAREA); - commentsTextArea.classList.remove('bp-is-active'); - - // Removes dialog from page - dialog.element.parentNode.removeChild(dialog.element); - dialog.activateReply(); - - dialog.show(); - expect(stubs.position).to.be.called; - }); - - it('should position the dialog', () => { - dialog.hasAnnotations = true; - dialog.deactivateReply(); - const commentsTextArea = dialog.element.querySelector(constants.SELECTOR_ANNOTATION_TEXTAREA); - commentsTextArea.classList.remove('bp-is-active'); - - dialog.show(); - expect(stubs.position).to.be.called; - }); - - it('should hide the reply/edit/delete UI if user cannot annotate', () => { - dialog.canAnnotate = false; - dialog.hasAnnotations = true; - dialog.deactivateReply(); - - dialog.show(); - expect(dialog.element).to.have.class(CLASS_CANNOT_ANNOTATE); - }); - - it('should focus textarea if in viewport', () => { - dialog.canAnnotate = false; - dialog.hasAnnotations = true; - dialog.deactivateReply(); - sandbox.stub(annotatorUtil, 'isElementInViewport').returns(true); - - dialog.show(); - expect(document.activeElement).to.have.class(CLASS_REPLY_TEXTAREA); - }); - - it('should activate reply textarea if dialog has annotations', () => { - dialog.canAnnotate = false; - dialog.hasAnnotations = true; - dialog.deactivateReply(); - sandbox.stub(dialog, 'activateReply'); - - dialog.show(); - const textArea = dialog.element.querySelector(`.${CLASS_REPLY_TEXTAREA}`); - expect(textArea).to.not.have.class(constants.CLASS_ACTIVE); - expect(dialog.activateReply).to.be.called; - }); - - it('should activate textarea if dialog does not have annotations', () => { - dialog.canAnnotate = false; - dialog.hasAnnotations = false; - dialog.deactivateReply(); - sandbox.stub(dialog, 'activateReply'); - - dialog.show(); - const textArea = dialog.element.querySelector(constants.SELECTOR_ANNOTATION_TEXTAREA); - expect(textArea).to.have.class(constants.CLASS_ACTIVE); - expect(dialog.activateReply).to.not.be.called; - }); - - it('should populate the mobile dialog if using a mobile browser', () => { - dialog.isMobile = true; - dialog.highlightDialogEl = null; - stubs.show = sandbox.stub(annotatorUtil, 'showElement'); - stubs.bind = sandbox.stub(dialog, 'bindDOMListeners'); - - dialog.show(); - expect(stubs.show).to.be.calledWith(dialog.element); - expect(stubs.bind).to.be.called; - expect(dialog.position).to.not.be.called; - expect(dialog.element.classList.contains(CLASS_ANIMATE_DIALOG)).to.be.true; - }); - - it('should add the animation class to the the mobile dialog if using a mobile browser', () => { - dialog.isMobile = true; - - dialog.show(); - expect(dialog.element.classList.contains(CLASS_ANIMATE_DIALOG)).to.be.true; - }); - - it('should reset the annotation dialog to be a plain highlight if no comments are present', () => { - dialog.isMobile = true; - dialog.highlightDialogEl = {}; - sandbox.stub(dialog.element, 'querySelectorAll').withArgs('.annotation-comment').returns([]); - stubs.show = sandbox.stub(annotatorUtil, 'showElement'); - stubs.bind = sandbox.stub(dialog, 'bindDOMListeners'); - dialog.show(); - - expect(dialog.element.classList.contains(constants.CLASS_ANNOTATION_PLAIN_HIGHLIGHT)).to.be.true; - }); - }); - - describe('hideMobileDialog()', () => { - it('should do nothing if the dialog element does not exist', () => { - dialog.element = null; - stubs.hide = sandbox.stub(annotatorUtil, 'hideElement'); - dialog.hideMobileDialog(); - expect(stubs.hide).to.not.be.called; - }); - - it('should hide and reset the mobile annotations dialog', () => { - dialog.element = document.querySelector(constants.SELECTOR_MOBILE_ANNOTATION_DIALOG); - stubs.hide = sandbox.stub(annotatorUtil, 'hideElement'); - stubs.unbind = sandbox.stub(dialog, 'unbindDOMListeners'); - stubs.cancel = sandbox.stub(dialog, 'cancelAnnotation'); - dialog.hasAnnotations = true; - - dialog.hideMobileDialog(); - expect(stubs.hide).to.be.called; - expect(stubs.unbind).to.be.called; - expect(stubs.cancel).to.be.called; - expect(dialog.element.classList.contains(CLASS_ANIMATE_DIALOG)).to.be.false; - }); - - it('should remove the animation class', () => { - dialog.element = document.querySelector(constants.SELECTOR_MOBILE_ANNOTATION_DIALOG); - dialog.hideMobileDialog(); - expect(dialog.element.classList.contains(CLASS_ANIMATE_DIALOG)).to.be.false; - }); - - it('should cancel unsaved annotations only if the dialog does not have annotations', () => { - dialog.element = document.querySelector(constants.SELECTOR_MOBILE_ANNOTATION_DIALOG); - stubs.cancel = sandbox.stub(dialog, 'cancelAnnotation'); - dialog.hasAnnotations = false; - - dialog.hideMobileDialog(); - expect(stubs.cancel).to.be.called; - }); - }); - - describe('hide()', () => { - it('should do nothing if element is already hidden', () => { - dialog.element.classList.add(constants.CLASS_HIDDEN); - sandbox.stub(annotatorUtil, 'hideElement'); - dialog.hide(); - expect(annotatorUtil.hideElement).to.not.have.called; - }); - - it('should hide dialog immediately', () => { - sandbox.stub(dialog, 'toggleFlippedThreadEl'); - dialog.hide(); - expect(dialog.element).to.have.class(constants.CLASS_HIDDEN); - expect(dialog.toggleFlippedThreadEl).to.be.called; - }); - - it('should hide the mobile dialog if using a mobile browser', () => { - dialog.isMobile = true; - sandbox.stub(dialog, 'hideMobileDialog'); - dialog.hide(); - expect(dialog.hideMobileDialog).to.be.called; - }); - }); - - describe('addAnnotation()', () => { - beforeEach(() => { - stubs.addEl = sandbox.stub(dialog, 'addAnnotationElement'); - stubs.deactivate = sandbox.stub(dialog, 'deactivateReply'); - }); - - it('should add annotation to the dialog and deactivate the reply area', () => { - dialog.addAnnotation(new Annotation({})); - expect(stubs.addEl).to.be.called; - expect(stubs.deactivate).to.be.calledWith(true); - }); - - it('should hide the create section and show the show section if there are no annotations', () => { - // Add dialog to DOM - dialog.annotatedElement.appendChild(dialog.element); - - dialog.addAnnotation(new Annotation({})); - const createSectionEl = document.querySelector(constants.SECTION_CREATE); - const showSectionEl = document.querySelector(constants.SECTION_SHOW); - expect(createSectionEl).to.have.class(constants.CLASS_HIDDEN); - expect(showSectionEl).to.not.have.class(constants.CLASS_HIDDEN); - }); - }); - - describe('removeAnnotation()', () => { - it('should remove annotation element and deactivate reply', () => { - stubs.deactivate = sandbox.stub(dialog, 'deactivateReply'); - - dialog.addAnnotation( - new Annotation({ - annotationID: 'someID', - text: 'blah', - user: {}, - permissions: {} - }) - ); - - dialog.removeAnnotation('someID'); - const annotationEl = dialog.element.querySelector('[data-annotation-id="someID"]'); - expect(annotationEl).to.be.null; - expect(stubs.deactivate).to.be.called; - }); - - it('should not do anything if the specified annotation does not exist', () => { - stubs.deactivate = sandbox.stub(dialog, 'deactivateReply'); - - dialog.removeAnnotation('someID'); - expect(stubs.deactivate).to.not.be.called; - }); - }); - - describe('element()', () => { - it('should return dialog element', () => { - expect(dialog.element).to.equal(dialog.element); - }); - }); - - describe('setup()', () => { - beforeEach(() => { - const dialogEl = document.createElement('div'); - sandbox.stub(dialog, 'generateDialogEl').returns(dialogEl); - stubs.bind = sandbox.stub(dialog, 'bindDOMListeners'); - stubs.add = sandbox.stub(dialog, 'addAnnotationElement'); - - stubs.annotation = new Annotation({ - annotationID: 'someID', - text: 'blah', - user: {}, - permissions: {}, - threadNumber: 1 - }); - - dialog.isMobile = false; - }); - - it('should set up HTML element, add annotations to the dialog, and bind DOM listeners', () => { - dialog.setup([stubs.annotation], {}); - expect(dialog.element).to.not.be.null; - expect(dialog.element.dataset.threadNumber).to.equal('1'); - expect(stubs.bind).to.be.called; - expect(dialog.threadEl).not.be.null; - }); - - it('should not set thread number if there are no annotations in the thread', () => { - dialog.setup([], {}); - expect(dialog.element.dataset.threadNumber).to.be.undefined; - }); - - it('should not create dialog element if using a mobile browser', () => { - dialog.isMobile = true; - dialog.setup([stubs.annotation, stubs.annotation], {}); - expect(stubs.bind).to.not.be.called; - expect(stubs.add).to.be.calledTwice; - }); - }); - - describe('bindDOMListeners()', () => { - it('should bind DOM listeners', () => { - stubs.add = sandbox.stub(dialog.element, 'addEventListener'); - - dialog.bindDOMListeners(); - expect(stubs.add).to.be.calledWith('keydown', sinon.match.func); - expect(stubs.add).to.be.calledWith('click', sinon.match.func); - expect(stubs.add).to.be.calledWith('mouseup', sinon.match.func); - expect(stubs.add).to.be.calledWith('mouseenter', sinon.match.func); - expect(stubs.add).to.be.calledWith('mouseleave', sinon.match.func); - expect(stubs.add).to.be.calledWith('wheel', sinon.match.func); - }); - - it('should not bind mouseenter/leave events for mobile browsers', () => { - stubs.add = sandbox.stub(dialog.element, 'addEventListener'); - dialog.isMobile = true; - - dialog.bindDOMListeners(); - expect(stubs.add).to.be.calledWith('keydown', sinon.match.func); - expect(stubs.add).to.be.calledWith('click', sinon.match.func); - expect(stubs.add).to.be.calledWith('mouseup', sinon.match.func); - expect(stubs.add).to.not.be.calledWith('mouseenter', sinon.match.func); - expect(stubs.add).to.not.be.calledWith('mouseleave', sinon.match.func); - expect(stubs.add).to.be.calledWith('wheel', sinon.match.func); - }); - }); - - describe('unbindDOMListeners()', () => { - it('should unbind DOM listeners', () => { - stubs.remove = sandbox.stub(dialog.element, 'removeEventListener'); - - dialog.unbindDOMListeners(); - expect(stubs.remove).to.be.calledWith('keydown', sinon.match.func); - expect(stubs.remove).to.be.calledWith('click', sinon.match.func); - expect(stubs.remove).to.be.calledWith('mouseup', sinon.match.func); - expect(stubs.remove).to.be.calledWith('mouseenter', sinon.match.func); - expect(stubs.remove).to.be.calledWith('mouseleave', sinon.match.func); - expect(stubs.remove).to.be.calledWith('wheel', sinon.match.func); - }); - - it('should not bind mouseenter/leave events for mobile browsers', () => { - stubs.remove = sandbox.stub(dialog.element, 'removeEventListener'); - dialog.isMobile = true; - - dialog.unbindDOMListeners(); - expect(stubs.remove).to.be.calledWith('keydown', sinon.match.func); - expect(stubs.remove).to.be.calledWith('click', sinon.match.func); - expect(stubs.remove).to.be.calledWith('mouseup', sinon.match.func); - expect(stubs.remove).to.not.be.calledWith('mouseenter', sinon.match.func); - expect(stubs.remove).to.not.be.calledWith('mouseleave', sinon.match.func); - expect(stubs.remove).to.be.calledWith('wheel', sinon.match.func); - }); - }); - - describe('keydownHandler()', () => { - it('should hide the dialog when user presses Esc', () => { - stubs.hide = sandbox.stub(dialog, 'hide'); - - dialog.keydownHandler({ - key: 'U+001B', // esc key - stopPropagation: () => {} - }); - expect(stubs.hide).to.be.called; - }); - - it('should activate the reply area when user presses another key inside the reply area', () => { - stubs.activate = sandbox.stub(dialog, 'activateReply'); - - dialog.keydownHandler({ - key: ' ', // space - target: dialog.element.querySelector(`.${CLASS_REPLY_TEXTAREA}`), - stopPropagation: () => {} - }); - expect(stubs.activate).to.be.called; - }); - }); - - describe('stopPropagation()', () => { - it('should stop propagation on the event', () => { - const event = { - stopPropagation: () => {} - }; - stubs.stop = sandbox.stub(event, 'stopPropagation'); - - dialog.stopPropagation(event); - expect(stubs.stop).to.be.called; - }); - }); - - describe('mouseenterHandler()', () => { - beforeEach(() => { - stubs.show = sandbox.stub(annotatorUtil, 'showElement'); - }); - - it('should show the element only if the element is currently hidden', () => { - dialog.element.classList.add(constants.CLASS_HIDDEN); - - dialog.mouseenterHandler(); - expect(annotatorUtil.showElement).to.be.called; - }); - - it('should do nothing if the element is already shown', () => { - dialog.mouseenterHandler(); - expect(annotatorUtil.showElement).to.not.be.called; - }); - - it('should emit \'annotationcommentpending\' when user hovers back into a dialog that has a pending comment', () => { - dialog.element.classList.add(constants.CLASS_HIDDEN); - const commentsTextArea = dialog.element.querySelector(constants.SELECTOR_ANNOTATION_TEXTAREA); - commentsTextArea.textContent = 'bleh'; - - dialog.mouseenterHandler(); - expect(stubs.show).to.be.called; - expect(stubs.emit).to.be.calledWith('annotationcommentpending'); - }); - }); - - describe('mouseleaveHandler()', () => { - it('should not do anything if there are no annotations in the dialog', () => { - stubs.hide = sandbox.stub(dialog, 'hide'); - - dialog.mouseleaveHandler(); - expect(stubs.hide).to.not.be.called; - }); - - it('should hide dialog if there are annotations in the dialog', () => { - stubs.hide = sandbox.stub(dialog, 'hide'); - - dialog.addAnnotation( - new Annotation({ - annotationID: 'someID', - text: 'blah', - user: {}, - permissions: {} - }) - ); - - dialog.mouseleaveHandler(); - expect(stubs.hide).to.be.called; - }); - }); - - describe('clickHandler()', () => { - beforeEach(() => { - stubs.event = { - stopPropagation: () => {}, - target: document.createElement('div') - }; - stubs.post = sandbox.stub(dialog, 'postAnnotation'); - stubs.cancel = sandbox.stub(dialog, 'cancelAnnotation'); - stubs.deactivate = sandbox.stub(dialog, 'deactivateReply'); - stubs.activate = sandbox.stub(dialog, 'activateReply'); - stubs.findClosest = sandbox.stub(annotatorUtil, 'findClosestDataType'); - stubs.showDelete = sandbox.stub(dialog, 'showDeleteConfirmation'); - stubs.hideDelete = sandbox.stub(dialog, 'hideDeleteConfirmation'); - stubs.delete = sandbox.stub(dialog, 'deleteAnnotation'); - stubs.reply = sandbox.stub(dialog, 'postReply'); - stubs.hideMobile = sandbox.stub(dialog, 'hideMobileDialog'); - }); - - it('should post annotation when post annotation button is clicked', () => { - stubs.findClosest.returns(constants.CLASS_ANNOTATION_BUTTON_POST); - - dialog.clickHandler(stubs.event); - expect(stubs.post).to.be.called; - }); - - it('should cancel annotation when cancel annotation button is clicked', () => { - stubs.findClosest.returns(constants.CLASS_ANNOTATION_BUTTON_CANCEL); - dialog.isMobile = false; - - dialog.clickHandler(stubs.event); - expect(stubs.cancel).to.be.called; - expect(stubs.hideMobile).to.not.be.called; - expect(stubs.deactivate).to.be.calledWith(true); - }); - - it('should only hide the mobile dialog when the cancel annotation button is clicked on mobile', () => { - stubs.findClosest.returns(constants.CLASS_ANNOTATION_BUTTON_CANCEL); - dialog.isMobile = true; - - dialog.clickHandler(stubs.event); - expect(stubs.cancel).to.not.be.called; - expect(stubs.hideMobile).to.be.called; - expect(stubs.deactivate).to.be.calledWith(true); - }); - - it('should activate reply area when textarea is clicked', () => { - stubs.findClosest.returns(CLASS_REPLY_TEXTAREA); - - dialog.clickHandler(stubs.event); - expect(stubs.activate).to.be.called; - }); - - it('should deactivate reply area when cancel reply button is clicked', () => { - stubs.findClosest.returns('cancel-reply-btn'); - - dialog.clickHandler(stubs.event); - expect(stubs.deactivate).to.be.calledWith(true); - }); - - it('should post reply when post reply button is clicked', () => { - stubs.findClosest.returns('post-reply-btn'); - - dialog.clickHandler(stubs.event); - expect(stubs.reply).to.be.called; - }); - - it('should show delete confirmation when delete button is clicked', () => { - stubs.findClosest.onFirstCall().returns('delete-btn'); - stubs.findClosest.onSecondCall().returns('someID'); - - dialog.clickHandler(stubs.event); - expect(stubs.showDelete).to.be.calledWith('someID'); - }); - - it('should cancel deletion when cancel delete button is clicked', () => { - stubs.findClosest.onFirstCall().returns(CLASS_CANCEL_DELETE); - stubs.findClosest.onSecondCall().returns('someID'); - - dialog.clickHandler(stubs.event); - expect(stubs.hideDelete).to.be.calledWith('someID'); - }); - - it('should confirm deletion when confirm delete button is clicked', () => { - stubs.findClosest.onFirstCall().returns('confirm-delete-btn'); - stubs.findClosest.onSecondCall().returns('someID'); - - dialog.clickHandler(stubs.event); - expect(stubs.delete).to.be.calledWith('someID'); - }); - - it('should do nothing if dataType does not match any button in the annotation dialog', () => { - stubs.findClosest.returns(null); - - dialog.clickHandler(stubs.event); - expect(stubs.post).to.not.be.called; - expect(stubs.reply).to.not.be.called; - expect(stubs.cancel).to.not.be.called; - expect(stubs.deactivate).to.not.be.called; - expect(stubs.activate).to.not.be.called; - expect(stubs.reply).to.not.be.called; - expect(stubs.showDelete).to.not.be.called; - expect(stubs.hideDelete).to.not.be.called; - expect(stubs.delete).to.not.be.called; - }); - }); - - describe('addAnnotationElement()', () => { - it('should add an annotation comment if text is present', () => { - dialog.addAnnotationElement( - new Annotation({ - annotationID: 1, - text: 'the preview sdk is awesome!', - user: {}, - permissions: {} - }) - ); - const annotationComment = document.querySelector('.comment-text'); - expect(annotationComment).to.contain.html('the preview sdk is awesome!'); - }); - - it('should display the posting message if the user id is 0', () => { - dialog.addAnnotationElement( - new Annotation({ - annotationID: 1, - text: 'the preview sdk is awesome!', - user: { id: 0 }, - permissions: {} - }) - ); - const username = document.querySelector('.user-name'); - expect(username).to.contain.html(dialog.localized.posting); - }); - - it('should display user name if the user id is not 0', () => { - dialog.addAnnotationElement( - new Annotation({ - annotationID: 1, - text: 'the preview sdk is awesome!', - user: { id: 1, name: 'user' }, - permissions: {} - }) - ); - const username = document.querySelector('.user-name'); - expect(username).to.contain.html('user'); - }); - - it('should hide the delete icon if the user does\'nt have delete permissions', () => { - dialog.addAnnotationElement( - new Annotation({ - annotationID: 1, - text: 'the preview sdk is amazing!', - user: { id: 1, name: 'user' }, - permissions: { can_delete: false } - }) - ); - const deleteButton = document.querySelector(`.${CLASS_BUTTON_DELETE_COMMENT}`); - expect(deleteButton).to.have.class(constants.CLASS_HIDDEN); - }); - - it('should make the delete icon hidden if the delete permission is not specified', () => { - dialog.addAnnotationElement( - new Annotation({ - annotationID: 1, - text: 'the preview sdk is amazing!', - user: { id: 1, name: 'user' }, - permissions: {} - }) - ); - const deleteButton = document.querySelector(`.${CLASS_BUTTON_DELETE_COMMENT}`); - expect(deleteButton).to.have.class(constants.CLASS_HIDDEN); - }); - - it('should make delete icon visible if the user has delete permission', () => { - dialog.addAnnotationElement( - new Annotation({ - annotationID: 1, - text: 'the preview sdk is amazing!', - user: { id: 1, name: 'user' }, - permissions: { can_delete: true } - }) - ); - const deleteButton = document.querySelector(`.${CLASS_BUTTON_DELETE_COMMENT}`); - expect(deleteButton).to.not.have.class(constants.CLASS_HIDDEN); - }); - - it('should hide the delete confirmation UI by default', () => { - dialog.addAnnotationElement( - new Annotation({ - annotationID: 1, - text: 'the preview sdk is amazing!', - user: { id: 1, name: 'user' }, - permissions: { can_delete: true } - }) - ); - const deleteConfirmation = document.querySelector(SELECTOR_DELETE_CONFIRMATION); - expect(deleteConfirmation).to.have.class(constants.CLASS_HIDDEN); - }); - - it('should correctly format the date and time in a different locale', () => { - const date = new Date(); - stubs.locale = sandbox.stub(Date.prototype, 'toLocaleString'); - dialog.locale = 'en-GB'; - - dialog.addAnnotationElement( - new Annotation({ - annotationID: 1, - text: 'the preview sdk is amazing!', - user: { id: 1, name: 'user' }, - permissions: { can_delete: true }, - created: date - }) - ); - expect(stubs.locale).to.be.calledWith('en-GB'); - }); - }); - - describe('_postAannotation()', () => { - it('should not post an annotation to the dialog if it has no text', () => { - dialog.postAnnotation(); - expect(stubs.emit).to.not.be.called; - }); - - it('should post an annotation to the dialog if it has text', () => { - document.querySelector('textarea').innerHTML += 'the preview SDK is great!'; - - dialog.postAnnotation(); - expect(stubs.emit).to.be.calledWith('annotationcreate', { text: 'the preview SDK is great!' }); - }); - - it('should clear the annotation text element after posting', () => { - const annotationTextEl = document.querySelector('textarea'); - annotationTextEl.innerHTML += 'the preview SDK is great!'; - - dialog.postAnnotation(); - expect(annotationTextEl).to.have.value(''); - }); - }); - - describe('cancelAnnotation()', () => { - it('should emit the annotationcancel message', () => { - dialog.cancelAnnotation(); - expect(stubs.emit).to.be.calledWith('annotationcancel'); - }); - }); - - describe('activateReply()', () => { - it('should do nothing if the dialogEl does not exist', () => { - dialog.dialogEl = null; - sandbox.stub(annotatorUtil, 'showElement'); - dialog.activateReply(); - expect(annotatorUtil.showElement).to.not.be.called; - }); - - it('should do nothing if reply textarea is already active', () => { - const replyTextEl = dialog.element.querySelector(`.${CLASS_REPLY_TEXTAREA}`); - replyTextEl.classList.add('bp-is-active'); - sandbox.stub(annotatorUtil, 'showElement'); - - dialog.activateReply(); - expect(annotatorUtil.showElement).to.not.be.called; - }); - - it('should show the correct UI when the reply textarea is activated', () => { - document.querySelector('textarea').innerHTML += 'the preview SDK is great!'; - dialog.addAnnotationElement({ - annotationID: 1, - text: 'the preview sdk is amazing!', - user: { id: 1, name: 'user' }, - permissions: { can_delete: true } - }); - const replyTextEl = document.querySelector(`.${CLASS_REPLY_TEXTAREA}`); - const buttonContainer = replyTextEl.parentNode.querySelector(constants.SELECTOR_BUTTON_CONTAINER); - - dialog.activateReply(); - expect(replyTextEl).to.have.class(constants.CLASS_ACTIVE); - expect(buttonContainer).to.not.have.class(constants.CLASS_HIDDEN); - }); - }); - - describe('deactivateReply()', () => { - it('should do nothing if element does not exist', () => { - dialog.dialogEl = null; - sandbox.stub(annotatorUtil, 'resetTextarea'); - - dialog.deactivateReply(); - expect(annotatorUtil.resetTextarea).to.not.be.called; - }); - - it('should show the correct UI when the reply textarea is deactivated', () => { - dialog.addAnnotationElement( - new Annotation({ - annotationID: 1, - text: 'the preview sdk is amazing!', - user: { id: 1, name: 'user' }, - permissions: { can_delete: true } - }) - ); - const replyTextEl = document.querySelector(`.${CLASS_REPLY_TEXTAREA}`); - const buttonContainer = replyTextEl.parentNode.querySelector(constants.SELECTOR_BUTTON_CONTAINER); - - dialog.deactivateReply(); - expect(replyTextEl).to.not.have.class(constants.CLASS_ACTIVE); - expect(buttonContainer).to.have.class(constants.CLASS_HIDDEN); - }); - }); - - describe('postReply()', () => { - it('should not post reply to the dialog if it has no text', () => { - dialog.addAnnotationElement( - new Annotation({ - annotationID: 1, - text: 'the preview sdk is amazing!', - user: { id: 1, name: 'user' }, - permissions: { can_delete: true } - }) - ); - dialog.activateReply(); - - dialog.postReply(); - expect(stubs.emit).to.not.be.called; - }); - - it('should post a reply to the dialog if it has text', () => { - dialog.addAnnotationElement( - new Annotation({ - annotationID: 1, - text: 'the preview sdk is amazing!', - user: { id: 1, name: 'user' }, - permissions: { can_delete: true } - }) - ); - const replyTextEl = document.querySelector(`.${CLASS_REPLY_TEXTAREA}`); - dialog.activateReply(); - replyTextEl.innerHTML += 'the preview SDK is great!'; - - dialog.postReply(); - expect(stubs.emit).to.be.calledWith('annotationcreate', { text: 'the preview SDK is great!' }); - }); - - it('should clear the reply text element after posting', () => { - dialog.addAnnotationElement( - new Annotation({ - annotationID: 1, - text: 'the preview sdk is amazing!', - user: { id: 1, name: 'user' }, - permissions: { can_delete: true } - }) - ); - const replyTextEl = document.querySelector(`.${CLASS_REPLY_TEXTAREA}`); - dialog.activateReply(); - replyTextEl.innerHTML += 'the preview SDK is great!'; - - dialog.postReply(); - expect(replyTextEl).to.have.value(''); - }); - }); - - describe('showDeleteConfirmation()', () => { - it('should show the correct UI when a user clicks on delete', () => { - dialog.addAnnotationElement( - new Annotation({ - annotationID: 1, - text: 'the preview sdk is amazing!', - user: { id: 1, name: 'user' }, - permissions: { can_delete: true } - }) - ); - const showElementStub = sandbox.stub(annotatorUtil, 'showElement'); - - dialog.showDeleteConfirmation(1); - expect(showElementStub).to.be.called; - }); - }); - - describe('hideDeleteConfirmation()', () => { - it('should show the correct UI when a user clicks cancel in the delete confirmation', () => { - dialog.addAnnotationElement( - new Annotation({ - annotationID: 1, - text: 'the preview sdk is amazing!', - user: { id: 1, name: 'user' }, - permissions: { can_delete: true } - }) - ); - const hideElementStub = sandbox.stub(annotatorUtil, 'hideElement'); - dialog.showDeleteConfirmation(1); - - dialog.hideDeleteConfirmation(1); - expect(hideElementStub).to.be.called; - }); - }); - - describe('deleteAnnotation()', () => { - it('should emit the annotationdelete message', () => { - dialog.addAnnotationElement( - new Annotation({ - annotationID: 1, - text: 'the preview sdk is amazing!', - user: { id: 1, name: 'user' }, - permissions: { can_delete: true } - }) - ); - - dialog.deleteAnnotation(1); - expect(stubs.emit).to.be.calledWith('annotationdelete', { annotationID: 1 }); - }); - }); - - describe('generateDialogEl', () => { - it('should generate a blank annotations dialog element', () => { - const dialogEl = dialog.generateDialogEl(0); - const createSectionEl = dialogEl.querySelector(constants.SECTION_CREATE); - const showSectionEl = dialogEl.querySelector(constants.SECTION_SHOW); - expect(createSectionEl).to.not.have.class(constants.CLASS_HIDDEN); - expect(showSectionEl).to.have.class(constants.CLASS_HIDDEN); - }); - - it('should generate an annotations dialog element with annotations', () => { - const dialogEl = dialog.generateDialogEl(1); - const createSectionEl = dialogEl.querySelector(constants.SECTION_CREATE); - const showSectionEl = dialogEl.querySelector(constants.SECTION_SHOW); - expect(createSectionEl).to.have.class(constants.CLASS_HIDDEN); - expect(showSectionEl).to.not.have.class(constants.CLASS_HIDDEN); - }); - }); - - describe('flipDialog()', () => { - const containerHeight = 5; - - beforeEach(() => { - sandbox.stub(dialog.element, 'querySelector').returns(document.createElement('div')); - sandbox.stub(dialog, 'fitDialogHeightInPage'); - sandbox.stub(dialog, 'toggleFlippedThreadEl'); - }); - - afterEach(() => { - dialog.element = null; - }); - - it('should keep the dialog below the annotation icon if the annotation is in the top half of the viewport', () => { - const { top, bottom } = dialog.flipDialog(2, containerHeight); - expect(dialog.element).to.not.have.class(CLASS_FLIPPED_DIALOG); - expect(top).not.equals(''); - expect(bottom).equals(''); - expect(dialog.fitDialogHeightInPage).to.be.called; - expect(dialog.toggleFlippedThreadEl).to.be.called; - }); - - it('should flip the dialog above the annotation icon if the annotation is in the lower half of the viewport', () => { - const { top, bottom } = dialog.flipDialog(4, containerHeight); - expect(dialog.element).to.have.class(CLASS_FLIPPED_DIALOG); - expect(top).equals(''); - expect(bottom).not.equals(''); - }); - }); - - describe('toggleFlippedThreadEl()', () => { - beforeEach(() => { - dialog.threadEl = document.createElement('div'); - }); - - it('should do nothing if the dialog is not flipped', () => { - stubs.add = sandbox.stub(dialog.threadEl.classList, 'add'); - stubs.remove = sandbox.stub(dialog.threadEl.classList, 'remove'); - dialog.toggleFlippedThreadEl(); - expect(stubs.add).to.not.be.called; - expect(stubs.remove).to.not.be.called; - }); - - it('should reset thread icon if dialog is flipped and hidden', () => { - dialog.element.classList.add(CLASS_FLIPPED_DIALOG); - stubs.add = sandbox.stub(dialog.threadEl.classList, 'add'); - stubs.remove = sandbox.stub(dialog.threadEl.classList, 'remove'); - dialog.toggleFlippedThreadEl(); - expect(stubs.add).to.be.called; - expect(stubs.remove).to.not.be.called; - }) - - it('should flip thread icon if dialog is flipped and not hidden', () => { - dialog.element.classList.add(CLASS_FLIPPED_DIALOG); - dialog.element.classList.add(constants.CLASS_HIDDEN); - stubs.add = sandbox.stub(dialog.threadEl.classList, 'add'); - stubs.remove = sandbox.stub(dialog.threadEl.classList, 'remove'); - dialog.toggleFlippedThreadEl(); - expect(stubs.add).to.not.be.called; - expect(stubs.remove).to.be.called; - }) - }); - - describe('fitDialogHeightInPage()', () => { - it('should allow scrolling on annotations dialog if file is a powerpoint', () => { - dialog.dialogEl = { style: {} }; - dialog.container = { clientHeight: 100 }; - dialog.fitDialogHeightInPage(); - expect(dialog.dialogEl.style.maxHeight).equals('50px'); - }); - }); -}); diff --git a/src/lib/annotations/__tests__/AnnotationModeController-test.js b/src/lib/annotations/__tests__/AnnotationModeController-test.js deleted file mode 100644 index ca6cb6bfc..000000000 --- a/src/lib/annotations/__tests__/AnnotationModeController-test.js +++ /dev/null @@ -1,194 +0,0 @@ -import AnnotationModeController from '../AnnotationModeController'; -import DocDrawingThread from '../doc/DocDrawingThread'; -import * as util from '../annotatorUtil'; - -let annotationModeController; -let stubs; -const sandbox = sinon.sandbox.create(); - -describe('lib/annotations/AnnotationModeController', () => { - beforeEach(() => { - annotationModeController = new AnnotationModeController(); - stubs = {}; - }); - - afterEach(() => { - sandbox.verifyAndRestore(); - stubs = null; - annotationModeController = null; - }); - - describe('registerAnnotator()', () => { - it('should internally keep track of the registered annotator', () => { - const annotator = 'I am an annotator'; - expect(annotationModeController.annotator).to.be.undefined; - - annotationModeController.registerAnnotator(annotator); - expect(annotationModeController.annotator).to.equal(annotator); - }); - }); - - describe('bindModeListeners()', () => { - it('should bind mode listeners', () => { - const handlerObj = { - type: 'event', - func: () => {}, - eventObj: { - addEventListener: sandbox.stub() - } - }; - sandbox.stub(annotationModeController, 'setupHandlers', () => { - annotationModeController.handlers = [handlerObj]; - }); - expect(annotationModeController.handlers.length).to.equal(0); - - annotationModeController.bindModeListeners(); - expect(handlerObj.eventObj.addEventListener).to.be.calledWith(handlerObj.type, handlerObj.func); - expect(annotationModeController.handlers.length).to.equal(1); - }); - }); - - describe('unbindModeListeners()', () => { - it('should unbind mode listeners', () => { - const handlerObj = { - type: 'event', - func: () => {}, - eventObj: { - removeEventListener: sandbox.stub() - } - }; - - annotationModeController.handlers = [handlerObj]; - expect(annotationModeController.handlers.length).to.equal(1); - - annotationModeController.unbindModeListeners(); - expect(handlerObj.eventObj.removeEventListener).to.be.calledWith(handlerObj.type, handlerObj.func); - expect(annotationModeController.handlers.length).to.equal(0); - }); - }); - - describe('registerThread()', () => { - it('should internally keep track of the registered thread', () => { - const thread = 'I am a thread'; - expect(annotationModeController.threads.includes(thread)).to.be.falsy; - - annotationModeController.registerThread(thread); - expect(annotationModeController.threads.includes(thread)).to.be.truthy; - }); - }); - - describe('unregisterThread()', () => { - it('should internally keep track of the registered thread', () => { - const thread = 'I am a thread'; - annotationModeController.threads = [thread, 'other']; - expect(annotationModeController.threads.includes(thread)).to.be.truthy; - - annotationModeController.unregisterThread(thread); - expect(annotationModeController.threads.includes(thread)).to.be.falsy; - }); - }); - - describe('bindCustomListenersOnThread()', () => { - it('should do nothing when the input is empty', () => { - annotationModeController.annotator = { - bindCustomListenersOnThread: sandbox.stub() - }; - - annotationModeController.bindCustomListenersOnThread(undefined); - expect(annotationModeController.annotator.bindCustomListenersOnThread).to.not.be.called; - }); - - it('should bind custom listeners on thread', () => { - const thread = { - addListener: sandbox.stub() - }; - annotationModeController.annotator = { - bindCustomListenersOnThread: sandbox.stub() - }; - - annotationModeController.bindCustomListenersOnThread(thread); - expect(annotationModeController.annotator.bindCustomListenersOnThread).to.be.called; - expect(thread.addListener).to.be.called; - }); - - // Catches edge case where sometimes the first click upon entering - // Draw annotation mode, the annotator is not registered properly - // with the controller - it('should maintain annotator context when a "threadevent" is fired', () => { - Object.defineProperty(DocDrawingThread.prototype, 'setup', { value: sandbox.stub() }); - Object.defineProperty(DocDrawingThread.prototype, 'getThreadEventData', { value: sandbox.stub() }); - const thread = new DocDrawingThread({ threadID: 123 }); - - annotationModeController.handleAnnotationEvent = () => { - expect(annotationModeController.annotator).to.not.be.undefined; - }; - annotationModeController.annotator = { - bindCustomListenersOnThread: sandbox.stub() - }; - - annotationModeController.bindCustomListenersOnThread(thread); - thread.emit('threadevent', {}); - }); - }); - - describe('unbindCustomListenersOnThread()', () => { - it('should do nothing when the input is empty', () => { - const thread = { - removeAllListeners: sandbox.stub() - }; - - annotationModeController.unbindCustomListenersOnThread(undefined); - expect(thread.removeAllListeners).to.not.be.called; - }); - - it('should bind custom listeners on thread', () => { - const thread = { - removeAllListeners: sandbox.stub() - }; - - annotationModeController.unbindCustomListenersOnThread(thread); - expect(thread.removeAllListeners).to.be.calledWith('threadevent'); - }); - }); - - describe('pushElementHandler()', () => { - it('should do nothing when the element is invalid', () => { - const lengthBefore = annotationModeController.handlers.length; - - annotationModeController.pushElementHandler(undefined, 'type', () => {}); - const lengthAfter = annotationModeController.handlers.length; - expect(lengthAfter).to.equal(lengthBefore); - }); - - it('should add a handler descriptor to the handlers array', () => { - const lengthBefore = annotationModeController.handlers.length; - const element = 'element'; - const type = ['type1', 'type2']; - const fn = 'fn'; - - annotationModeController.pushElementHandler(element, type, fn); - const handlers = annotationModeController.handlers; - const lengthAfter = handlers.length; - expect(lengthAfter).to.equal(lengthBefore+1); - expect(handlers[handlers.length - 1]).to.deep.equal({ - eventObj: element, - func: fn, - type - }); - }); - }); - - describe('setupHeader()', () => { - it('should insert the new header into the container before the baseheader', () => { - stubs.insertTemplate = sandbox.stub(util, 'insertTemplate'); - const container = { - firstElementChild: 'child' - }; - const header = document.createElement('div'); - - annotationModeController.setupHeader(container, header); - - expect(stubs.insertTemplate).to.be.calledWith(container, header); - }); - }); -}); diff --git a/src/lib/annotations/__tests__/AnnotationService-test.js b/src/lib/annotations/__tests__/AnnotationService-test.js deleted file mode 100644 index e7e7199fa..000000000 --- a/src/lib/annotations/__tests__/AnnotationService-test.js +++ /dev/null @@ -1,475 +0,0 @@ -/* eslint-disable no-unused-expressions */ -import fetchMock from 'fetch-mock'; -import '../../polyfill'; -import Annotation from '../Annotation'; -import AnnotationService from '../AnnotationService'; - -const API_HOST = 'https://app.box.com/api'; - -let annotationService; -let sandbox; - -describe('lib/annotations/AnnotationService', () => { - beforeEach(() => { - sandbox = sinon.sandbox.create(); - annotationService = new AnnotationService({ - apiHost: API_HOST, - fileId: 1, - token: 'someToken', - canAnnotate: true - }); - }); - - afterEach(() => { - sandbox.verifyAndRestore(); - fetchMock.restore(); - }); - - describe('generateID()', () => { - it('should return a rfc4122v4-compliant GUID', () => { - const GUID = AnnotationService.generateID(); - const regex = /^[a-z0-9]{8}-[a-z0-9]{4}-4[a-z0-9]{3}-[a-z0-9]{4}-[a-z0-9]{12}$/i; - expect(GUID.match(regex).length).to.satisfy; - }); - - it('should (almost always) return unique GUIDs', () => { - expect(AnnotationService.generateID() === AnnotationService.generateID()).to.be.false; - }); - }); - - describe('create()', () => { - const annotationToSave = new Annotation({ - fileVersionId: 2, - threadID: AnnotationService.generateID(), - type: 'point', - threadNumber: '1', - text: 'blah', - location: { x: 0, y: 0 } - }); - const url = `${API_HOST}/2.0/annotations`; - - it('should create annotation and return created object', () => { - fetchMock.mock(url, { - body: { - id: AnnotationService.generateID(), - item: { - id: annotationToSave.fileVersionId - }, - details: { - type: annotationToSave.type, - threadID: annotationToSave.threadID, - location: annotationToSave.location - }, - thread: annotationToSave.threadNumber, - message: annotationToSave.text, - created_by: {} - } - }); - const emitStub = sandbox.stub(annotationService, 'emit'); - - return annotationService.create(annotationToSave).then((createdAnnotation) => { - expect(createdAnnotation.fileVersionId).to.equal(annotationToSave.fileVersionId); - expect(createdAnnotation.threadID).to.equal(annotationToSave.threadID); - expect(createdAnnotation.threadNumber).to.equal(annotationToSave.threadNumber); - expect(createdAnnotation.type).to.equal(annotationToSave.type); - expect(createdAnnotation.text).to.equal(annotationToSave.text); - expect(createdAnnotation.location.x).to.equal(annotationToSave.location.x); - expect(createdAnnotation.location.y).to.equal(annotationToSave.location.y); - expect(emitStub).to.not.be.called; - }); - }); - - it('should reject with an error if there was a problem creating', () => { - fetchMock.mock(url, { - body: { - type: 'error' - } - }); - const emitStub = sandbox.stub(annotationService, 'emit'); - - return annotationService.create(annotationToSave).then( - () => { - throw new Error('Annotation should not be returned'); - }, - (error) => { - expect(error.message).to.equal('Could not create annotation'); - expect(emitStub).to.be.calledWith('annotationerror', { - reason: 'create', - error: sinon.match.string - }); - } - ); - }); - }); - - describe('read()', () => { - const url = `${API_HOST}/2.0/files/1/annotations?version=2&fields=item,thread,details,message,created_by,created_at,modified_at,permissions`; - - it('should return array of annotations for the specified file and file version', () => { - const annotation1 = new Annotation({ - fileVersionId: 2, - threadID: AnnotationService.generateID(), - type: 'point', - text: 'blah', - threadNumber: '1', - location: { x: 0, y: 0 } - }); - - const annotation2 = new Annotation({ - fileVersionId: 2, - threadID: AnnotationService.generateID(), - type: 'highlight', - text: 'blah2', - threadNumber: '2', - location: { x: 0, y: 0 } - }); - - fetchMock.mock(url, { - body: { - entries: [ - { - id: AnnotationService.generateID(), - item: { - id: annotation1.fileVersionId - }, - details: { - type: annotation1.type, - threadID: annotation1.threadID, - location: annotation1.location - }, - message: annotation1.text, - thread: annotation1.threadNumber, - created_by: {} - }, - { - id: AnnotationService.generateID(), - item: { - id: annotation2.fileVersionId - }, - details: { - type: annotation2.type, - threadID: annotation2.threadID, - location: annotation2.location - }, - message: annotation2.text, - threadNumber: annotation2.threadNumber, - created_by: {} - } - ] - } - }); - - return annotationService.read(2).then((annotations) => { - expect(annotations.length).to.equal(2); - - const createdAnnotation1 = annotations[0]; - const createdAnnotation2 = annotations[1]; - expect(createdAnnotation1.text).to.equal(annotation1.text); - expect(createdAnnotation2.text).to.equal(annotation2.text); - }); - }); - - it('should reject with an error if there was a problem reading', () => { - fetchMock.mock(url, { - body: { - type: 'error' - } - }); - - return annotationService.read(2).then( - () => { - throw new Error('Annotations should not be returned'); - }, - (error) => { - expect(error.message).to.equal('Could not read annotations from file version with ID 2'); - } - ); - }); - }); - - describe('delete()', () => { - const url = `${API_HOST}/2.0/annotations/3`; - - it('should successfully delete the annotation', () => { - fetchMock.mock(url, 204); - const emitStub = sandbox.stub(annotationService, 'emit'); - - return annotationService.delete(3).then(() => { - expect(fetchMock.called(url)).to.be.true; - expect(emitStub).to.not.be.called; - }); - }); - - it('should reject with an error if there was a problem deleting', () => { - fetchMock.mock(url, { - body: { - type: 'error' - } - }); - const emitStub = sandbox.stub(annotationService, 'emit'); - - return annotationService.delete(3).then( - () => { - throw new Error('Annotation should not have been deleted'); - }, - (error) => { - expect(error.message).to.equal('Could not delete annotation with ID 3'); - expect(emitStub).to.be.calledWith('annotationerror', { - reason: 'delete', - error: sinon.match.string - }); - } - ); - }); - }); - - describe('getThreadMap()', () => { - it('should call read and then generate a map of thread ID to annotations in those threads', () => { - const annotation1 = new Annotation({ - fileVersionId: 2, - threadID: AnnotationService.generateID(), - type: 'point', - text: 'blah', - threadNumber: '1', - location: { x: 0, y: 0 } - }); - - const annotation2 = new Annotation({ - fileVersionId: 2, - threadID: AnnotationService.generateID(), - type: 'point', - text: 'blah2', - threadNumber: '2', - location: { x: 0, y: 0 } - }); - - const annotation3 = new Annotation({ - fileVersionId: 2, - threadID: annotation1.threadID, - type: 'point', - text: 'blah3', - threadNumber: '1', - location: { x: 0, y: 0 } - }); - - sandbox.stub(annotationService, 'read').returns(Promise.resolve([annotation1, annotation2, annotation3])); - - return annotationService.getThreadMap(2).then((threadMap) => { - expect(threadMap[annotation1.threadID].length).to.equal(2); - expect(threadMap[annotation2.threadID][0]).to.contain(annotation2); - expect(threadMap[annotation1.threadID][0].threadNumber).to.equal(threadMap[annotation1.threadID][1].threadNumber); - expect(threadMap[annotation1.threadID][0].threadNumber).to.not.equal( - threadMap[annotation2.threadID][0].thread - ); - }); - }); - }); - - describe('createThreadMap()', () => { - it('should create a thread map with the correct annotations, in the correct order', () => { - // Dates are provided as a string format from the API such as "2016-10-30T14:19:56", - // ensures that the method converts to a Date() format for comparison/sorting - // Hard coding dates to ensure formatting resembles API response - const annotation1 = new Annotation({ - fileVersionId: 2, - threadID: AnnotationService.generateID(), - type: 'point', - text: 'blah', - threadNumber: '1', - location: { x: 0, y: 0 }, - created: '2016-10-29T14:19:56' - }); - - // Ensures annotations are not provided in chronological order - const annotation4 = new Annotation({ - fileVersionId: 2, - threadID: annotation1.threadID, - type: 'point', - text: 'blah4', - threadNumber: '1', - location: { x: 0, y: 0 }, - created: '2016-10-30T14:19:56' - }); - - const annotation2 = new Annotation({ - fileVersionId: 2, - threadID: AnnotationService.generateID(), - type: 'point', - text: 'blah2', - threadNumber: '2', - location: { x: 0, y: 0 }, - created: '2016-10-30T14:19:56' - }); - - const annotation3 = new Annotation({ - fileVersionId: 2, - threadID: annotation1.threadID, - type: 'point', - text: 'blah3', - threadNumber: '1', - location: { x: 0, y: 0 }, - created: '2016-10-31T14:19:56' - }); - - const threadMap = annotationService.createThreadMap([annotation1, annotation2, annotation3, annotation4]); - - expect(threadMap[annotation1.threadID].length).to.equal(3); - expect(threadMap[annotation1.threadID][0]).to.equal(annotation1); - expect(threadMap[annotation1.threadID][1]).to.equal(annotation4); - expect(threadMap[annotation1.threadID][0].threadNumber).to.equal(threadMap[annotation1.threadID][1].threadNumber); - expect(threadMap[annotation1.threadID][0].threadNumber).to.not.equal(threadMap[annotation2.threadID][0].threadNumber); - }); - }); - - describe('createAnnotation()', () => { - it('should call the Annotation constructor', () => { - const data = { - fileVersionId: 2, - threadID: 1, - type: 'point', - text: 'blah3', - threadNumber: '1', - location: { x: 0, y: 0 }, - created: Date.now(), - item: { id: 1 }, - details: { threadID: 1 }, - created_by: { id: 1 } - }; - const annotation1 = annotationService.createAnnotation(data); - - expect(annotation1 instanceof Annotation).to.be.true; - }); - }); - - describe('readFromMarker()', () => { - it('should get subsequent annotations if a marker is present', () => { - const markerUrl = annotationService.getReadUrl(2, 'a', 1); - - const annotation2 = new Annotation({ - fileVersionId: 2, - threadID: AnnotationService.generateID(), - type: 'highlight', - text: 'blah2', - threadNumber: '1', - location: { x: 0, y: 0 } - }); - - fetchMock.mock(markerUrl, { - body: { - entries: [ - { - id: AnnotationService.generateID(), - item: { - id: annotation2.fileVersionId - }, - details: { - type: annotation2.type, - threadID: annotation2.threadID, - location: annotation2.location - }, - thread: annotation2.threadNumber, - message: annotation2.text, - created_by: {} - } - ] - } - }); - - let resolve; - let reject; - const promise = new Promise((success, failure) => { - resolve = success; - reject = failure; - }); - - annotationService.annotations = []; - annotationService.readFromMarker(resolve, reject, 2, 'a', 1); - promise.then((result) => { - expect(result.length).to.equal(1); - expect(result[0].text).to.equal(annotation2.text); - expect(result[0].threadNumber).to.equal(annotation2.threadNumber); - }); - }); - - it('should reject with an error and show a notification if there was a problem reading', () => { - const markerUrl = annotationService.getReadUrl(2, 'a', 1); - - fetchMock.mock(markerUrl, { - body: { - type: 'error' - } - }); - const emitStub = sandbox.stub(annotationService, 'emit'); - - let resolve; - let reject; - const promise = new Promise((success, failure) => { - resolve = success; - reject = failure; - }); - - annotationService.annotations = []; - annotationService.readFromMarker(resolve, reject, 2, 'a', 1); - return promise.then( - () => { - throw new Error('Annotation should not have been deleted'); - }, - (error) => { - expect(error.message).to.equal('Could not read annotations from file version with ID 2'); - expect(emitStub).to.be.calledWith('annotationerror', { - reason: 'read', - error: sinon.match.string - }); - } - ); - }); - - it('should reject with an error and show a notification if the token is invalid', () => { - const markerUrl = annotationService.getReadUrl(2, 'a', 1); - - fetchMock.mock(markerUrl, 401); - const emitStub = sandbox.stub(annotationService, 'emit'); - - let resolve; - let reject; - const promise = new Promise((success, failure) => { - resolve = success; - reject = failure; - }); - - annotationService.annotations = []; - annotationService.readFromMarker(resolve, reject, 2, 'a', 1); - return promise.catch((error) => { - expect(error.message).to.equal('Could not read annotations from file due to invalid or expired token'); - expect(emitStub).to.be.calledWith('annotationerror', { - reason: 'authorization', - error: sinon.match.string - }); - }); - }); - }); - - describe('getReadUrl()', () => { - it('should return the original url if no limit or marker exists', () => { - annotationService.api = 'box'; - annotationService.fileId = 1; - const fileVersionId = 2; - const url = `${annotationService.api}/2.0/files/${annotationService.fileId}/annotations?version=${fileVersionId}&fields=item,thread,details,message,created_by,created_at,modified_at,permissions`; - - const result = annotationService.getReadUrl(fileVersionId); - expect(result).to.equal(url); - }); - - it('should add a marker and limit if provided', () => { - annotationService.api = 'box'; - annotationService.fileId = 1; - const fileVersionId = 2; - const marker = 'next_annotation'; - const limit = 1; - const url = `${annotationService.api}/2.0/files/${annotationService.fileId}/annotations?version=${fileVersionId}&fields=item,thread,details,message,created_by,created_at,modified_at,permissions&marker=${marker}&limit=${limit}`; - - const result = annotationService.getReadUrl(fileVersionId, marker, limit); - expect(result).to.equal(url); - }); - }); -}); diff --git a/src/lib/annotations/__tests__/AnnotationThread-test.html b/src/lib/annotations/__tests__/AnnotationThread-test.html deleted file mode 100644 index 5ed98fbe4..000000000 --- a/src/lib/annotations/__tests__/AnnotationThread-test.html +++ /dev/null @@ -1 +0,0 @@ -
diff --git a/src/lib/annotations/__tests__/AnnotationThread-test.js b/src/lib/annotations/__tests__/AnnotationThread-test.js deleted file mode 100644 index ce447f380..000000000 --- a/src/lib/annotations/__tests__/AnnotationThread-test.js +++ /dev/null @@ -1,783 +0,0 @@ -/* eslint-disable no-unused-expressions */ -import EventEmitter from 'events'; -import AnnotationThread from '../AnnotationThread'; -import Annotation from '../Annotation'; -import * as annotatorUtil from '../annotatorUtil'; -import { - STATES, - TYPES, - CLASS_ANNOTATION_POINT_MARKER, - DATA_TYPE_ANNOTATION_INDICATOR, - CLASS_HIDDEN, - THREAD_EVENT -} from '../annotationConstants'; - -let thread; -const sandbox = sinon.sandbox.create(); -let stubs = {}; - -describe('lib/annotations/AnnotationThread', () => { - before(() => { - fixture.setBase('src/lib'); - }); - - beforeEach(() => { - fixture.load('annotations/__tests__/AnnotationThread-test.html'); - - thread = new AnnotationThread({ - annotatedElement: document.querySelector('.annotated-element'), - annotations: [], - annotationService: {}, - fileVersionId: '1', - isMobile: false, - location: {}, - threadID: '2', - threadNumber: '1', - type: 'point' - }); - - thread.dialog = { - addListener: () => {}, - addAnnotation: () => {}, - destroy: () => {}, - setup: () => {}, - removeAllListeners: () => {}, - show: () => {}, - hide: () => {} - }; - stubs.dialogMock = sandbox.mock(thread.dialog); - - thread.annotationService = { - user: { id: '1' } - }; - - stubs.emit = sandbox.stub(thread, 'emit'); - }); - - afterEach(() => { - thread.annotationService = undefined; - sandbox.verifyAndRestore(); - if (typeof stubs.destroy === 'function') { - stubs.destroy(); - thread = null; - } - stubs = {}; - }); - - describe('destroy()', () => { - it('should unbind listeners and remove thread element and broadcast that the thread was deleted', () => { - stubs.unbindCustom = sandbox.stub(thread, 'unbindCustomListenersOnDialog'); - stubs.unbindDOM = sandbox.stub(thread, 'unbindDOMListeners'); - - thread.destroy(); - expect(stubs.unbindCustom).to.be.called; - expect(stubs.unbindDOM).to.be.called; - expect(stubs.emit).to.be.calledWith(THREAD_EVENT.threadDelete); - }); - - it('should not destroy the dialog on mobile', () => { - stubs.unbindCustom = sandbox.stub(thread, 'unbindCustomListenersOnDialog'); - stubs.destroyDialog = sandbox.stub(thread.dialog, 'destroy'); - thread.element = null; - thread.isMobile = true; - - thread.destroy(); - expect(stubs.unbindCustom).to.not.be.called; - expect(stubs.destroyDialog).to.not.be.called; - }); - }); - - describe('hide()', () => { - it('should hide the thread element', () => { - thread.hide(); - expect(thread.element).to.have.class(CLASS_HIDDEN); - }); - }); - - describe('reset()', () => { - it('should set the thread state to inactive', () => { - thread.reset(); - expect(thread.state).to.equal(STATES.inactive); - }); - }); - - describe('showDialog()', () => { - it('should setup the thread dialog if the dialog element does not already exist', () => { - thread.dialog.element = null; - stubs.dialogMock.expects('setup'); - stubs.dialogMock.expects('show'); - thread.showDialog(); - }); - - it('should not setup the thread dialog if the dialog element already exists', () => { - thread.dialog.element = {}; - stubs.dialogMock.expects('setup').never(); - stubs.dialogMock.expects('show'); - thread.showDialog(); - }); - }); - - describe('hideDialog()', () => { - it('should hide the thread dialog', () => { - stubs.dialogMock.expects('hide'); - thread.hideDialog(); - expect(thread.state).equals(STATES.inactive); - }); - }); - - describe('saveAnnotation()', () => { - let annotationService; - - beforeEach(() => { - annotationService = { - create: () => {} - }; - - thread = new AnnotationThread({ - annotatedElement: document.querySelector('.annotated-element'), - annotations: [], - annotationService, - fileVersionId: '1', - location: {}, - threadID: '2', - threadNumber: '1', - type: 'point' - }); - - sandbox.stub(thread, 'getThreadEventData').returns({}); - stubs.create = sandbox.stub(annotationService, 'create'); - }); - - it('should save an annotation with the specified type and text', () => { - stubs.create.returns(Promise.resolve({})); - - thread.saveAnnotation('point', 'blah'); - expect(stubs.create).to.be.calledWith( - sinon.match({ - fileVersionId: '1', - type: 'point', - text: 'blah', - threadID: '2', - threadNumber: '1' - }) - ); - expect(thread.state).to.equal(STATES.hover); - }); - - it('should delete the temporary annotation and broadcast an error if there was an error saving', (done) => { - stubs.create.returns(Promise.reject()); - stubs.handleError = sandbox.stub(thread, 'handleThreadSaveError'); - stubs.serverSave = sandbox.stub(thread, 'updateTemporaryAnnotation'); - - const promise = thread.saveAnnotation('point', 'blah'); - promise.should.be.fulfilled.then(() => { - expect(stubs.handleError).to.be.called; - done(); - }).catch(() => { - Assert.fail(); - }); - expect(stubs.create).to.be.called; - expect(stubs.serverSave).to.not.be.called; - }); - }); - - describe('updateTemporaryAnnotation()', () => { - let annotationService; - - beforeEach(() => { - annotationService = { - create: () => {} - }; - - thread = new AnnotationThread({ - annotatedElement: document.querySelector('.annotated-element'), - annotations: [], - annotationService, - fileVersionId: '1', - location: {}, - threadID: '2', - threadNumber: '1', - type: 'point' - }); - - stubs.create = sandbox.stub(annotationService, 'create'); - stubs.saveAnnotationToThread = sandbox.stub(thread, 'saveAnnotationToThread'); - sandbox.stub(thread, 'getThreadEventData').returns({}); - sandbox.stub(thread, 'showDialog'); - }); - - it('should save annotation to thread if it does not exist in annotations array', () => { - const serverAnnotation = 'real annotation'; - const tempAnnotation = serverAnnotation; - - thread.updateTemporaryAnnotation(tempAnnotation, serverAnnotation); - - expect(stubs.saveAnnotationToThread).to.be.called; - }); - - it('should overwrite a local annotation to the thread if it does exist as an associated annotation', () => { - const serverAnnotation = 'real annotation'; - const tempAnnotation = 'placeholder annotation'; - const isServerAnnotation = (annotation => (annotation === serverAnnotation)); - - thread.annotations.push(tempAnnotation) - expect(thread.annotations.find(isServerAnnotation)).to.be.undefined; - thread.updateTemporaryAnnotation(tempAnnotation, serverAnnotation); - expect(stubs.saveAnnotationToThread).to.not.be.called; - expect(thread.annotations.find(isServerAnnotation)).to.not.be.undefined; - }); - - it('should emit an annotationsaved event on success', (done) => { - const serverAnnotation = { threadNumber: 1 }; - const tempAnnotation = serverAnnotation; - thread.threadNumber = undefined; - thread.addListener(THREAD_EVENT.save, () => { - expect(stubs.saveAnnotationToThread).to.be.called; - done(); - }); - - thread.updateTemporaryAnnotation(tempAnnotation, serverAnnotation); - }); - }) - - describe('deleteAnnotation()', () => { - let annotationService; - - beforeEach(() => { - annotationService = { - delete: () => {} - }; - - stubs.annotation = { - annotationID: 'someID', - permissions: { - can_delete: true - } - }; - - stubs.annotation2 = { - annotationID: 'someID2', - permissions: { - can_delete: false - } - }; - - thread = new AnnotationThread({ - annotatedElement: document.querySelector('.annotated-element'), - annotations: [stubs.annotation], - annotationService, - fileVersionId: '1', - isMobile: false, - location: {}, - threadID: '2', - threadNumber: '1', - type: 'point' - }); - - thread.dialog = { - addListener: () => {}, - addAnnotation: () => {}, - destroy: () => {}, - removeAllListeners: () => {}, - show: () => {}, - hide: () => {}, - removeAnnotation: () => {}, - hideMobileDialog: () => {} - }; - stubs.dialogMock = sandbox.mock(thread.dialog); - - stubs.isPlain = sandbox.stub(annotatorUtil, 'isPlainHighlight'); - stubs.cancel = sandbox.stub(thread, 'cancelFirstComment'); - stubs.destroy = sandbox.stub(thread, 'destroy'); - thread.annotationService.user = { - id: 1 - }; - sandbox.stub(thread, 'getThreadEventData').returns({ - threadNumber: 1 - }); - }); - - it('should destroy the thread if the deleted annotation was the last annotation in the thread', () => { - thread.isMobile = false; - stubs.dialogMock.expects('removeAnnotation').never(); - stubs.dialogMock.expects('hideMobileDialog').never(); - sandbox.stub(annotationService, 'delete').returns(Promise.resolve()); - thread.deleteAnnotation('someID', false); - expect(stubs.destroy).to.be.called; - }); - - it('should destroy the thread and hide the mobile dialog if the deleted annotation was the last annotation in the thread on mobile', () => { - thread.isMobile = true; - stubs.dialogMock.expects('removeAnnotation'); - stubs.dialogMock.expects('hideMobileDialog'); - sandbox.stub(annotationService, 'delete').returns(Promise.resolve()); - thread.deleteAnnotation('someID', false); - }); - - it('should remove the relevant annotation from its dialog if the deleted annotation was not the last one', () => { - // Add another annotation to thread so 'someID' isn't the only annotation - thread.annotations.push(stubs.annotation2); - stubs.dialogMock.expects('removeAnnotation').withArgs('someID'); - sandbox.stub(annotationService, 'delete').returns(Promise.resolve()); - thread.deleteAnnotation('someID', false); - }); - - it('should make a server call to delete an annotation with the specified ID if useServer is true', () => { - sandbox.stub(annotationService, 'delete').returns(Promise.resolve()); - const promise = thread.deleteAnnotation('someID'); - promise.should.be.fulfilled.then(() => { - expect(stubs.emit).to.not.be.calledWith(THREAD_EVENT.threadCleanup); - expect(stubs.emit).to.be.calledWith(THREAD_EVENT.delete); - done(); - }).catch(() => { - Assert.fail(); - }); - expect(annotationService.delete).to.be.calledWith('someID'); - }); - - it('should also delete blank highlight comment from the server when removing the last comment on a highlight thread', () => { - stubs.annotation2.permissions.can_delete = false; - thread.annotations.push(stubs.annotation2); - stubs.isPlain.returns(true); - sandbox.stub(annotationService, 'delete').returns(Promise.resolve()); - thread.deleteAnnotation('someID', true); - expect(annotationService.delete).to.be.calledWith('someID'); - }); - - it('should not make a server call to delete an annotation with the specified ID if useServer is false', () => { - sandbox.stub(annotationService, 'delete').returns(Promise.resolve()); - thread.deleteAnnotation('someID', false); - expect(annotationService.delete).to.not.be.called; - }); - - it('should broadcast an error if there was an error deleting from server', (done) => { - sandbox.stub(annotationService, 'delete').returns(Promise.reject()); - thread.on('annotationdeleteerror', () => { - done(); - }); - thread.deleteAnnotation('someID', true); - expect(annotationService.delete).to.be.called; - }); - - it('should toggle highlight dialogs with the delete of the last comment if user does not have permission to delete the entire annotation', () => { - sandbox.stub(annotationService, 'delete').returns(Promise.resolve()); - thread.annotations.push(stubs.annotation2); - stubs.isPlain.returns(true); - thread.deleteAnnotation('someID', false); - expect(stubs.cancel).to.be.called; - expect(stubs.destroy).to.not.be.called; - }); - - it('should destroy the annotation with the delete of the last comment if the user has permissions', () => { - stubs.annotation2.permissions.can_delete = true; - thread.annotations.push(stubs.annotation2); - stubs.isPlain.returns(true); - sandbox.stub(annotationService, 'delete').returns(Promise.resolve()); - const promise = thread.deleteAnnotation('someID'); - promise.should.be.fulfilled.then(() => { - expect(stubs.emit).to.be.calledWith(THREAD_EVENT.threadCleanup); - expect(stubs.emit).to.be.calledWith(THREAD_EVENT.delete); - done(); - }).catch(() => { - Assert.fail(); - }); - expect(stubs.cancel).to.not.be.called; - expect(stubs.destroy).to.be.called; - }); - }); - - describe('scrollIntoView()', () => { - it('should scroll to annotation page and center annotation in viewport', () => { - sandbox.stub(thread, 'scrollToPage'); - sandbox.stub(thread, 'centerAnnotation'); - thread.scrollIntoView(); - expect(thread.scrollToPage); - expect(thread.centerAnnotation).to.be.calledWith(sinon.match.number); - }) - }); - - describe('scrollToPage()', () => { - it('should do nothing if annotation does not have a location or page', () => { - const pageEl = { - scrollIntoView: sandbox.stub() - }; - - thread.location = {}; - thread.scrollToPage(); - - thread.location = null; - thread.scrollToPage(); - expect(pageEl.scrollIntoView).to.not.be.called; - }); - - it('should scroll annotation\'s page into view', () => { - thread.location = { page: 1 }; - const pageEl = { - scrollIntoView: sandbox.stub() - }; - thread.annotatedElement = { - querySelector: sandbox.stub().returns(pageEl) - }; - thread.scrollToPage(); - expect(pageEl.scrollIntoView).to.be.called; - }); - }); - - describe('centerAnnotation', () => { - beforeEach(() => { - thread.annotatedElement = { - scrollHeight: 100, - scrollTop: 0, - scrollBottom: 200 - }; - }); - - it('should scroll so annotation is vertically centered in viewport', () => { - thread.centerAnnotation(50); - expect(thread.annotatedElement.scrollTop).equals(50); - }); - - it('should scroll so annotation is vertically centered in viewport', () => { - thread.centerAnnotation(150); - expect(thread.annotatedElement.scrollTop).equals(200); - }); - }); - - describe('location()', () => { - it('should get location', () => { - expect(thread.location).to.equal(thread.location); - }); - }); - - describe('threadID()', () => { - it('should get threadID', () => { - expect(thread.threadID).to.equal(thread.threadID); - }); - }); - - describe('thread()', () => { - it('should get thread', () => { - expect(thread.thread).to.equal(thread.thread); - }); - }); - - describe('type()', () => { - it('should get type', () => { - expect(thread.type).to.equal(thread.type); - }); - }); - - describe('state()', () => { - it('should get state', () => { - expect(thread.state).to.equal(thread.state); - }); - }); - - describe('setup()', () => { - beforeEach(() => { - stubs.create = sandbox.stub(thread, 'createDialog'); - stubs.bind = sandbox.stub(thread, 'bindCustomListenersOnDialog'); - stubs.setup = sandbox.stub(thread, 'setupElement'); - }); - - it('should setup dialog', () => { - thread.dialog = {}; - thread.setup(); - expect(stubs.create).to.be.called; - expect(stubs.bind).to.be.called; - expect(stubs.setup).to.be.called; - expect(thread.dialog.isMobile).to.equal(thread.isMobile); - }); - - it('should set state to pending if thread is initialized with no annotations', () => { - thread.setup(); - expect(thread.state).to.equal(STATES.pending); - }); - - it('should set state to inactive if thread is initialized with annotations', () => { - thread = new AnnotationThread({ - annotatedElement: document.querySelector('.annotated-element'), - annotations: [{}], - annotationService: {}, - fileVersionId: '1', - isMobile: false, - location: {}, - threadID: '2', - threadNumber: '1', - type: 'point' - }); - - thread.setup(); - expect(thread.state).to.equal(STATES.inactive); - }); - }); - - describe('setupElement()', () => { - it('should create element and bind listeners', () => { - stubs.bind = sandbox.stub(thread, 'bindDOMListeners'); - - thread.setupElement(); - expect(thread.element instanceof HTMLElement).to.be.true; - expect(thread.element).to.have.class(CLASS_ANNOTATION_POINT_MARKER); - expect(stubs.bind).to.be.called; - }); - }); - - describe('bindDOMListeners()', () => { - beforeEach(() => { - thread.element = document.createElement('div'); - stubs.add = sandbox.stub(thread.element, 'addEventListener'); - thread.isMobile = false; - }); - - it('should do nothing if element does not exist', () => { - thread.element = null; - thread.bindDOMListeners(); - expect(stubs.add).to.not.be.called; - }); - - it('should bind DOM listeners', () => { - thread.bindDOMListeners(); - expect(stubs.add).to.be.calledWith('click', sinon.match.func); - expect(stubs.add).to.be.calledWith('mouseenter', sinon.match.func); - expect(stubs.add).to.be.calledWith('mouseleave', sinon.match.func); - }); - - it('should not add mouseleave listener for mobile browsers', () => { - thread.isMobile = true; - thread.bindDOMListeners(); - expect(stubs.add).to.be.calledWith('click', sinon.match.func); - expect(stubs.add).to.be.calledWith('mouseenter', sinon.match.func); - expect(stubs.add).to.not.be.calledWith('mouseleave', sinon.match.func); - }); - }); - - describe('unbindDOMListeners()', () => { - beforeEach(() => { - thread.element = document.createElement('div'); - stubs.remove = sandbox.stub(thread.element, 'removeEventListener'); - thread.isMobile = false; - }); - - it('should do nothing if element does not exist', () => { - thread.element = null; - thread.unbindDOMListeners(); - expect(stubs.remove).to.not.be.called; - }); - - it('should unbind DOM listeners', () => { - thread.unbindDOMListeners(); - expect(stubs.remove).to.be.calledWith('click', sinon.match.func); - expect(stubs.remove).to.be.calledWith('mouseenter', sinon.match.func); - expect(stubs.remove).to.be.calledWith('mouseleave', sinon.match.func); - }); - - it('should not add mouseleave listener for mobile browsers', () => { - thread.isMobile = true; - thread.unbindDOMListeners(); - expect(stubs.remove).to.be.calledWith('click', sinon.match.func); - expect(stubs.remove).to.be.calledWith('mouseenter', sinon.match.func); - expect(stubs.remove).to.not.be.calledWith('mouseleave', sinon.match.func); - }); - }); - - describe('bindCustomListenersOnDialog()', () => { - it('should do nothing if dialog does not exist', () => { - thread.dialog = null; - stubs.dialogMock.expects('addListener').never(); - thread.bindCustomListenersOnDialog(); - }); - - it('should bind custom listeners on dialog', () => { - stubs.dialogMock.expects('addListener').withArgs('annotationcreate', sinon.match.func); - stubs.dialogMock.expects('addListener').withArgs('annotationcancel', sinon.match.func); - stubs.dialogMock.expects('addListener').withArgs('annotationdelete', sinon.match.func); - thread.bindCustomListenersOnDialog(); - }); - }); - - describe('unbindCustomListenersOnDialog()', () => { - it('should do nothing if dialog does not exist', () => { - thread.dialog = null; - stubs.dialogMock.expects('removeAllListeners').never(); - thread.unbindCustomListenersOnDialog(); - }); - - it('should unbind custom listeners from dialog', () => { - stubs.dialogMock.expects('removeAllListeners').withArgs('annotationcreate'); - stubs.dialogMock.expects('removeAllListeners').withArgs('annotationcancel'); - stubs.dialogMock.expects('removeAllListeners').withArgs('annotationdelete'); - thread.unbindCustomListenersOnDialog(); - }); - }); - - describe('cancelUnsavedAnnotation()', () => { - it('should only destroy thread if on a mobile browser or in a pending/pending-active state', () => { - sandbox.stub(thread, 'destroy'); - - // mobile - thread.isMobile = true; - thread.cancelUnsavedAnnotation(); - expect(thread.destroy).to.be.called; - expect(thread.emit).to.be.calledWith(THREAD_EVENT.cancel); - - // 'pending' state - thread.isMobile = false; - thread.state = STATES.pending; - thread.cancelUnsavedAnnotation(); - expect(thread.destroy).to.be.called; - expect(thread.emit).to.be.calledWith(THREAD_EVENT.cancel); - - // 'pending-active' state - thread.state = STATES.pending_active; - thread.cancelUnsavedAnnotation(); - expect(thread.destroy).to.be.called; - expect(thread.emit).to.be.calledWith(THREAD_EVENT.cancel); - }); - }); - - describe('getThreadEventData()', () => { - it('should return thread type and threadID', () => { - thread.annotationService.user = { id: -1 }; - thread.threadNumber = undefined; - const data = thread.getThreadEventData(); - expect(data).to.deep.equal({ - type: thread.type, - threadID: thread.threadID - }); - }); - - it('should also return annotator\'s user id', () => { - thread.annotationService.user = { id: 1 }; - thread.threadNumber = undefined; - const data = thread.getThreadEventData(); - expect(data).to.deep.equal({ - type: thread.type, - threadID: thread.threadID, - userId: 1 - }); - }); - - it('should return thread type and threadID', () => { - thread.annotationService.user = { id: -1 }; - thread.threadNumber = 1; - const data = thread.getThreadEventData(); - expect(data).to.deep.equal({ - type: thread.type, - threadID: thread.threadID, - threadNumber: 1 - }); - }); - }); - - describe('createElement()', () => { - it('should create an element with the right class and attribute', () => { - const element = thread.createElement(); - expect(element).to.have.class(CLASS_ANNOTATION_POINT_MARKER); - expect(element).to.have.attribute('data-type', DATA_TYPE_ANNOTATION_INDICATOR); - }); - }); - - describe('mouseoutHandler()', () => { - it('should do nothing if event does not exist', () => { - stubs.isInDialog = sandbox.stub(annotatorUtil, 'isInDialog'); - thread.mouseoutHandler(); - expect(stubs.isInDialog).to.not.be.called; - }); - - it('should not call hideDialog if there are no annotations in the thread', () => { - stubs.hide = sandbox.stub(thread, 'hideDialog'); - thread.mouseoutHandler({}); - expect(stubs.hide).to.not.be.called; - }); - - it('should call hideDialog if there are annotations in the thread', () => { - stubs.hide = sandbox.stub(thread, 'hideDialog'); - const annotation = new Annotation({ - fileVersionId: '2', - threadID: '1', - type: 'point', - text: 'blah', - threadNumber: '1', - location: { x: 0, y: 0 }, - created: Date.now() - }); - - thread.annotations = [annotation]; - thread.mouseoutHandler({}); - expect(stubs.hide).to.be.called; - }); - }); - - describe('saveAnnotationToThread()', () => { - it('should push the annotation, and add to the dialog when the dialog exists', () => { - stubs.add = sandbox.stub(thread.dialog, 'addAnnotation'); - stubs.push = sandbox.stub(thread.annotations, 'push'); - const annotation = new Annotation({ - fileVersionId: '2', - threadID: '1', - type: 'point', - text: 'blah', - threadNumber: '1', - location: { x: 0, y: 0 }, - created: Date.now() - }); - - thread.saveAnnotationToThread(annotation); - expect(stubs.add).to.be.calledWith(annotation); - expect(stubs.push).to.be.calledWith(annotation); - }); - - it('should not try to push an annotation to the dialog if it doesn\'t exist', () => { - stubs.add = sandbox.stub(thread.dialog, 'addAnnotation'); - const annotation = new Annotation({ - fileVersionId: '2', - threadID: '1', - type: 'point', - text: 'blah', - threadNumber: '1', - location: { x: 0, y: 0 }, - created: Date.now() - }); - - thread.dialog = undefined; - thread.saveAnnotationToThread(annotation); - expect(stubs.add).to.not.be.called; - }); - }); - - describe('createAnnotationDialog()', () => { - it('should correctly create the annotation data object', () => { - const annotationData = thread.createAnnotationData('highlight', 'test'); - expect(annotationData.location).to.equal(thread.location); - expect(annotationData.fileVersionId).to.equal(thread.fileVersionId); - expect(annotationData.thread).to.equal(thread.thread); - expect(annotationData.user.id).to.equal('1'); - }); - }); - - describe('createAnnotation()', () => { - it('should create a new point annotation', () => { - sandbox.stub(thread, 'saveAnnotation'); - thread.createAnnotation({ text: 'bleh' }); - expect(thread.saveAnnotation).to.be.calledWith(TYPES.point, 'bleh'); - }); - }); - - describe('deleteAnnotationWithID()', () => { - it('should delete a point annotation with the matching annotationID', () => { - sandbox.stub(thread, 'deleteAnnotation'); - thread.deleteAnnotationWithID({ annotationID: 1 }); - expect(thread.deleteAnnotation).to.be.calledWith(1); - }); - }); - - describe('handleThreadSaveError()', () => { - it('should delete temp annotation and emit event', () => { - sandbox.stub(thread, 'deleteAnnotation'); - thread.handleThreadSaveError(new Error(), 1); - expect(thread.deleteAnnotation).to.be.calledWith(1, false); - expect(thread.emit).to.be.calledWith(THREAD_EVENT.createError); - }); - }); -}); diff --git a/src/lib/annotations/__tests__/Annotator-test.html b/src/lib/annotations/__tests__/Annotator-test.html deleted file mode 100644 index 48a5d5c03..000000000 --- a/src/lib/annotations/__tests__/Annotator-test.html +++ /dev/null @@ -1,2 +0,0 @@ - -
diff --git a/src/lib/annotations/__tests__/Annotator-test.js b/src/lib/annotations/__tests__/Annotator-test.js deleted file mode 100644 index 82f567e24..000000000 --- a/src/lib/annotations/__tests__/Annotator-test.js +++ /dev/null @@ -1,1275 +0,0 @@ -/* eslint-disable no-unused-expressions */ -import EventEmitter from 'events'; -import Annotator from '../Annotator'; -import * as annotatorUtil from '../annotatorUtil'; -import AnnotationService from '../AnnotationService'; -import { - STATES, - TYPES, - CLASS_ANNOTATION_DRAW_MODE, - CLASS_ANNOTATION_MODE, - CLASS_ACTIVE, - CLASS_HIDDEN, - SELECTOR_ANNOTATION_BUTTON_DRAW_POST, - SELECTOR_ANNOTATION_BUTTON_DRAW_UNDO, - SELECTOR_ANNOTATION_BUTTON_DRAW_REDO, - SELECTOR_ANNOTATION_DRAWING_HEADER, - SELECTOR_BOX_PREVIEW_BASE_HEADER, - ANNOTATOR_EVENT, - THREAD_EVENT -} from '../annotationConstants'; - -let annotator; -let stubs = {}; -const sandbox = sinon.sandbox.create(); - -describe('lib/annotations/Annotator', () => { - before(() => { - fixture.setBase('src/lib'); - }); - - beforeEach(() => { - fixture.load('annotations/__tests__/Annotator-test.html'); - - const options = { - annotator: { - NAME: 'name' - } - }; - annotator = new Annotator({ - canAnnotate: true, - container: document, - annotationService: {}, - file: { - file_version: { id: 1 } - }, - isMobile: false, - options, - modeButtons: {}, - location: {}, - localizedStrings: { - anonymousUserName: 'anonymous', - loadError: 'load error', - createError: 'create error', - deleteError: 'delete error', - authError: 'auth error', - } - }); - annotator.threads = {}; - annotator.modeControllers = {}; - - stubs.thread = { - threadID: '123abc', - show: () => {}, - hide: () => {}, - addListener: () => {}, - unbindCustomListenersOnThread: () => {}, - removeListener: () => {}, - scrollIntoView: () => {}, - getThreadEventData: () => {}, - type: 'type' - }; - stubs.threadMock = sandbox.mock(stubs.thread); - - stubs.thread2 = { - threadID: '456def', - show: () => {}, - hide: () => {}, - addListener: () => {}, - unbindCustomListenersOnThread: () => {}, - removeAllListeners: () => {}, - type: 'type' - }; - stubs.threadMock2 = sandbox.mock(stubs.thread2); - - stubs.thread3 = { - threadID: '789ghi', - show: () => {}, - hide: () => {}, - addListener: () => {}, - unbindCustomListenersOnThread: () => {}, - removeAllListeners: () => {}, - type: 'type' - }; - stubs.threadMock3 = sandbox.mock(stubs.thread3); - }); - - afterEach(() => { - sandbox.verifyAndRestore(); - annotator.modeButtons = {}; - annotator.modeControllers = {}; - - if (typeof annotator.destroy === 'function') { - annotator.destroy(); - annotator = null; - } - - stubs = {}; - }); - - describe('init()', () => { - beforeEach(() => { - const annotatedEl = document.querySelector('.annotated-element'); - sandbox.stub(annotator, 'getAnnotatedEl').returns(annotatedEl); - annotator.annotatedElement = annotatedEl; - - stubs.scale = sandbox.stub(annotator, 'setScale'); - stubs.setup = sandbox.stub(annotator, 'setupAnnotations'); - stubs.show = sandbox.stub(annotator, 'showAnnotations'); - stubs.setupMobileDialog = sandbox.stub(annotator, 'setupMobileDialog'); - stubs.showButton = sandbox.stub(annotator, 'showModeAnnotateButton'); - stubs.getPermissions = sandbox.stub(annotator, 'getAnnotationPermissions'); - - annotator.permissions = { canAnnotate: true }; - annotator.modeButtons = { - point: { selector: 'point_btn' }, - draw: { selector: 'draw_btn' } - }; - }); - - afterEach(() => { - annotator.modeButtons = {}; - }); - - it('should set scale and setup annotations', () => { - annotator.init(5); - expect(stubs.scale).to.be.calledWith(5); - expect(stubs.setup).to.be.called; - expect(stubs.show).to.be.called; - expect(annotator.annotationService).to.not.be.null; - expect(stubs.getPermissions).to.be.called; - }); - - it('should setup mobile dialog for mobile browsers', () => { - annotator.isMobile = true; - annotator.init(); - expect(stubs.setupMobileDialog).to.be.called; - expect(stubs.getPermissions).to.be.called; - }); - }); - - describe('setupMobileDialog()', () => { - it('should generate mobile annotations dialog and append to container', () => { - annotator.container = { - appendChild: sandbox.mock() - }; - annotator.setupMobileDialog(); - expect(annotator.container.appendChild).to.be.called; - }); - }); - - describe('showAnnotations()', () => { - it('should fetch and then render annotations', () => { - const renderStub = sandbox.stub(annotator, 'renderAnnotations'); - const fetchPromise = Promise.resolve(); - const fetchStub = sandbox.stub(annotator, 'fetchAnnotations').returns(fetchPromise); - - annotator.showAnnotations(); - - expect(fetchStub).to.be.called; - return fetchPromise.then(() => { - expect(renderStub).to.be.called; - }); - }); - }); - - describe('setupAnnotations()', () => { - it('should initialize thread map and bind DOM listeners', () => { - sandbox.stub(annotator, 'bindDOMListeners'); - sandbox.stub(annotator, 'bindCustomListenersOnService'); - sandbox.stub(annotator, 'addListener'); - - annotator.setupAnnotations(); - - expect(annotator.threads).to.not.be.undefined; - expect(annotator.bindDOMListeners).to.be.called; - expect(annotator.bindCustomListenersOnService).to.be.called; - expect(annotator.addListener).to.be.calledWith(ANNOTATOR_EVENT.scale, sinon.match.func); - }); - }); - - describe('once annotator is initialized', () => { - beforeEach(() => { - const annotatedEl = document.querySelector('.annotated-element'); - annotator.annotatedElement = annotatedEl; - sandbox.stub(annotator, 'getAnnotatedEl').returns(annotatedEl); - sandbox.stub(annotator, 'setupAnnotations'); - sandbox.stub(annotator, 'showAnnotations'); - - stubs.thread.location = { page: 1 }; - stubs.thread2.location = { page: 2 }; - stubs.thread3.location = { page: 2 }; - annotator.addThreadToMap(stubs.thread); - annotator.addThreadToMap(stubs.thread2); - annotator.addThreadToMap(stubs.thread3); - - annotator.init(); - }); - - afterEach(() => { - annotator.threads = {}; - }); - - describe('destroy()', () => { - it('should unbind custom listeners on thread and unbind DOM listeners', () => { - stubs.thread.location = { page: 1 }; - annotator.addThreadToMap(stubs.thread); - - const unbindCustomStub = sandbox.stub(annotator, 'unbindCustomListenersOnThread'); - const unbindDOMStub = sandbox.stub(annotator, 'unbindDOMListeners'); - const unbindCustomListenersOnService = sandbox.stub(annotator, 'unbindCustomListenersOnService'); - const unbindListener = sandbox.stub(annotator, 'removeListener'); - - annotator.destroy(); - - expect(unbindCustomStub).to.be.calledWith(stubs.thread); - expect(unbindDOMStub).to.be.called; - expect(unbindCustomListenersOnService).to.be.called; - expect(unbindListener).to.be.calledWith(ANNOTATOR_EVENT.scale, sinon.match.func); - }); - }); - - describe('hideAnnotations()', () => { - it('should call hide on each thread in map', () => { - sandbox.stub(annotator, 'hideAnnotationsOnPage'); - annotator.hideAnnotations(); - expect(annotator.hideAnnotationsOnPage).to.be.calledTwice; - }); - }); - - 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); - }); - }); - - describe('renderAnnotations()', () => { - it('should call show on each thread', () => { - sandbox.stub(annotator, 'renderAnnotationsOnPage'); - annotator.renderAnnotations(); - expect(annotator.renderAnnotationsOnPage).to.be.calledTwice; - }); - }); - - describe('renderAnnotationsOnPage()', () => { - it('should call show on each thread', () => { - sandbox.stub(annotator, 'isModeAnnotatable').returns(true); - stubs.threadMock.expects('show'); - stubs.threadMock2.expects('show').never(); - 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()', () => { - beforeEach(() => { - annotator.permissions.canAnnotate = true; - stubs.hide = sandbox.stub(annotatorUtil, 'hideElement'); - stubs.show = sandbox.stub(annotatorUtil, 'showElement'); - stubs.render = sandbox.stub(annotator, 'renderAnnotations'); - - annotator.modeButtons = { - point: { selector: 'point_btn' }, - draw: { selector: 'draw_btn' } - }; - }); - - afterEach(() => { - annotator.modeButtons = {}; - }); - - it('should only render annotations if user cannot annotate', () => { - annotator.permissions.canAnnotate = false; - annotator.rotateAnnotations(); - expect(stubs.hide).to.not.be.called; - expect(stubs.show).to.not.be.called; - expect(stubs.render).to.be.called; - }); - - it('should hide point annotation button if image is rotated', () => { - annotator.rotateAnnotations(90); - expect(stubs.hide).to.be.called; - expect(stubs.show).to.not.be.called; - expect(stubs.render).to.be.called; - }); - - it('should show point annotation button if image is rotated', () => { - annotator.rotateAnnotations(); - expect(stubs.hide).to.not.be.called; - expect(stubs.show).to.be.called; - expect(stubs.render).to.be.called; - }); - }); - - 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('exitAnnotationModesExcept()', () => { - it('should call disableAnnotationMode on all modes except the specified one', () => { - annotator.modeButtons = { - 'type1': { - selector: 'bogus', - button: 'button1' - }, - 'type2': { - selector: 'test', - button: 'button2' - } - }; - - sandbox.stub(annotator, 'disableAnnotationMode'); - annotator.exitAnnotationModesExcept('type2'); - expect(annotator.disableAnnotationMode).to.be.calledWith('type1', 'button1'); - expect(annotator.disableAnnotationMode).to.not.be.calledWith('type2', 'button2'); - }); - }); - - describe('toggleAnnotationHandler()', () => { - beforeEach(() => { - stubs.destroyStub = sandbox.stub(annotator, 'destroyPendingThreads'); - stubs.annotationMode = sandbox.stub(annotator, 'isInAnnotationMode'); - stubs.exitModes = sandbox.stub(annotator, 'exitAnnotationModesExcept'); - stubs.disable = sandbox.stub(annotator, 'disableAnnotationMode'); - stubs.enable = sandbox.stub(annotator, 'enableAnnotationMode'); - sandbox.stub(annotator, 'getAnnotateButton'); - stubs.isAnnotatable = sandbox.stub(annotator, 'isModeAnnotatable').returns(true); - - annotator.modeButtons = { - point: { selector: 'point_btn' }, - draw: { selector: 'draw_btn' } - }; - - annotator.createHighlightDialog = { - isVisible: false, - hide: sandbox.stub() - } - }); - - afterEach(() => { - annotator.modeButtons = {}; - }); - - it('should do nothing if specified annotation type is not annotatable', () => { - stubs.isAnnotatable.returns(false); - annotator.toggleAnnotationHandler('bleh'); - expect(stubs.destroyStub).to.not.be.called; - }); - - it('should do nothing if specified annotation type does not have a mode button', () => { - annotator.toggleAnnotationHandler(TYPES.highlight); - expect(stubs.destroyStub).to.be.called; - expect(stubs.exitModes).to.not.be.called; - }); - - it('should hide the highlight dialog and remove selection if it is visible', () => { - const getSelectionStub = sandbox.stub(document, 'getSelection').returns({ - removeAllRanges: sandbox.stub() - }); - - annotator.toggleAnnotationHandler(TYPES.highlight); - expect(annotator.createHighlightDialog.hide).to.not.be.called; - expect(getSelectionStub).to.not.be.called; - - annotator.createHighlightDialog.isVisible = true; - - annotator.toggleAnnotationHandler(TYPES.highlight); - expect(annotator.createHighlightDialog.hide).to.be.called; - expect(getSelectionStub).to.be.called; - }); - - it('should turn annotation mode on if it is off', () => { - stubs.annotationMode.returns(false); - - annotator.toggleAnnotationHandler(TYPES.point); - - expect(stubs.destroyStub).to.be.called; - expect(stubs.exitModes).to.be.called; - expect(stubs.enable).to.be.called; - }); - - it('should turn annotation mode off if it is on', () => { - stubs.annotationMode.returns(true); - - annotator.toggleAnnotationHandler(TYPES.point); - - expect(stubs.destroyStub).to.be.called; - expect(stubs.exitModes).to.be.called; - expect(stubs.disable).to.be.called; - }); - }); - - describe('disableAnnotationMode()', () => { - beforeEach(() => { - annotator.currentAnnotationMode = TYPES.point; - stubs.isModeAnnotatable = sandbox.stub(annotator, 'isModeAnnotatable').returns(true); - stubs.isInMode = sandbox.stub(annotator, 'isInAnnotationMode').returns(false); - stubs.emit = sandbox.stub(annotator, 'emit'); - stubs.unbindMode = sandbox.stub(annotator, 'unbindModeListeners'); - stubs.bindDOM = sandbox.stub(annotator, 'bindDOMListeners'); - }); - - it('should do nothing when the mode is not annotatable', () => { - stubs.isModeAnnotatable.returns(false); - annotator.annotatedElement = null; - - expect(annotator.disableAnnotationMode, TYPES.draw).to.not.throw(); - }); - - it('should exit annotation mode if currently in the specified mode', () => { - stubs.isInMode.returns(true); - annotator.disableAnnotationMode(TYPES.point); - expect(stubs.emit).to.be.calledWith(ANNOTATOR_EVENT.modeExit, sinon.match.object); - expect(stubs.unbindMode).to.be.calledWith(TYPES.point); - expect(stubs.bindDOM).to.be.called; - expect(annotator.annotatedElement).to.not.have.class(CLASS_ANNOTATION_MODE); - expect(annotator.currentAnnotationMode).to.be.null; - }); - - it('should deactivate point annotation mode button', () => { - const btn = document.querySelector('.bp-btn-annotate'); - annotator.disableAnnotationMode(TYPES.point, btn); - expect(btn).to.not.have.class(CLASS_ACTIVE); - }); - - it('should deactivate draw annotation mode button', () => { - const btn = document.querySelector('.bp-btn-annotate'); - annotator.disableAnnotationMode(TYPES.draw, btn); - expect(btn).to.not.have.class(CLASS_ACTIVE); - }); - }); - - describe('enableAnnotationMode()', () => { - beforeEach(() => { - stubs.emit = sandbox.stub(annotator, 'emit'); - stubs.unbindDOM = sandbox.stub(annotator, 'unbindDOMListeners'); - stubs.bindMode = sandbox.stub(annotator, 'bindModeListeners'); - }); - - it('should enter annotation mode', () => { - annotator.enableAnnotationMode(TYPES.point); - expect(stubs.emit).to.be.calledWith(ANNOTATOR_EVENT.modeEnter, sinon.match.object); - expect(stubs.unbindDOM).to.be.called; - expect(stubs.bindMode).to.be.calledWith(TYPES.point); - expect(annotator.annotatedElement).to.have.class(CLASS_ANNOTATION_MODE); - }); - - it('should deactivate point annotation mode button', () => { - const btn = document.querySelector('.bp-btn-annotate'); - annotator.enableAnnotationMode(TYPES.point, btn); - expect(btn).to.have.class(CLASS_ACTIVE); - }); - - it('should deactivate draw annotation mode button', () => { - const btn = document.querySelector('.bp-btn-annotate'); - annotator.enableAnnotationMode(TYPES.draw, btn); - expect(btn).to.have.class(CLASS_ACTIVE); - }); - }); - - describe('fetchAnnotations()', () => { - beforeEach(() => { - annotator.annotationService = { - getThreadMap: () => {} - }; - stubs.serviceMock = sandbox.mock(annotator.annotationService); - - const threadMap = { - someID: [{}, {}], - someID2: [{}] - }; - stubs.threadPromise = Promise.resolve(threadMap); - sandbox.stub(annotator, 'emit'); - sandbox.stub(annotator, 'isModeAnnotatable').returns(true); - - annotator.permissions = { - canViewAllAnnotations: true, - canViewOwnAnnotations: true - }; - }); - - it('should not fetch existing annotations if the user does not have correct permissions', () => { - stubs.serviceMock.expects('getThreadMap').never(); - annotator.permissions = { - canViewAllAnnotations: false, - canViewOwnAnnotations: false - }; - const result = annotator.fetchAnnotations(); - expect(result instanceof Promise).to.be.truthy; - }); - - it('should fetch existing annotations if the user can view all annotations', () => { - stubs.serviceMock.expects('getThreadMap').returns(Promise.resolve()); - annotator.permissions = { - canViewAllAnnotations: false, - canViewOwnAnnotations: true - }; - const result = annotator.fetchAnnotations(); - expect(result instanceof Promise).to.be.truthy; - }); - - it('should fetch existing annotations if the user can view all annotations', () => { - stubs.serviceMock.expects('getThreadMap').returns(Promise.resolve()); - annotator.permissions = { - canViewAllAnnotations: true, - canViewOwnAnnotations: false - }; - const result = annotator.fetchAnnotations(); - expect(result instanceof Promise).to.be.truthy; - }); - - it('should reset and create a new thread map by fetching annotation data from the server', () => { - stubs.serviceMock.expects('getThreadMap').returns(stubs.threadPromise); - 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(annotator.threads).to.not.be.undefined; - expect(annotator.createAnnotationThread).to.be.calledTwice; - expect(annotator.bindCustomListenersOnThread).to.be.calledTwice; - expect(result).to.be.an.object; - }); - }); - - it('should emit a message to indicate that all annotations have been fetched', () => { - stubs.serviceMock.expects('getThreadMap').returns(stubs.threadPromise); - annotator.fetchAnnotations(); - return stubs.threadPromise.then(() => { - expect(annotator.emit).to.be.calledWith(ANNOTATOR_EVENT.fetch); - }); - }); - }); - - 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 - }); - const addListenerStub = sandbox.stub(annotator.annotationService, 'addListener'); - - annotator.bindCustomListenersOnService(); - expect(addListenerStub).to.be.calledWith(ANNOTATOR_EVENT.error, sinon.match.func); - }); - }); - - describe('handleServiceEvents()', () => { - beforeEach(() => { - sandbox.stub(annotator, 'emit'); - }); - - it('should emit annotatorerror on read error event', () => { - annotator.handleServiceEvents({ reason: 'read' }); - expect(annotator.emit).to.be.calledWith(ANNOTATOR_EVENT.error, sinon.match.string); - }); - - it('should emit annotatorerror and show annotations on create error event', () => { - annotator.handleServiceEvents({ reason: 'create' }); - expect(annotator.emit).to.be.calledWith(ANNOTATOR_EVENT.error, sinon.match.string); - expect(annotator.showAnnotations).to.be.called; - }); - - it('should emit annotatorerror and show annotations on delete error event', () => { - annotator.handleServiceEvents({ reason: 'delete' }); - expect(annotator.emit).to.be.calledWith(ANNOTATOR_EVENT.error, sinon.match.string); - expect(annotator.showAnnotations).to.be.called; - }); - - it('should emit annotatorerror on authorization error event', () => { - annotator.handleServiceEvents({ reason: 'authorization' }); - expect(annotator.emit).to.be.calledWith(ANNOTATOR_EVENT.error, sinon.match.string); - }); - - it('should not emit annotatorerror when event does not match', () => { - annotator.handleServiceEvents({ reason: 'no match' }); - expect(annotator.emit).to.not.be.called; - }); - }); - - 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; - }); - - 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, 'removeListener'); - - annotator.unbindCustomListenersOnService(); - expect(removeListenerStub).to.be.called; - }); - }); - - describe('bindCustomListenersOnThread()', () => { - it('should bind custom listeners on the thread', () => { - stubs.threadMock.expects('addListener').withArgs('threadevent', sinon.match.func); - annotator.bindCustomListenersOnThread(stubs.thread); - }); - - it('should do nothing when given thread is empty', () => { - stubs.threadMock.expects('addListener').never(); - annotator.bindCustomListenersOnThread(null); - }) - }); - - describe('unbindCustomListenersOnThread()', () => { - it('should unbind custom listeners from the thread', () => { - stubs.threadMock.expects('removeListener').withArgs('threadevent'); - annotator.unbindCustomListenersOnThread(stubs.thread); - }); - }); - - describe('bindModeListeners()', () => { - let drawingThread; - - beforeEach(() => { - annotator.annotatedElement = { - addEventListener: sandbox.stub(), - removeEventListener: sandbox.stub() - }; - - stubs.controllers = { - [TYPES.draw]: { - bindModeListeners: sandbox.stub() - } - }; - - annotator.modeControllers = stubs.controllers; - drawingThread = { - handleStart: () => {}, - handleStop: () => {}, - handleMove: () => {}, - addListener: sandbox.stub() - }; - }); - - it('should get event handlers for point annotation mode', () => { - annotator.bindModeListeners(TYPES.point); - expect(annotator.annotatedElement.addEventListener).to.be.calledWith( - 'mousedown', - annotator.pointClickHandler - ); - expect(annotator.annotatedElement.addEventListener).to.be.calledWith( - 'touchstart', - annotator.pointClickHandler - ); - expect(annotator.annotationModeHandlers.length).equals(2); - }); - - it('should bind draw mode click handlers if post button exists', () => { - annotator.bindModeListeners(TYPES.draw); - - expect(annotator.annotatedElement.addEventListener).to.not.be.called; - expect(stubs.controllers[TYPES.draw].bindModeListeners).to.be.called; - }); - }); - - describe('unbindModeListeners()', () => { - it('should unbind mode handlers', () => { - sandbox.stub(annotator.annotatedElement, 'removeEventListener'); - annotator.annotationModeHandlers = [ - { - type: 'event1', - func: () => {}, - eventObj: annotator.annotatedElement - }, - { - type: 'event2', - func: () => {}, - eventObj: annotator.annotatedElement - } - ]; - - annotator.unbindModeListeners(); - expect(annotator.annotatedElement.removeEventListener).to.be.calledWith( - 'event1', - sinon.match.func - ); - expect(annotator.annotatedElement.removeEventListener).to.be.calledWith( - 'event2', - sinon.match.func - ); - }); - - it('should delegate to the controller', () => { - annotator.modeControllers = { - [TYPES.draw]: { - name: 'drawingModeController', - unbindModeListeners: sandbox.stub() - } - }; - - annotator.unbindModeListeners(TYPES.draw); - expect(annotator.modeControllers[TYPES.draw].unbindModeListeners).to.be.called; - }); - }); - - describe('pointClickHandler()', () => { - const event = { - stopPropagation: () => {}, - preventDefault: () => {} - }; - - 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, 'disableAnnotationMode'); - stubs.emit = sandbox.stub(annotator, 'emit'); - annotator.modeButtons = { - point: { - title: 'Point Annotation Mode', - selector: '.bp-btn-annotate' - } - }; - }); - - afterEach(() => { - annotator.modeButtons = {}; - annotator.container = document; - }); - - 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); - - expect(annotator.getLocationFromEvent).to.not.be.called; - expect(annotator.bindCustomListenersOnThread).to.not.be.called; - expect(annotator.disableAnnotationMode).to.not.be.called; - }); - - it('should not do anything if thread is invalid', () => { - stubs.destroy.returns(false); - - stubs.threadMock.expects('show').never(); - annotator.pointClickHandler(event); - - expect(annotator.getLocationFromEvent).to.be.called; - expect(annotator.disableAnnotationMode).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); - - stubs.threadMock.expects('show').never(); - annotator.pointClickHandler(event); - - expect(annotator.getLocationFromEvent).to.be.called; - expect(annotator.bindCustomListenersOnThread).to.not.be.called; - expect(annotator.disableAnnotationMode).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); - stubs.threadMock.expects('getThreadEventData').returns('data'); - - stubs.threadMock.expects('show'); - annotator.pointClickHandler(event); - - expect(annotator.getLocationFromEvent).to.be.called; - expect(annotator.bindCustomListenersOnThread).to.be.called; - expect(annotator.disableAnnotationMode).to.be.called; - expect(annotator.emit).to.be.calledWith(THREAD_EVENT.pending, 'data'); - }); - }); - - describe('addThreadToMap()', () => { - it('should add valid threads to the thread map', () => { - stubs.thread.location = { page: 1 }; - stubs.thread2.location = { page: 1 }; - - const threadMap = { '456def': stubs.thread2 }; - annotator.threads = { 1: threadMap }; - annotator.addThreadToMap(stubs.thread); - - const pageThreads = annotator.getThreadsOnPage(1); - expect(pageThreads).to.have.any.keys(stubs.thread.threadID); - }); - }); - - describe('removeThreadFromMap()', () => { - it('should remove a valid thread from the thread map', () => { - stubs.thread.location = { page: 1 }; - stubs.thread2.location = { page: 1 }; - annotator.addThreadToMap(stubs.thread); - annotator.addThreadToMap(stubs.thread2); - - annotator.removeThreadFromMap(stubs.thread); - const pageThreads = annotator.getThreadsOnPage(1); - expect(pageThreads).to.not.have.any.keys(stubs.thread.threadID); - expect(pageThreads).to.have.any.keys(stubs.thread2.threadID); - }); - }); - - describe('isInAnnotationMode()', () => { - it('should return whether the annotator is in specified annotation mode or not', () => { - annotator.currentAnnotationMode = TYPES.draw; - expect(annotator.isInAnnotationMode(TYPES.draw)).to.be.true; - - annotator.currentAnnotationMode = TYPES.point; - expect(annotator.isInAnnotationMode(TYPES.draw)).to.be.false; - }); - }); - - describe('scrollToAnnotation()', () => { - beforeEach(() => { - stubs.thread.location = { page: 1 }; - annotator.addThreadToMap(stubs.thread); - }); - - it('should do nothing if no threadID is provided', () => { - stubs.threadMock.expects('scrollIntoView').never(); - annotator.scrollToAnnotation(); - }); - - it('should do nothing if threadID does not exist on page', () => { - stubs.threadMock.expects('scrollIntoView').never(); - annotator.scrollToAnnotation('wrong'); - }); - - it('should scroll to annotation if threadID exists on page', () => { - stubs.threadMock.expects('scrollIntoView'); - annotator.scrollToAnnotation(stubs.thread.threadID); - }); - }); - - describe('scaleAnnotations()', () => { - it('should set scale and rotate annotations based on the annotated element', () => { - sandbox.stub(annotator, 'setScale'); - sandbox.stub(annotator, 'rotateAnnotations'); - - const data = { - scale: 5, - rotationAngle: 90, - pageNum: 2 - }; - annotator.scaleAnnotations(data); - expect(annotator.setScale).to.be.calledWith(data.scale); - expect(annotator.rotateAnnotations).to.be.calledWith(data.rotationAngle, data.pageNum); - }); - }); - - describe('getThreadsOnPage()', () => { - it('should add page to threadMap if it does not already exist', () => { - annotator.threads = { - 1: 'not empty' - }; - const threads = annotator.getThreadsOnPage(2); - expect(threads).to.not.be.undefined; - annotator.threads = {}; - }); - - it('should return an existing page in the threadMap', () => { - annotator.threads = { - 1: 'not empty' - }; - const threads = annotator.getThreadsOnPage(1); - expect(threads).equals('not empty'); - annotator.threads = {}; - }); - }); - - describe('getThreadByID()', () => { - it('should find and return annotation thread specified by threadID', () => { - annotator.threads = { 1: {} }; - sandbox.stub(annotator, 'getThreadsOnPage').returns({ - '123abc': stubs.thread - }); - const thread = annotator.getThreadByID(stubs.thread.threadID); - expect(thread).to.deep.equals(stubs.thread); - }); - - it('should return null if specified annotation thread is invalid', () => { - annotator.threads = { 1: {} }; - sandbox.stub(annotator, 'getThreadsOnPage').returns({ - '123abc': stubs.thread - }); - const thread = annotator.getThreadByID('random'); - expect(thread).to.deep.equals(null); - }); - }); - - describe('destroyPendingThreads()', () => { - beforeEach(() => { - stubs.thread = { - threadID: '123abc', - location: { page: 2 }, - type: 'type', - state: STATES.pending, - destroy: () => {}, - unbindCustomListenersOnThread: () => {}, - removeAllListeners: () => {} - }; - stubs.threadMock = sandbox.mock(stubs.thread); - stubs.isPending = sandbox.stub(annotatorUtil, 'isPending').returns(false); - stubs.isPending.withArgs(STATES.pending).returns(true); - - annotator.addThreadToMap(stubs.thread); - annotator.init(); - }); - - it('should destroy and return true if there are any pending threads', () => { - 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.threads = {}; - stubs.threadMock.expects('destroy').never(); - stubs.isPending.returns(false); - const destroyed = annotator.destroyPendingThreads(); - expect(destroyed).to.equal(false); - }); - - it('should not destroy and return false if the threads are not pending', () => { - stubs.thread.state = 'NOT_PENDING'; - stubs.threadMock.expects('destroy').never(); - const destroyed = annotator.destroyPendingThreads(); - expect(destroyed).to.equal(false); - }); - - it('should destroy only pending threads, and return true', () => { - stubs.thread.state = 'NOT_PENDING'; - const pendingThread = { - threadID: '456def', - location: { page: 1 }, - type: 'type', - state: STATES.pending, - destroy: () => {}, - unbindCustomListenersOnThread: () => {}, - removeAllListeners: () => {} - }; - stubs.pendingMock = sandbox.mock(pendingThread); - 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 annotatorerror was already emitted', () => { - sandbox.stub(annotator, 'emit'); - annotator.validationErrorEmitted = true; - annotator.handleValidationError(); - expect(annotator.emit).to.not.be.calledWith(ANNOTATOR_EVENT.error); - expect(annotator.validationErrorEmitted).to.be.true; - }); - - it('should emit annotatorerror on first error', () => { - sandbox.stub(annotator, 'emit'); - annotator.validationErrorEmitted = false; - annotator.handleValidationError(); - expect(annotator.emit).to.be.calledWith(ANNOTATOR_EVENT.error, sinon.match.string); - expect(annotator.validationErrorEmitted).to.be.true; - }); - }); - - describe('handleAnnotationThreadEvents()', () => { - beforeEach(() => { - stubs.getThread = sandbox.stub(annotator, 'getThreadByID'); - stubs.emit = sandbox.stub(annotator, 'emit'); - stubs.unbind = sandbox.stub(annotator, 'unbindCustomListenersOnThread'); - stubs.remove = sandbox.stub(annotator, 'removeThreadFromMap'); - }); - - it('should do nothing if invalid params are specified', () => { - annotator.handleAnnotationThreadEvents('no data'); - annotator.handleAnnotationThreadEvents({ data: 'no threadID'}); - expect(stubs.getThread).to.not.be.called; - - annotator.handleAnnotationThreadEvents({ data: { threadID: 1 }}); - expect(stubs.emit).to.not.be.called; - expect(stubs.unbind).to.not.be.called; - expect(stubs.remove).to.not.be.called; - }); - - it('should unbind custom thread listeners on threadCleanup', () => { - stubs.getThread.returns(stubs.thread); - annotator.handleAnnotationThreadEvents({ - event: THREAD_EVENT.threadCleanup, - data: { threadID: 1 } - }); - expect(stubs.unbind).to.be.called; - expect(stubs.emit).to.not.be.called; - expect(stubs.remove).to.not.be.called; - }); - - it('should remove thread from map on threadDelete', () => { - stubs.getThread.returns(stubs.thread); - const data = { - event: THREAD_EVENT.threadDelete, - data: { threadID: 1 } - }; - annotator.handleAnnotationThreadEvents(data); - expect(stubs.emit).to.be.calledWith(data.event, data.data); - expect(stubs.remove).to.be.called; - expect(stubs.unbind).to.not.be.called; - }); - - it('should emit delete error notification event', () => { - stubs.getThread.returns(stubs.thread); - const data = { - event: THREAD_EVENT.deleteError, - data: { threadID: 1 } - }; - annotator.handleAnnotationThreadEvents(data); - expect(stubs.emit).to.be.calledWith(data.event, data.data); - expect(stubs.emit).to.be.calledWith(ANNOTATOR_EVENT.error, sinon.match.string); - expect(stubs.unbind).to.not.be.called; - expect(stubs.remove).to.not.be.called; - }); - - it('should emit save error notification event', () => { - stubs.getThread.returns(stubs.thread); - const data = { - event: THREAD_EVENT.createError, - data: { threadID: 1 } - }; - annotator.handleAnnotationThreadEvents(data); - expect(stubs.emit).to.be.calledWith(data.event, data.data); - expect(stubs.emit).to.be.calledWith(ANNOTATOR_EVENT.error, sinon.match.string); - expect(stubs.unbind).to.not.be.called; - expect(stubs.remove).to.not.be.called; - }); - - it('should emit thread event', () => { - stubs.getThread.returns(stubs.thread); - const data = { - event: THREAD_EVENT.pending, - data: { threadID: 1 } - }; - annotator.handleAnnotationThreadEvents(data); - expect(stubs.emit).to.be.calledWith(data.event, data.data); - expect(stubs.unbind).to.not.be.called; - expect(stubs.remove).to.not.be.called; - }); - }); - - describe('emit()', () => { - const emitFunc = EventEmitter.prototype.emit; - - afterEach(() => { - Object.defineProperty(EventEmitter.prototype, 'emit', { value: emitFunc }); - }); - - it('should pass through the event as well as broadcast it as a annotator event', () => { - const fileId = '1'; - const fileVersionId = '1'; - const event = 'someEvent'; - const data = {}; - const annotatorName = 'name'; - - annotator = new Annotator({ - canAnnotate: true, - container: document, - annotationService: {}, - isMobile: false, - annotator: { NAME: annotatorName }, - file: { - id: fileId, - file_version: { - id: fileVersionId - } - }, - location: { locale: 'en-US' }, - modeButtons: {} - }); - - const emitStub = sandbox.stub(); - Object.defineProperty(EventEmitter.prototype, 'emit', { value: emitStub }); - - annotator.emit(event, data); - - expect(emitStub).to.be.calledWith(event, data); - expect(emitStub).to.be.calledWithMatch('annotatorevent', { - event, - data, - annotatorName, - fileId, - fileVersionId - }); - }); - }); - - describe('isModeAnnotatable()', () => { - beforeEach(() => { - annotator.options.annotator = { - TYPE: [TYPES.point, 'highlight'] - }; - }); - - it('should return false if annotations are not allowed on the current viewer', () => { - annotator.options.annotator = undefined; - expect(annotator.isModeAnnotatable(TYPES.point)).to.equal(false); - }) - - it('should return true if the type is supported by the viewer', () => { - expect(annotator.isModeAnnotatable(TYPES.point)).to.equal(true); - }); - - it('should return false if the type is not supported by the viewer', () => { - expect(annotator.isModeAnnotatable('drawing')).to.equal(false); - }); - }); - - describe('showModeAnnotateButton()', () => { - beforeEach(() => { - annotator.modeButtons = { - point: { - title: 'Point Annotation Mode', - selector: '.bp-btn-annotate' - } - }; - annotator.permissions.canAnnotate = true; - }); - - afterEach(() => { - annotator.modeButtons = {}; - annotator.container = document; - }); - - it('should do nothing if user cannot annotate', () => { - annotator.permissions.canAnnotate = false; - sandbox.stub(annotator, 'isModeAnnotatable').returns(true); - sandbox.stub(annotator, 'getAnnotationModeClickHandler'); - annotator.showModeAnnotateButton(TYPES.point); - expect(annotator.getAnnotationModeClickHandler).to.not.be.called; - }); - - it('should do nothing if the mode does not require a button', () => { - sandbox.stub(annotator, 'getAnnotationModeClickHandler'); - sandbox.stub(annotator, 'isModeAnnotatable').returns(true); - annotator.showModeAnnotateButton(TYPES.highlight); - expect(annotator.getAnnotationModeClickHandler).to.not.be.called; - }); - - it('should do nothing if the annotation type is not supported ', () => { - sandbox.stub(annotator, 'getAnnotationModeClickHandler'); - sandbox.stub(annotator, 'isModeAnnotatable').returns(false); - annotator.showModeAnnotateButton('bleh'); - expect(annotator.getAnnotationModeClickHandler).to.not.be.called; - }); - - it('should do nothing if the button is not in the container', () => { - annotator.modeButtons = { - point: { - title: 'Point Annotation Mode', - selector: 'wrong-selector' - } - }; - sandbox.stub(annotator, 'isModeAnnotatable').returns(true); - sandbox.stub(annotator, 'getAnnotationModeClickHandler'); - annotator.showModeAnnotateButton(TYPES.point); - expect(annotator.getAnnotationModeClickHandler).to.not.be.called; - }); - - it('should set up and show an annotate button', () => { - const buttonEl = annotator.container.querySelector('.bp-btn-annotate'); - buttonEl.classList.add('point-selector'); - buttonEl.classList.add(CLASS_HIDDEN); - - sandbox.stub(annotator, 'isModeAnnotatable').returns(true); - sandbox.stub(annotator, 'getAnnotationModeClickHandler'); - sandbox.mock(buttonEl).expects('addEventListener').withArgs('click'); - - annotator.showModeAnnotateButton(TYPES.point); - expect(buttonEl.title).to.equal('Point Annotation Mode'); - expect(annotator.getAnnotationModeClickHandler).to.be.called; - }); - }); - - describe('getAnnotateButton()', () => { - it('should return the annotate button', () => { - const selector = 'bp-btn-annotate'; - const buttonEl = annotator.getAnnotateButton(`.${selector}`); - expect(buttonEl).to.have.class(selector); - }); - }); - - describe('getAnnotationModeClickHandler()', () => { - beforeEach(() => { - stubs.isModeAnnotatable = sandbox.stub(annotator, 'isModeAnnotatable').returns(false); - }); - - it('should return null if you cannot annotate', () => { - const handler = annotator.getAnnotationModeClickHandler(TYPES.point); - expect(stubs.isModeAnnotatable).to.be.called; - expect(handler).to.equal(null); - }); - - it('should return the toggle point mode handler', () => { - stubs.isModeAnnotatable.returns(true); - stubs.toggle = sandbox.stub(annotator, 'toggleAnnotationHandler'); - - const handler = annotator.getAnnotationModeClickHandler(TYPES.point); - expect(stubs.isModeAnnotatable).to.be.called; - expect(handler).to.be.a('function'); - - handler(event); - expect(stubs.toggle).to.be.calledWith(TYPES.point); - }); - }); - }); -}); diff --git a/src/lib/annotations/__tests__/BoxAnnotations-test.js b/src/lib/annotations/__tests__/BoxAnnotations-test.js deleted file mode 100644 index c9187a108..000000000 --- a/src/lib/annotations/__tests__/BoxAnnotations-test.js +++ /dev/null @@ -1,237 +0,0 @@ -/* eslint-disable no-unused-expressions */ -import BoxAnnotations from '../BoxAnnotations'; -import { TYPES } from '../annotationConstants'; -import * as annotatorUtil from '../annotatorUtil'; -import DrawingModeController from '../drawing/DrawingModeController'; - -let loader; -let stubs; -const sandbox = sinon.sandbox.create(); - -describe('lib/annotators/BoxAnnotations', () => { - beforeEach(() => { - stubs = {}; - loader = new BoxAnnotations(); - }); - - afterEach(() => { - sandbox.verifyAndRestore(); - - if (typeof loader.destroy === 'function') { - loader.destroy(); - } - - loader = null; - stubs = 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('getAnnotatorsForViewer()', () => { - beforeEach(() => { - stubs.instantiateControllers = sandbox.stub(loader, 'instantiateControllers'); - }); - it('should return undefined if the annotator does not exist', () => { - const annotator = loader.getAnnotatorsForViewer('not_supported_type'); - expect(annotator).to.be.undefined; - expect(stubs.instantiateControllers).to.be.called; - }); - - it('should return the correct annotator for the viewer name', () => { - const name = 'Document'; - const annotator = loader.getAnnotatorsForViewer(name); - expect(annotator.NAME).to.equal(name); // First entry is Document annotator - expect(stubs.instantiateControllers).to.be.called; - }); - - it('should return nothing if the viewer requested is disabled', () => { - const annotator = loader.getAnnotatorsForViewer('Document', ['Document']); - expect(annotator).to.be.undefined; - expect(stubs.instantiateControllers).to.be.called; - }); - }); - - describe('determineAnnotator()', () => { - beforeEach(() => { - stubs.instantiateControllers = sandbox.stub(loader, 'instantiateControllers'); - stubs.canLoad = sandbox.stub(annotatorUtil, 'canLoadAnnotations').returns(true); - - stubs.annotator = { - NAME: 'Document', - VIEWER: ['Document'], - TYPE: ['point'], - DEFAULT_TYPES: ['point'] - }; - - stubs.options = { - file: { - permissions: {} - }, - viewer: { - NAME: 'Document' - } - } - }); - - it('should not return an annotator if the user has incorrect permissions/scopes', () => { - sandbox.stub(loader, 'getAnnotatorsForViewer').returns(stubs.annotator); - stubs.canLoad.returns(false); - expect(loader.determineAnnotator(stubs.options)).to.be.null; - }); - - it('should choose the first annotator that matches the viewer', () => { - const viewer = 'Document'; - sandbox.stub(loader, 'getAnnotatorsForViewer').returns(stubs.annotator); - const annotator = loader.determineAnnotator(stubs.options); - expect(annotator.NAME).to.equal(viewer); - expect(loader.getAnnotatorsForViewer).to.be.called; - }); - - it('should not return an annotator if no matching annotator is found', () => { - sandbox.stub(loader, 'getAnnotatorsForViewer').returns(undefined); - const annotator = loader.determineAnnotator(stubs.options); - expect(annotator).to.be.null; - }); - - it('should return a copy of the annotator that matches', () => { - const viewer = 'Document'; - - const docAnnotator = { - NAME: viewer, - VIEWER: ['Document'], - TYPE: ['point', 'highlight'], - DEFAULT_TYPES: ['point'] - }; - - sandbox.stub(loader, 'getAnnotatorsForViewer').returns(docAnnotator); - const annotator = loader.determineAnnotator(stubs.options); - - stubs.annotator.NAME = 'another_name'; - expect(annotator.NAME).to.not.equal(stubs.annotator.NAME); - }); - - it('should return null if the config for the viewer disables annotations', () => { - const config = { - enabled: false - }; - sandbox.stub(loader, 'getAnnotatorsForViewer').returns(stubs.annotator); - const annotator = loader.determineAnnotator(stubs.options, config); - expect(annotator).to.be.null; - }); - - it('should filter disabled annotation types from the annotator.TYPE', () => { - const config = { - enabled: true, - disabledTypes: ['point'] - }; - - const docAnnotator = { - NAME: 'Document', - VIEWER: ['Document'], - TYPE: ['point', 'highlight'], - DEFAULT_TYPES: ['point', 'highlight'] - }; - sandbox.stub(loader, 'getAnnotatorsForViewer').returns(docAnnotator); - const annotator = loader.determineAnnotator(stubs.options, config); - - expect(annotator.TYPE.includes('point')).to.be.false; - expect(annotator.TYPE.includes('highlight')).to.be.true; - expect(annotator).to.deep.equal({ - NAME: 'Document', - VIEWER: ['Document'], - TYPE: ['highlight'], - DEFAULT_TYPES: ['point', 'highlight'] - }); - expect(loader.getAnnotatorsForViewer).to.be.called; - }); - - it('should filter and only keep allowed types of annotations', () => { - const config = { - enabled: true, - enabledTypes: ['point', 'timestamp'] - }; - - const docAnnotator = { - NAME: 'Document', - VIEWER: ['Document'], - TYPE: ['point', 'highlight', 'highlight-comment', 'draw'], - DEFAULT_TYPES: ['point', 'highlight'] - }; - - sandbox.stub(loader, 'getAnnotatorsForViewer').returns(docAnnotator); - const annotator = loader.determineAnnotator(stubs.options, config); - expect(annotator.TYPE.includes('point')).to.be.true; - expect(annotator.TYPE.includes('highlight')).to.be.false; - expect(annotator).to.deep.equal({ - NAME: 'Document', - VIEWER: ['Document'], - TYPE: ['point'], - DEFAULT_TYPES: ['point', 'highlight'] - }); - }); - - it('should respect default annotators if none provided', () => { - const config = { - enabled: true - }; - - const docAnnotator = { - NAME: 'Document', - VIEWER: ['Document'], - TYPE: ['point', 'highlight', 'highlight-comment', 'draw'], - DEFAULT_TYPES: ['point', 'draw'] - }; - sandbox.stub(loader, 'getAnnotatorsForViewer').returns(docAnnotator); - const annotator = loader.determineAnnotator(stubs.options, config); - - expect(annotator.TYPE).to.deep.equal(['point', 'draw']); - }); - }); - - describe('instantiateControllers()', () => { - it('Should do nothing when a controller exists', () => { - const config = { - CONTROLLERS: { - [TYPES.draw]: { - CONSTRUCTOR: sandbox.stub() - } - } - }; - - expect(() => loader.instantiateControllers(config)).to.not.throw(); - }); - - it('Should do nothing when given an undefined object', () => { - const config = undefined; - expect(() => loader.instantiateControllers(config)).to.not.throw(); - }); - - it('Should do nothing when config has no types', () => { - const config = { - TYPE: undefined - }; - expect(() => loader.instantiateControllers(config)).to.not.throw(); - }); - - it('Should instantiate controllers and assign them to the CONTROLLERS attribute', () => { - const config = { - TYPE: [TYPES.draw, 'typeWithoutController'] - }; - - loader.instantiateControllers(config); - expect(config.CONTROLLERS).to.not.equal(undefined); - expect(config.CONTROLLERS[TYPES.draw] instanceof DrawingModeController).to.be.truthy; - const assignedControllers = Object.keys(config.CONTROLLERS); - expect(assignedControllers.length).to.equal(1); - }); - }); -}); diff --git a/src/lib/annotations/__tests__/CommentBox-test.js b/src/lib/annotations/__tests__/CommentBox-test.js deleted file mode 100644 index b3fb1b348..000000000 --- a/src/lib/annotations/__tests__/CommentBox-test.js +++ /dev/null @@ -1,275 +0,0 @@ -/* eslint-disable no-unused-expressions */ -import CommentBox from '../CommentBox'; -import { - CLASS_HIDDEN, - SELECTOR_ANNOTATION_BUTTON_CANCEL, - SELECTOR_ANNOTATION_BUTTON_POST -} from '../annotationConstants'; - -describe('lib/annotations/CommentBox', () => { - const sandbox = sinon.sandbox.create(); - let commentBox; - let parentEl; - - beforeEach(() => { - parentEl = document.createElement('span'); - commentBox = new CommentBox(parentEl, { - localized: { - cancelButton: 'cancel' - } - }); - }); - - afterEach(() => { - commentBox.destroy(); - commentBox = null; - parentEl = null; - }); - - describe('constructor()', () => { - let tempCommentBox; - const localized = { - cancelButton: 'cancel', - postButton: 'post', - addCommentPlaceholder: 'placeholder' - }; - - beforeEach(() => { - tempCommentBox = new CommentBox(parentEl, { localized }); - }); - - afterEach(() => { - tempCommentBox.destroy(); - tempCommentBox = null; - }); - - it('should assign the parentEl to the container passed in', () => { - expect(tempCommentBox.parentEl).to.deep.equal(parentEl); - }); - - it('should assign cancelText to the string passed in the config', () => { - expect(tempCommentBox.cancelText).to.equal(localized.cancelButton); - }); - - it('should assign postText to the string passed in the config', () => { - expect(tempCommentBox.postText).to.equal(localized.postButton); - }); - - it('should assign placeholderText to the string passed in the config', () => { - expect(tempCommentBox.placeholderText).to.equal(localized.addCommentPlaceholder); - }); - }); - - describe('focus()', () => { - beforeEach(() => { - // Kickstart creation of UI - commentBox.show(); - }); - - it('should do nothing if the textarea HTML doesn\'t exist', () => { - const focus = sandbox.stub(commentBox.textAreaEl, 'focus'); - - commentBox.textAreaEl.remove(); - commentBox.textAreaEl = null; - - commentBox.focus(); - expect(focus).to.not.be.called; - }); - - it('should focus on the text area contained by the comment box', () => { - const focus = sandbox.stub(commentBox.textAreaEl, 'focus'); - - commentBox.focus(); - expect(focus).to.be.called; - }); - }); - - describe('clear()', () => { - beforeEach(() => { - // Kickstart creation of UI - commentBox.show(); - sandbox.stub(commentBox, 'preventDefaultAndPropagation'); - }); - - it('should overwrite the text area\'s value with an empty string', () => { - commentBox.textAreaEl.value = 'yay'; - - commentBox.clear(); - expect(commentBox.textAreaEl.value).to.equal(''); - }); - }); - - describe('hide()', () => { - beforeEach(() => { - // Kickstart creation of UI - commentBox.show(); - sandbox.stub(commentBox, 'preventDefaultAndPropagation'); - }); - - it('should do nothing if the comment box HTML doesn\'t exist', () => { - const addClass = sandbox.stub(commentBox.containerEl.classList, 'add'); - commentBox.containerEl = null; - commentBox.hide(); - - expect(addClass).to.not.be.called; - }); - - it('should add the hidden class to the comment box element', () => { - commentBox.hide(); - expect(commentBox.containerEl.classList.contains(CLASS_HIDDEN)).to.be.true; - }); - }); - - describe('show()', () => { - it('should invoke createComment box, if UI has not been created', () => { - const containerEl = document.createElement('div'); - const create = sandbox.stub(commentBox, 'createCommentBox').returns(containerEl); - - commentBox.show(); - expect(create).to.be.called; - // Nullify to prevent fail during destroy - commentBox.containerEl = null; - }); - - it('should add the container element to the parent, if the UI has not been created', () => { - const append = sandbox.stub(parentEl, 'appendChild'); - - commentBox.show(); - expect(append).to.be.calledWith(commentBox.containerEl); - }); - - it('should remove the hidden class from the container', () => { - commentBox.show(); - expect(commentBox.containerEl.classList.contains(CLASS_HIDDEN)).to.be.false; - }); - }); - - describe('destroy()', () => { - it('should do nothing if it\'s UI has not been created', () => { - const unchanged = 'not even the right kind of data'; - commentBox.parentEl = unchanged; - commentBox.destroy(); - expect(commentBox.parentEl).to.equal(unchanged); - }); - - it('should remove the UI container from the parent element', () => { - commentBox.show(); - const remove = sandbox.stub(commentBox.containerEl, 'remove'); - commentBox.destroy(); - expect(remove).to.be.called; - }); - - it('should remove event listener from cancel button', () => { - commentBox.show(); - const remove = sandbox.stub(commentBox.cancelEl, 'removeEventListener'); - commentBox.destroy(); - expect(remove).to.be.called; - }); - - it('should remove event listener from post button', () => { - commentBox.show(); - const remove = sandbox.stub(commentBox.postEl, 'removeEventListener'); - commentBox.destroy(); - expect(remove).to.be.called; - }); - }); - - describe('createHTML()', () => { - let el; - beforeEach(() => { - el = commentBox.createHTML(); - }); - - it('should create and return a section element with bp-create-highlight-comment class on it', () => { - expect(el.nodeName).to.equal('SECTION'); - expect(el.classList.contains('bp-create-highlight-comment')).to.be.true; - }); - - it('should create a text area with the provided placeholder text', () => { - expect(el.querySelector('textarea')).to.exist; - }); - - it('should create a cancel button with the provided cancel text', () => { - expect(el.querySelector(SELECTOR_ANNOTATION_BUTTON_CANCEL)).to.exist; - }); - - it('should create a post button with the provided text', () => { - expect(el.querySelector(SELECTOR_ANNOTATION_BUTTON_POST)).to.exist; - }); - }); - - describe('onCancel()', () => { - beforeEach(() => { - sandbox.stub(commentBox, 'preventDefaultAndPropagation'); - }); - - it('should invoke clear()', () => { - const clear = sandbox.stub(commentBox, 'clear'); - commentBox.onCancel({ preventDefault: () => {} }); - expect(clear).to.be.called; - }); - - it('should emit a cancel event', () => { - const emit = sandbox.stub(commentBox, 'emit'); - commentBox.onCancel({ preventDefault: () => {} }); - expect(emit).to.be.calledWith(CommentBox.CommentEvents.cancel); - }); - }); - - describe('onPost()', () => { - beforeEach(() => { - sandbox.stub(commentBox, 'preventDefaultAndPropagation'); - }); - - it('should emit a post event with the value of the text box', () => { - const emit = sandbox.stub(commentBox, 'emit'); - const text = 'a comment'; - commentBox.textAreaEl = { - value: text - }; - commentBox.onPost({ preventDefault: () => {} }); - expect(emit).to.be.calledWith(CommentBox.CommentEvents.post, text); - }); - - it('should invoke clear()', () => { - const clear = sandbox.stub(commentBox, 'clear'); - commentBox.onCancel({ preventDefault: () => {} }); - expect(clear).to.be.called; - }); - }); - - describe('createCommentBox()', () => { - it('should create and return an HTML element for the UI', () => { - const el = commentBox.createCommentBox(); - expect(el.nodeName).to.exist; - }); - - it('should create a reference to the text area', () => { - commentBox.createCommentBox(); - expect(commentBox.textAreaEl).to.exist; - }); - - it('should create a reference to the cancel button', () => { - commentBox.createCommentBox(); - expect(commentBox.cancelEl).to.exist; - }); - - it('should create a reference to the post button', () => { - commentBox.createCommentBox(); - expect(commentBox.postEl).to.exist; - }); - - it('should add an event listener on the cancel and post buttons', () => { - const uiElement = { - addEventListener: sandbox.stub() - }; - const el = document.createElement('section'); - sandbox.stub(el, 'querySelector').returns(uiElement); - sandbox.stub(commentBox, 'createHTML').returns(el); - - commentBox.createCommentBox(); - expect(uiElement.addEventListener).to.be.calledWith('click', commentBox.onCancel); - expect(uiElement.addEventListener).to.be.calledWith('click', commentBox.onPost); - }); - }); -}); diff --git a/src/lib/annotations/__tests__/annotatorUtil-test.html b/src/lib/annotations/__tests__/annotatorUtil-test.html deleted file mode 100644 index bb5673bec..000000000 --- a/src/lib/annotations/__tests__/annotatorUtil-test.html +++ /dev/null @@ -1,17 +0,0 @@ -
-
-
-
-
- - - -
-
-
- -
-
-
- -
diff --git a/src/lib/annotations/__tests__/annotatorUtil-test.js b/src/lib/annotations/__tests__/annotatorUtil-test.js deleted file mode 100644 index c331d4729..000000000 --- a/src/lib/annotations/__tests__/annotatorUtil-test.js +++ /dev/null @@ -1,690 +0,0 @@ -/* eslint-disable no-unused-expressions */ -import { - findClosestElWithClass, - findClosestDataType, - getPageInfo, - showElement, - hideElement, - enableElement, - disableElement, - showInvisibleElement, - hideElementVisibility, - resetTextarea, - isElementInViewport, - getAvatarHtml, - getScale, - isPlainHighlight, - isHighlightAnnotation, - getDimensionScale, - htmlEscape, - repositionCaret, - isPending, - validateThreadParams, - eventToLocationHandler, - decodeKeydown, - getHeaders, - replacePlaceholders, - createLocation, - round, - prevDefAndStopProp, - canLoadAnnotations, - insertTemplate, - generateBtn -} from '../annotatorUtil'; -import { - STATES, - TYPES, - SELECTOR_ANNOTATION_DIALOG, - SELECTOR_ANNOTATION_CARET -} from '../annotationConstants'; - -const DIALOG_WIDTH = 81; - -const sandbox = sinon.sandbox.create(); -let stubs = {}; - -describe('lib/annotations/annotatorUtil', () => { - let childEl; - let parentEl; - - before(() => { - fixture.setBase('src/lib'); - }); - - beforeEach(() => { - fixture.load('annotations/__tests__/annotatorUtil-test.html'); - - childEl = document.querySelector('.child'); - parentEl = document.querySelector('.parent'); - }); - - afterEach(() => { - sandbox.verifyAndRestore(); - fixture.cleanup(); - }); - - describe('findClosestElWithClass()', () => { - it('should return closest ancestor element with the specified class', () => { - assert.equal(findClosestElWithClass(childEl, 'parent'), parentEl); - }); - - it('should return null if no matching ancestor is found', () => { - assert.equal(findClosestElWithClass(childEl, 'otherParent'), null); - }); - }); - - describe('findClosestDataType()', () => { - it('should return the data type of the closest ancestor with a data type when no attributeName is provided', () => { - assert.equal(findClosestDataType(childEl), 'someType'); - }); - - it('should return the attribute name of the closest ancestor with the specified attributeName', () => { - assert.equal(findClosestDataType(childEl, 'data-name'), 'someName'); - }); - - it('should return empty string if no matching ancestor is found', () => { - assert.equal(findClosestDataType(childEl, 'data-foo'), ''); - }); - }); - - describe('getPageInfo()', () => { - it('should return page element and page number that the specified element is on', () => { - const fooEl = document.querySelector('.foo'); - const pageEl = document.querySelector('.page'); - const result = getPageInfo(fooEl); - assert.equal(result.pageEl, pageEl, 'Page element should be equal'); - assert.equal(result.page, 2, 'Page number should be equal'); - }); - - it('should return no page element and -1 page number if no page is found', () => { - const barEl = document.querySelector('.bar'); - const result = getPageInfo(barEl); - assert.equal(result.pageEl, null, 'Page element should be null'); - assert.equal(result.page, 1, 'Page number should be 1'); - }); - }); - - describe('showElement()', () => { - it('should remove hidden class from element with matching selector', () => { - // Hide element before testing show function - childEl.classList.add('bp-is-hidden'); - showElement('.child'); - assert.ok(!childEl.classList.contains('bp-is-hidden')); - }); - - it('should remove hidden class from provided element', () => { - // Hide element before testing show function - childEl.classList.add('bp-is-hidden'); - showElement(childEl); - assert.ok(!childEl.classList.contains('bp-is-hidden')); - }); - }); - - describe('hideElement()', () => { - it('should add hidden class to matching element', () => { - hideElement('.child'); - assert.ok(childEl.classList.contains('bp-is-hidden')); - }); - - it('should add hidden class to provided element', () => { - hideElement(childEl); - assert.ok(childEl.classList.contains('bp-is-hidden')); - }); - }); - - describe('enableElement()', () => { - it('should remove disabled class from element with matching selector', () => { - // Hide element before testing show function - childEl.classList.add('is-disabled'); - enableElement('.child'); - assert.ok(!childEl.classList.contains('is-disabled')); - }); - - it('should remove hidden class from provided element', () => { - // Hide element before testing show function - childEl.classList.add('is-disabled'); - enableElement(childEl); - assert.ok(!childEl.classList.contains('is-disabled')); - }); - }); - - describe('disableElement()', () => { - it('should add hidden class to matching element', () => { - disableElement('.child'); - assert.ok(childEl.classList.contains('is-disabled')); - }); - - it('should add hidden class to provided element', () => { - disableElement(childEl); - assert.ok(childEl.classList.contains('is-disabled')); - }); - }); - - describe('showInvisibleElement()', () => { - it('should remove invisible class from element with matching selector', () => { - // Hide element before testing show function - childEl.classList.add('bp-is-invisible'); - showInvisibleElement('.child'); - expect(childEl.classList.contains('bp-is-invisible')).to.be.false; - }); - - it('should remove invisible class from provided element', () => { - // Hide element before testing show function - childEl.classList.add('bp-is-invisible'); - showInvisibleElement(childEl); - expect(childEl.classList.contains('bp-is-invisible')).to.be.false; - }); - }); - - describe('hideElementVisibility()', () => { - it('should add invisible class to matching element', () => { - hideElementVisibility('.child'); - expect(childEl.classList.contains('bp-is-invisible')).to.be.true; - }); - - it('should add invisible class to provided element', () => { - hideElementVisibility(childEl); - expect(childEl.classList.contains('bp-is-invisible')).to.be.true; - }); - }); - - describe('resetTextarea()', () => { - it('should reset text area', () => { - const textAreaEl = document.querySelector('.textarea'); - - // Fake making text area 'active' - textAreaEl.classList.add('bp-is-active'); - textAreaEl.value = 'test'; - textAreaEl.style.width = '10px'; - textAreaEl.style.height = '10px'; - - resetTextarea(textAreaEl); - - assert.ok(!textAreaEl.classList.contains('bp-is-active'), 'Should be inactive'); - assert.equal(textAreaEl.value, 'test', 'Value should NOT be reset'); - assert.equal(textAreaEl.style.width, '', 'Width should be reset'); - assert.equal(textAreaEl.style.height, '', 'Height should be reset'); - }); - - it('should reset text area', () => { - const textAreaEl = document.querySelector('.textarea'); - - // Fake making text area 'active' - textAreaEl.classList.add('bp-is-active'); - textAreaEl.value = 'test'; - textAreaEl.style.width = '10px'; - textAreaEl.style.height = '10px'; - - resetTextarea(textAreaEl, true); - - assert.ok(!textAreaEl.classList.contains('bp-is-active'), 'Should be inactive'); - assert.equal(textAreaEl.value, '', 'Value should be reset'); - assert.equal(textAreaEl.style.width, '', 'Width should be reset'); - assert.equal(textAreaEl.style.height, '', 'Height should be reset'); - }); - }); - - describe('insertTemplate()', () => { - it('should insert template into node', () => { - const node = document.createElement('div'); - document.querySelector('.container').appendChild(node); - - insertTemplate(node, '
'); - assert.equal(node.firstElementChild.className, 'foo'); - }); - }); - - describe('generateBtn()', () => { - it('should return button node from specified details', () => { - const btn = generateBtn('class', 'title', document.createElement('div'), 'type'); - expect(btn).to.have.class('bp-btn-plain'); - expect(btn).to.have.class('class'); - expect(btn).to.have.attribute('data-type', 'type'); - expect(btn).to.have.attribute('title', 'title'); - expect(btn).to.contain.html(document.createElement('div')); - }); - }); - - describe('isElementInViewport()', () => { - it('should return true for an element fully in the viewport', () => { - assert.ok(isElementInViewport(childEl)); - }); - - it('should return false for an element not fully in the viewport', () => { - // Fake child element not being in viewport - childEl.style.position = 'absolute'; - childEl.style.left = '-10px'; - assert.ok(!isElementInViewport(childEl)); - }); - }); - - describe('getAvatarHtml()', () => { - it('should return avatar HTML with img if avatarUrl is provided', () => { - const expectedHtml = 'Avatar'; - assert.equal(getAvatarHtml('https://example.com', '1', 'Some Name', 'Avatar'), expectedHtml); - }); - - it('should return avatar HTML initials if no avatarUrl is provided', () => { - const expectedHtml = '
SN
'.trim(); - assert.equal(getAvatarHtml('', '1', 'Some Name'), expectedHtml); - }); - }); - - describe('getScale()', () => { - it('should return the zoom scale stored in the data-zoom attribute for the element', () => { - childEl.setAttribute('data-scale', 3); - assert.equal(getScale(childEl), 3); - }); - - it('should return a zoom scale of 1 if no stored zoom is found on the element', () => { - assert.equal(getScale(childEl), 1); - }); - }); - - describe('isPlainHighlight()', () => { - it('should return true if highlight annotation is a plain highlight', () => { - const annotations = [{ text: '' }]; - - expect(isPlainHighlight(annotations)).to.be.true; - }); - - it('should return false if a plain highlight annotation had comments added to it', () => { - const annotations = [{ text: '' }, { text: 'bleh' }]; - - expect(isPlainHighlight(annotations)).to.be.false; - }); - - it('should return false if highlight annotation has comments', () => { - const annotations = [{ text: 'bleh' }]; - - expect(isPlainHighlight(annotations)).to.be.false; - }); - }); - - describe('isHighlightAnnotation()', () => { - it('should return true if annotation is a plain highlight annotation', () => { - assert.ok(isHighlightAnnotation(TYPES.highlight)); - }); - - it('should return true if annotation is a highlight comment annotation', () => { - assert.ok(isHighlightAnnotation(TYPES.highlight_comment)); - }); - - it('should return false if annotation is a point annotation', () => { - assert.ok(!isHighlightAnnotation(TYPES.point)); - }); - }); - - describe('getDimensionScale()', () => { - it('should return null if no dimension scaling is needed', () => { - const dimensions = { - x: 100, - y: 100 - }; - const pageDimensions = { - width: 100, - height: 130 - }; - - const HEIGHT_PADDING = 30; - const result = getDimensionScale(dimensions, pageDimensions, 1, HEIGHT_PADDING); - expect(result).to.be.null; - }); - - it('should return dimension scaling factor if dimension scaling is needed', () => { - const dimensions = { - x: 100, - y: 100 - }; - const pageDimensions = { - width: 200, - height: 230 - }; - - const HEIGHT_PADDING = 30; - const result = getDimensionScale(dimensions, pageDimensions, 1, HEIGHT_PADDING); - expect(result.x).to.equal(2); - expect(result.y).to.equal(2); - }); - }); - - describe('htmlEscape()', () => { - it('should return HTML-escaped text', () => { - assert.equal(htmlEscape('test&file=what'), 'test&file=what', 'Should escape and symbol'); - assert.equal(htmlEscape('