diff --git a/src/AnnotationModeController.js b/src/AnnotationModeController.js deleted file mode 100644 index abaa7382f..000000000 --- a/src/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/Annotator.js b/src/Annotator.js index 9256d4605..925c53b87 100644 --- a/src/Annotator.js +++ b/src/Annotator.js @@ -5,20 +5,14 @@ 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'; @@ -54,7 +48,6 @@ class Annotator extends EventEmitter { this.validationErrorEmitted = false; this.isMobile = options.isMobile || false; this.hasTouch = options.hasTouch || false; - this.annotationModeHandlers = []; this.localized = options.localizedStrings; const { file } = this.options; @@ -68,27 +61,9 @@ class Annotator extends EventEmitter { * @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); - } + Object.keys(this.modeControllers).forEach((mode) => { + this.modeControllers[mode].destroy(); }); this.unbindDOMListeners(); @@ -130,12 +105,21 @@ class Annotator extends EventEmitter { // 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); + Object.keys(this.modeControllers).forEach((type) => { + const controller = this.modeControllers[type]; + controller.init({ + container: this.container, + annotatedElement: this.annotatedElement, + mode: type, + modeButton: this.modeButtons[type], + header: this.options.header, + permissions: this.permissions, + annotator: this + }); + + this.handleControllerEvents = this.handleControllerEvents.bind(this); + controller.addListener('annotationcontrollerevent', this.handleControllerEvents); }); this.setScale(initialScale); @@ -165,42 +149,6 @@ class Annotator extends EventEmitter { 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. * @@ -233,7 +181,7 @@ class Annotator extends EventEmitter { return; } - const pageThreads = this.getThreadsOnPage(pageNum); + const pageThreads = this.threads[pageNum] || {}; Object.keys(pageThreads).forEach((threadID) => { const thread = pageThreads[threadID]; thread.hide(); @@ -250,102 +198,6 @@ class Annotator extends EventEmitter { 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 //-------------------------------------------------------------------------- @@ -454,18 +306,8 @@ class Annotator extends EventEmitter { // 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); - } + const controller = this.modeControllers[firstAnnotation.type]; + controller.registerThread(thread); }); this.emit(ANNOTATOR_EVENT.fetch); @@ -522,161 +364,18 @@ class Annotator extends EventEmitter { 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 + * @param {string} currentMode - Current annotation mode * @return {boolean} Whether or not in the specified annotation mode */ - isInAnnotationMode(mode) { - return this.currentAnnotationMode === mode; + getCurrentAnnotationMode() { + return Object.keys(this.modeControllers).filter((mode) => { + const controller = this.modeControllers[mode]; + return controller.isModeEnabled(); + })[0]; } //-------------------------------------------------------------------------- @@ -707,7 +406,7 @@ class Annotator extends EventEmitter { return; } - const pageThreads = this.getThreadsOnPage(pageNum); + const pageThreads = this.threads[pageNum] || {}; Object.keys(pageThreads).forEach((threadID) => { const thread = pageThreads[threadID]; if (!this.isModeAnnotatable(thread.type)) { @@ -737,14 +436,14 @@ class Annotator extends EventEmitter { // Only show/hide point annotation button if user has the // appropriate permissions - if (!this.permissions.canAnnotate) { + const controller = this.modeControllers[TYPES.point]; + if (!this.permissions.canAnnotate || !controller) { return; } // Hide create annotations button if image is rotated const pointButtonSelector = this.modeButtons[TYPES.point].selector; - const pointAnnotateButton = this.getAnnotateButton(pointButtonSelector); - + const pointAnnotateButton = controller.getModeButton(pointButtonSelector); if (rotationAngle !== 0) { annotatorUtil.hideElement(pointAnnotateButton); } else { @@ -769,23 +468,6 @@ class Annotator extends EventEmitter { }; } - /** - * 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. * @@ -804,53 +486,15 @@ class Annotator extends EventEmitter { * @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] = {}; + toggleAnnotationMode(mode) { + const currentMode = this.getCurrentAnnotationMode(); + if (currentMode) { + this.modeControllers[currentMode].exit(); } - 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; + if (currentMode !== mode) { + this.modeControllers[mode].enter(); + } } /** @@ -873,30 +517,6 @@ class Annotator extends EventEmitter { }); } - /** - * 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. @@ -957,42 +577,37 @@ class Annotator extends EventEmitter { } /** - * Handles annotation thread events and emits them to the viewer + * Handle events emitted by the annotaiton service * * @private - * @param {Object} [data] - Annotation thread event data - * @param {string} [data.event] - Annotation thread event - * @param {string} [data.data] - Annotation thread event data + * @param {Object} [data] - Annotation service event data + * @param {string} [data.event] - Annotation service event + * @param {string} [data.data] - * @return {void} */ - handleAnnotationThreadEvents(data) { - if (!data.data || !data.data.threadID) { - return; - } - - const thread = this.getThreadByID(data.data.threadID); - if (!thread) { - return; - } - + handleControllerEvents(data) { + let opt = { page: 1, pageThreads: {} }; + const headerSelector = data.data ? data.data.headerSelector : ''; 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); + case 'togglemode': + this.toggleAnnotationMode(data.mode); break; - case THREAD_EVENT.threadDelete: - // Thread was deleted, remove from thread map - this.removeThreadFromMap(thread); - this.emit(data.event, data.data); + case ANNOTATOR_EVENT.modeEnter: + this.emit(data.event, { mode: data.mode, headerSelector }); + this.unbindDOMListeners(); + break; + case ANNOTATOR_EVENT.modeExit: + this.emit(data.event, { mode: data.mode, headerSelector }); + this.bindDOMListeners(); break; - case THREAD_EVENT.deleteError: - this.emit(ANNOTATOR_EVENT.error, this.localized.deleteError); + case 'registerthread': + opt = annotatorUtil.addThreadToMap(data.data, this.threads); + this.threads[opt.page] = opt.pageThreads; this.emit(data.event, data.data); break; - case THREAD_EVENT.createError: - this.emit(ANNOTATOR_EVENT.error, this.localized.createError); + case 'unregisterthread': + opt = annotatorUtil.removeThreadFromMap(data.data, this.threads); + this.threads[opt.page] = opt.pageThreads; this.emit(data.event, data.data); break; default: diff --git a/src/BoxAnnotations.js b/src/BoxAnnotations.js index a858afc9a..78eb81871 100644 --- a/src/BoxAnnotations.js +++ b/src/BoxAnnotations.js @@ -1,6 +1,8 @@ import DocAnnotator from './doc/DocAnnotator'; import ImageAnnotator from './image/ImageAnnotator'; -import DrawingModeController from './drawing/DrawingModeController'; +import DrawingModeController from './controllers/DrawingModeController'; +import PointModeController from './controllers/PointModeController'; +import HighlightModeController from './controllers/HighlightModeController'; import { TYPES } from './annotationConstants'; import { canLoadAnnotations } from './annotatorUtil'; @@ -29,6 +31,15 @@ const ANNOTATORS = [ ]; const ANNOTATOR_TYPE_CONTROLLERS = { + [TYPES.point]: { + CONSTRUCTOR: PointModeController + }, + [TYPES.highlight]: { + CONSTRUCTOR: HighlightModeController + }, + [TYPES.highlight_comment]: { + CONSTRUCTOR: HighlightModeController + }, [TYPES.draw]: { CONSTRUCTOR: DrawingModeController } @@ -85,7 +96,8 @@ class BoxAnnotations { /* eslint-disable no-param-reassign */ annotatorConfig.CONTROLLERS = {}; - annotatorConfig.TYPE.forEach((type) => { + const annotatorTypes = this.getAnnotatorTypes(annotatorConfig); + annotatorTypes.forEach((type) => { if (type in ANNOTATOR_TYPE_CONTROLLERS) { annotatorConfig.CONTROLLERS[type] = new ANNOTATOR_TYPE_CONTROLLERS[type].CONSTRUCTOR(); } @@ -93,6 +105,20 @@ class BoxAnnotations { /* eslint-enable no-param-reassign */ } + getAnnotatorTypes(annotatorConfig) { + const enabledTypes = this.viewerConfig.enabledTypes || [...annotatorConfig.DEFAULT_TYPES]; + + // Keeping disabledTypes for backwards compatibility + const disabledTypes = this.viewerConfig.disabledTypes || []; + + return enabledTypes.filter((type) => { + return ( + !disabledTypes.some((disabled) => disabled === type) && + annotatorConfig.TYPE.some((allowed) => allowed === type) + ); + }); + } + /** * Chooses an annotator based on viewer. * @@ -104,27 +130,15 @@ class BoxAnnotations { determineAnnotator(options, viewerConfig = {}, disabledAnnotators = []) { let modifiedAnnotator = null; + this.viewerConfig = viewerConfig; const hasAnnotationPermissions = canLoadAnnotations(options.file.permissions); const annotator = this.getAnnotatorsForViewer(options.viewer.NAME, disabledAnnotators); - if (!hasAnnotationPermissions || !annotator || viewerConfig.enabled === false) { + if (!hasAnnotationPermissions || !annotator || this.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; + modifiedAnnotator.TYPE = this.getAnnotatorTypes(modifiedAnnotator); return modifiedAnnotator; } diff --git a/src/annotatorUtil.js b/src/annotatorUtil.js index d0c7996df..ac1dd86af 100644 --- a/src/annotatorUtil.js +++ b/src/annotatorUtil.js @@ -717,3 +717,34 @@ export function canLoadAnnotations(permissions) { return !!canAnnotate || !!canViewAllAnnotations || !!canViewOwnAnnotations; } + +/** + * Adds thread to in-memory map. + * + * @protected + * @param {AnnotationThread} thread - Thread to add + * @param {Object} threadMap - Thread map + * @return {void} + */ +export function addThreadToMap(thread, threadMap) { + // Add thread to in-memory map + const page = thread.location.page || 1; // Defaults to page 1 if thread has no page' + const pageThreads = threadMap[page] || {}; + pageThreads[thread.threadID] = thread; + return { page, pageThreads }; +} + +/** + * Removes thread to in-memory map. + * + * @protected + * @param {AnnotationThread} thread - Thread to bind events to + * @param {Object} threadMap - Thread map + * @return {void} + */ +export function removeThreadFromMap(thread, threadMap) { + const page = thread.location.page || 1; // Defaults to page 1 if thread has no page' + const pageThreads = threadMap[page] || {}; + delete pageThreads[thread.threadID]; + return { page, pageThreads }; +} diff --git a/src/controllers/AnnotationModeController.js b/src/controllers/AnnotationModeController.js new file mode 100644 index 000000000..2f5015c59 --- /dev/null +++ b/src/controllers/AnnotationModeController.js @@ -0,0 +1,353 @@ +import EventEmitter from 'events'; +import { insertTemplate, isPending, addThreadToMap, removeThreadFromMap } from '../annotatorUtil'; +import { + CLASS_HIDDEN, + CLASS_ACTIVE, + CLASS_ANNOTATION_MODE, + ANNOTATOR_EVENT, + THREAD_EVENT +} from '../annotationConstants'; + +class AnnotationModeController extends EventEmitter { + /** @property {Object} - Object containing annotation threads */ + threads = {}; + + /** @property {Array} - The array of annotation handlers */ + handlers = []; + + /** @property {HTMLElement} - Container of the annotatedElement */ + container; + + /** @property {HTMLElement} - Annotated HTML DOM element */ + annotatedElement; + + /** @property {string} - Mode for annotation controller */ + mode; + + init(data) { + this.container = data.container; + this.annotatedElement = data.annotatedElement; + this.mode = data.mode; + this.annotator = data.annotator; + this.permissions = data.permissions; + + if (data.modeButton) { + this.modeButton = data.modeButton; + this.showModeButton(); + } + + this.handleThreadEvents = this.handleThreadEvents.bind(this); + } + + destroy() { + Object.keys(this.threads).forEach((page) => { + const pageThreads = this.threads[page] || {}; + + Object.keys(pageThreads).forEach((threadID) => { + const thread = pageThreads[threadID]; + this.unregisterThread(thread); + }); + }); + + if (this.buttonEl) { + this.buttonEl.removeEventListener('click', this.toggleMode); + } + } + + /** + * 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. + */ + getModeButton(annotatorSelector) { + return this.container.querySelector(annotatorSelector); + } + + /** + * Shows the annotate button for the specified mode + * + * @return {void} + */ + showModeButton() { + if (!this.permissions.canAnnotate) { + return; + } + + this.buttonEl = this.getModeButton(this.modeButton.selector); + if (this.buttonEl) { + this.buttonEl.title = this.modeButton.title; + this.buttonEl.classList.remove(CLASS_HIDDEN); + + this.toggleMode = this.toggleMode.bind(this); + this.buttonEl.addEventListener('click', this.toggleMode); + } + } + + /** + * Toggles annotation modes on and off. When an annotation mode is + * on, annotation threads will be created at that location. + * + * @return {void} + */ + toggleMode() { + this.destroyPendingThreads(); + + // No specific mode available for annotation type + if (!this.modeButton) { + return; + } + + // Exit any other annotation mode + this.emit('togglemode'); + } + + /** + * Disables the specified annotation mode + * + * @return {void} + */ + exit() { + this.destroyPendingThreads(); + this.annotatedElement.classList.remove(CLASS_ANNOTATION_MODE); + if (this.buttonEl) { + this.buttonEl.classList.remove(CLASS_ACTIVE); + } + + this.unbindListeners(); // Disable mode + this.emit(ANNOTATOR_EVENT.modeExit); + } + + /** + * Enables the specified annotation mode + * + * @return {void} + */ + enter() { + this.annotatedElement.classList.add(CLASS_ANNOTATION_MODE); + if (this.buttonEl) { + this.buttonEl.classList.add(CLASS_ACTIVE); + } + + this.emit(ANNOTATOR_EVENT.modeEnter); // Disable other annotations + this.bindListeners(); // Enable mode + } + + isModeEnabled() { + return this.buttonEl ? this.buttonEl.classList.contains(CLASS_ACTIVE) : false; + } + + /** + * Bind the mode listeners and store each handler for future unbinding + * + * @public + * @return {void} + */ + bindListeners() { + 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} + */ + unbindListeners() { + 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) { + const { page, pageThreads } = addThreadToMap(thread, this.threads); + this.threads[page] = pageThreads; + this.emit('registerthread', thread); + thread.addListener('threadevent', (data) => this.handleThreadEvents(thread, data)); + } + + /** + * Unregister a previously registered thread + * + * @public + * @param {AnnotationThread} thread - The thread to unregister with the controller + * @return {void} + */ + unregisterThread(thread) { + const { page, pageThreads } = removeThreadFromMap(thread, this.threads); + this.threads[page] = pageThreads; + this.emit('unregisterthread', thread); + thread.removeListener('threadevent', this.handleThreadEvents); + } + + /** + * 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.threads[page] || {}; + if (threadID in pageThreads) { + thread = pageThreads[threadID]; + } + }); + + return thread; + } + + /** + * Clean up any selected annotations + * + * @return {void} + */ + removeSelection() {} + + /** + * 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() {} + /* eslint-enable no-unused-vars */ + + /** + * Handles annotation thread events and emits them to the viewer + * + * @private + * @param {AnnotationThread} thread - The thread that emitted the event + * @param {Object} [data] - Annotation thread event data + * @param {string} [data.event] - Annotation thread event + * @param {string} [data.data] - Annotation thread event data + * @return {void} + */ + handleThreadEvents(thread, data) { + 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.unregisterThread(thread); + break; + case THREAD_EVENT.threadDelete: + // Thread was deleted, remove from thread map + this.unregisterThread(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); + } + } + + /** + * 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); + } + + /** + * 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.threads[page] || {}; + + Object.keys(pageThreads).forEach((threadID) => { + const thread = pageThreads[threadID]; + if (isPending(thread.state)) { + hasPendingThreads = true; + thread.destroy(); + } + }); + }); + return hasPendingThreads; + } + + /** + * Emits a generic annotator event + * + * @private + * @emits annotatorevent + * @param {string} event - Event name + * @param {Object} data - Event data + * @return {void} + */ + emit(event, data) { + super.emit(event, data); + super.emit('annotationcontrollerevent', { + event, + data, + mode: this.mode + }); + } +} + +export default AnnotationModeController; diff --git a/src/drawing/DrawingModeController.js b/src/controllers/DrawingModeController.js similarity index 76% rename from src/drawing/DrawingModeController.js rename to src/controllers/DrawingModeController.js index f62e50a29..d6a3a3e14 100644 --- a/src/drawing/DrawingModeController.js +++ b/src/controllers/DrawingModeController.js @@ -1,16 +1,22 @@ import rbush from 'rbush'; -import AnnotationModeController from '../AnnotationModeController'; +import AnnotationModeController from './AnnotationModeController'; import annotationsShell from './../annotationsShell.html'; import * as annotatorUtil from '../annotatorUtil'; import { - CLASS_ANNOTATION_DRAW, TYPES, + THREAD_EVENT, STATES, SELECTOR_ANNOTATION_BUTTON_DRAW_CANCEL, SELECTOR_ANNOTATION_BUTTON_DRAW_POST, SELECTOR_ANNOTATION_BUTTON_DRAW_UNDO, SELECTOR_ANNOTATION_BUTTON_DRAW_REDO, - DRAW_BORDER_OFFSET + SELECTOR_BOX_PREVIEW_BASE_HEADER, + SELECTOR_ANNOTATION_DRAWING_HEADER, + CLASS_ANNNOTATION_DRAWING_BACKGROUND, + DRAW_BORDER_OFFSET, + CLASS_ACTIVE, + CLASS_ANNOTATION_MODE, + ANNOTATOR_EVENT } from '../annotationConstants'; class DrawingModeController extends AnnotationModeController { @@ -34,28 +40,51 @@ class DrawingModeController extends AnnotationModeController { /** @property {HTMLElement} - The button to redo a stroke on the pending drawing thread */ redoButtonEl; + init(data) { + super.init(data); + + if (data.header !== 'none') { + // We need to create our own header UI + this.setupHeader(this.container, annotationsShell); + } + + this.cancelButtonEl = this.getModeButton(SELECTOR_ANNOTATION_BUTTON_DRAW_CANCEL); + this.postButtonEl = this.getModeButton(SELECTOR_ANNOTATION_BUTTON_DRAW_POST); + this.undoButtonEl = this.getModeButton(SELECTOR_ANNOTATION_BUTTON_DRAW_UNDO); + this.redoButtonEl = this.getModeButton(SELECTOR_ANNOTATION_BUTTON_DRAW_REDO); + } + /** - * Register the annotator and any information associated with the annotator + * Disables the specified annotation mode * - * @inheritdoc - * @public - * @param {Annotator} annotator - The annotator to be associated with the controller * @return {void} */ - registerAnnotator(annotator) { - super.registerAnnotator(annotator); + exit() { + this.emit(ANNOTATOR_EVENT.modeExit, { headerSelector: SELECTOR_BOX_PREVIEW_BASE_HEADER }); - if (annotator.options.header !== 'none') { - // We need to create our own header UI - this.setupHeader(annotator.container, annotationsShell); - } + this.annotatedElement.classList.remove(CLASS_ANNOTATION_MODE); + this.buttonEl.classList.remove(CLASS_ACTIVE); + + this.annotatedElement.classList.remove(CLASS_ANNNOTATION_DRAWING_BACKGROUND); + + this.unbindListeners(); // Disable mode + this.emit('binddomlisteners'); + } + + /** + * Enables the specified annotation mode + * + * @return {void} + */ + enter() { + this.annotatedElement.classList.add(CLASS_ANNOTATION_MODE); + this.buttonEl.classList.add(CLASS_ACTIVE); - this.cancelButtonEl = annotator.getAnnotateButton(SELECTOR_ANNOTATION_BUTTON_DRAW_CANCEL); - this.postButtonEl = annotator.getAnnotateButton(SELECTOR_ANNOTATION_BUTTON_DRAW_POST); - this.undoButtonEl = annotator.getAnnotateButton(SELECTOR_ANNOTATION_BUTTON_DRAW_UNDO); - this.redoButtonEl = annotator.getAnnotateButton(SELECTOR_ANNOTATION_BUTTON_DRAW_REDO); + this.annotatedElement.classList.add(CLASS_ANNNOTATION_DRAWING_BACKGROUND); - this.annotator.annotatedElement.classList.add(CLASS_ANNOTATION_DRAW); + this.emit(ANNOTATOR_EVENT.modeEnter, { headerSelector: SELECTOR_ANNOTATION_DRAWING_HEADER }); + this.emit('unbinddomlisteners'); // Disable other annotations + this.bindListeners(); // Enable mode } /** @@ -72,6 +101,10 @@ class DrawingModeController extends AnnotationModeController { } this.threads.insert(thread); + this.emit('registerthread', thread); + thread.addListener('threadevent', (data) => { + this.handleThreadEvents(thread, data); + }); } /** @@ -88,26 +121,8 @@ class DrawingModeController extends AnnotationModeController { } this.threads.remove(thread); - } - - /** - * Binds custom event listeners for a thread. - * - * @inheritdoc - * @protected - * @param {AnnotationThread} thread - Thread to bind events to - * @return {void} - */ - bindCustomListenersOnThread(thread) { - if (!thread) { - return; - } - - super.bindCustomListenersOnThread(thread); - - // On save, add the thread to the Rbush, on delete, remove it from the Rbush - thread.addListener('annotationsaved', () => this.registerThread(thread)); - thread.addListener('annotationdelete', () => this.unregisterThread(thread)); + this.emit('unregisterthread', thread); + thread.removeListener('threadevent', this.handleThreadEvents); } /** @@ -117,8 +132,8 @@ class DrawingModeController extends AnnotationModeController { * @protected * @return {void} */ - unbindModeListeners() { - super.unbindModeListeners(); + unbindListeners() { + super.unbindListeners(); annotatorUtil.disableElement(this.undoButtonEl); annotatorUtil.disableElement(this.redoButtonEl); @@ -154,35 +169,37 @@ class DrawingModeController extends AnnotationModeController { // Setup this.currentThread = this.annotator.createAnnotationThread([], {}, TYPES.draw); - this.bindCustomListenersOnThread(this.currentThread); + this.currentThread.addListener('threadevent', (data) => { + this.handleThreadEvents(this.currentThread, data); + }); // Get handlers this.pushElementHandler( - this.annotator.annotatedElement, + this.annotatedElement, ['mousemove', 'touchmove'], annotatorUtil.eventToLocationHandler(locationFunction, this.currentThread.handleMove) ); this.pushElementHandler( - this.annotator.annotatedElement, + this.annotatedElement, ['mousedown', 'touchstart'], annotatorUtil.eventToLocationHandler(locationFunction, this.currentThread.handleStart) ); this.pushElementHandler( - this.annotator.annotatedElement, + this.annotatedElement, ['mouseup', 'touchcancel', 'touchend'], annotatorUtil.eventToLocationHandler(locationFunction, this.currentThread.handleStop) ); this.pushElementHandler(this.cancelButtonEl, 'click', () => { this.currentThread.cancelUnsavedAnnotation(); - this.annotator.toggleAnnotationHandler(TYPES.draw); + this.toggleMode(); }); this.pushElementHandler(this.postButtonEl, 'click', () => { this.currentThread.saveAnnotation(TYPES.draw); - this.annotator.toggleAnnotationHandler(TYPES.draw); + this.toggleMode(); }); this.pushElementHandler(this.undoButtonEl, 'click', this.currentThread.undo); @@ -198,20 +215,20 @@ class DrawingModeController extends AnnotationModeController { * @param {Object} data - Extra data related to the annotation event * @return {void} */ - handleAnnotationEvent(thread, data = {}) { + handleThreadEvents(thread, data = {}) { const { eventData } = data; switch (data.event) { case 'locationassigned': // Register the thread to the threadmap when a starting location is assigned. Should only occur once. - this.annotator.addThreadToMap(thread); + this.registerThread(thread); break; case 'softcommit': // Save the original thread, create a new thread and // start drawing at the location indicating the page change this.currentThread = undefined; thread.saveAnnotation(TYPES.draw); - this.unbindModeListeners(); - this.bindModeListeners(); + this.unbindListeners(); + this.bindListeners(); // Given a location (page change) start drawing at the provided location if (eventData && eventData.location) { @@ -224,8 +241,8 @@ class DrawingModeController extends AnnotationModeController { // Soft delete, in-progress thread doesn't require a redraw or a delete on the server // Clear in-progress thread and restart drawing thread.destroy(); - this.unbindModeListeners(); - this.bindModeListeners(); + this.unbindListeners(); + this.bindListeners(); } else { thread.deleteThread(); this.unregisterThread(thread); @@ -238,6 +255,12 @@ class DrawingModeController extends AnnotationModeController { case 'availableactions': this.updateUndoRedoButtonEls(eventData.undo, eventData.redo); break; + case THREAD_EVENT.save: + this.registerThread(thread); + break; + case THREAD_EVENT.delete: + this.unregisterThread(thread); + break; default: } } diff --git a/src/controllers/HighlightModeController.js b/src/controllers/HighlightModeController.js new file mode 100644 index 000000000..0a756ce5d --- /dev/null +++ b/src/controllers/HighlightModeController.js @@ -0,0 +1,65 @@ +import AnnotationModeController from './AnnotationModeController'; +import { ANNOTATOR_EVENT, THREAD_EVENT } from '../annotationConstants'; + +class HighlightModeController extends AnnotationModeController { + /** + * Handles annotation thread events and emits them to the viewer + * + * @private + * @param {AnnotationThread} thread - The thread that emitted the event + * @param {Object} [data] - Annotation thread event data + * @param {string} [data.event] - Annotation thread event + * @param {string} [data.data] - Annotation thread event data + * @return {void} + */ + handleThreadEvents(thread, data) { + 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.unregisterThread(thread); + this.emit('showhighlights', thread.location.page); + break; + case THREAD_EVENT.threadDelete: + // Thread was deleted, remove from thread map + this.unregisterThread(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); + } + } + + /** + * Disables the specified annotation mode + * + * @return {void} + */ + exit() { + this.destroyPendingThreads(); + window.getSelection().removeAllRanges(); + this.unbindListeners(); // Disable mode + this.emit('binddomlisteners'); + } + + /** + * Enables the specified annotation mode + * + * @return {void} + */ + enter() { + this.emit('unbinddomlisteners'); // Disable other annotations + this.bindListeners(); // Enable mode + } +} + +export default HighlightModeController; diff --git a/src/controllers/PointModeController.js b/src/controllers/PointModeController.js new file mode 100644 index 000000000..916b6b47a --- /dev/null +++ b/src/controllers/PointModeController.js @@ -0,0 +1,79 @@ +import AnnotationModeController from './AnnotationModeController'; +import { TYPES, THREAD_EVENT } from '../annotationConstants'; + +class PointModeController extends AnnotationModeController { + /** @property {DrawingThread} - The currently selected DrawingThread */ + selectedThread; + + /** @property {HTMLElement} - The button to cancel the pending drawing thread */ + cancelButtonEl; + + /** @property {HTMLElement} - The button to commit the pending drawing thread */ + postButtonEl; + + /** + * Set up and return the necessary handlers for the annotation mode + * + * @inheritdoc + * @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() { + this.pointClickHandler = this.pointClickHandler.bind(this); + // Get handlers + this.pushElementHandler(this.annotatedElement, ['mousedown', 'touchstart'], this.pointClickHandler); + + this.pushElementHandler(this.cancelButtonEl, 'click', () => { + this.currentThread.cancelUnsavedAnnotation(); + this.emit('togglemode'); + }); + + this.pushElementHandler(this.postButtonEl, 'click', () => { + this.currentThread.saveAnnotation(TYPES.point); + this.emit('togglemode'); + }); + } + + /** + * 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 + this.emit('togglemode'); + + // Get annotation location from click event, ignore click if location is invalid + const location = this.annotator.getLocationFromEvent(event, TYPES.point); + if (!location) { + return; + } + + // Create new thread with no annotations, show indicator, and show dialog + const thread = this.annotator.createAnnotationThread([], location, TYPES.point); + + if (thread) { + thread.show(); + this.registerThread(thread); + } + + this.annotator.emit(THREAD_EVENT.pending, thread.getThreadEventData()); + } +} + +export default PointModeController; diff --git a/src/doc/DocAnnotator.js b/src/doc/DocAnnotator.js index 4c41d9cd6..67b9b0dd2 100644 --- a/src/doc/DocAnnotator.js +++ b/src/doc/DocAnnotator.js @@ -356,10 +356,8 @@ class DocAnnotator extends Annotator { thread = new DocPointThread(threadParams); } - if (!thread && this.notification) { + if (!thread) { this.emit(ANNOTATOR_EVENT.error, this.localized.createError); - } else if (thread && (type !== TYPES.draw || location.page)) { - this.addThreadToMap(thread); } return thread; @@ -381,7 +379,7 @@ class DocAnnotator extends Annotator { } // TODO (@jholdstock|@spramod) remove this if statement, and make super call, upon refactor. - const pageThreads = this.getThreadsOnPage(pageNum); + const pageThreads = this.threads[pageNum] || {}; Object.keys(pageThreads).forEach((threadID) => { const thread = pageThreads[threadID]; if (!this.isModeAnnotatable(thread.type)) { @@ -586,7 +584,8 @@ class DocAnnotator extends Annotator { thread.show(this.plainHighlightEnabled, this.commentHighlightEnabled); thread.dialog.postAnnotation(commentText); - this.bindCustomListenersOnThread(thread); + const controller = this.modeControllers[highlightType]; + controller.registerThread(thread); this.emit(THREAD_EVENT.threadSave, thread.getThreadEventData()); return thread; @@ -628,7 +627,7 @@ class DocAnnotator extends Annotator { // Set all annotations that are in the 'hover' state to 'inactive' Object.keys(this.threads).forEach((page) => { - const pageThreads = this.getThreadsOnPage(page); + const pageThreads = this.threads[page] || {}; const highlightThreads = this.getHighlightThreadsOnPage(pageThreads); highlightThreads.filter(isThreadInHoverState).forEach((thread) => { thread.reset(); @@ -673,7 +672,7 @@ class DocAnnotator extends Annotator { this.mouseY = event.clientY; Object.keys(this.threads).forEach((page) => { - const pageThreads = this.getThreadsOnPage(page); + const pageThreads = this.threads[page] || {}; const highlightThreads = this.getHighlightThreadsOnPage(pageThreads); highlightThreads.forEach((thread) => { thread.onMousedown(); @@ -745,7 +744,7 @@ class DocAnnotator extends Annotator { const delayThreads = []; let hoverActive = false; - const pageThreads = this.getThreadsOnPage(page); + const pageThreads = this.threads[page] || {}; Object.keys(pageThreads).forEach((threadID) => { const thread = pageThreads[threadID]; @@ -915,7 +914,7 @@ class DocAnnotator extends Annotator { let activeThread = null; const page = annotatorUtil.getPageInfo(event.target).page; - const pageThreads = this.getThreadsOnPage(page); + const pageThreads = this.threads[page] || {}; Object.keys(pageThreads).forEach((threadID) => { const thread = pageThreads[threadID]; @@ -976,7 +975,7 @@ class DocAnnotator extends Annotator { */ getHighlightThreadsOnPage(page) { const threads = []; - const pageThreads = this.getThreadsOnPage(page); + const pageThreads = this.threads[page] || {}; Object.keys(pageThreads).forEach((threadID) => { const thread = pageThreads[threadID]; @@ -1034,32 +1033,27 @@ class DocAnnotator extends Annotator { } /** - * Handles annotation thread events and emits them to the viewer + * Handle events emitted by the annotaiton service * * @private - * @param {Object} [data] - Annotation thread event data - * @param {string} [data.event] - Annotation thread event - * @param {string} [data.data] - Annotation thread event data + * @param {Object} [data] - Annotation service event data + * @param {string} [data.event] - Annotation service event + * @param {string} [data.data] - * @return {void} */ - handleAnnotationThreadEvents(data) { - if (!data.data || !data.data.threadID) { - return; - } - - const thread = this.getThreadByID(data.data.threadID); - if (!thread) { - return; - } - - super.handleAnnotationThreadEvents(data); - + handleControllerEvents(data) { switch (data.event) { - case THREAD_EVENT.threadDelete: - this.showHighlightsOnPage(thread.location.page); + case 'showhighlights': + this.showHighlightsOnPage(data.data); + break; + case 'binddomlisteners': + if (this.createHighlightDialog) { + this.createHighlightDialog.hide(); + } break; default: } + super.handleControllerEvents(data); } /** diff --git a/src/doc/DocDrawingThread.js b/src/doc/DocDrawingThread.js index d630b5ff8..8566f78c8 100644 --- a/src/doc/DocDrawingThread.js +++ b/src/doc/DocDrawingThread.js @@ -196,8 +196,12 @@ class DocDrawingThread extends DrawingThread { return; } - this.dialog.addListener('annotationcreate', () => this.emit('softcommit')); - this.dialog.addListener('annotationdelete', () => this.emit('dialogdelete')); + this.dialog.addListener('annotationcreate', () => { + this.emit('softcommit'); + }); + this.dialog.addListener('annotationdelete', () => { + this.emit('dialogdelete'); + }); } /** diff --git a/src/doc/DocHighlightThread.js b/src/doc/DocHighlightThread.js index 7bb7f9f0a..1e7b37637 100644 --- a/src/doc/DocHighlightThread.js +++ b/src/doc/DocHighlightThread.js @@ -4,6 +4,7 @@ import DocHighlightDialog from './DocHighlightDialog'; import * as annotatorUtil from '../annotatorUtil'; import * as docAnnotatorUtil from './docAnnotatorUtil'; import { + THREAD_EVENT, STATES, TYPES, SELECTOR_ADD_HIGHLIGHT_BTN, @@ -64,6 +65,7 @@ class DocHighlightThread extends AnnotationThread { if (this.state === STATES.pending) { window.getSelection().removeAllRanges(); } + this.emit(THREAD_EVENT.threadCleanup); } /** diff --git a/src/drawing/DrawingThread.js b/src/drawing/DrawingThread.js index 3d2da276d..5f87f478a 100644 --- a/src/drawing/DrawingThread.js +++ b/src/drawing/DrawingThread.js @@ -151,13 +151,16 @@ class DrawingThread extends AnnotationThread { // Calculate the bounding rectangle const [x, y, width, height] = this.getBrowserRectangularBoundary(); + // Clear the drawn thread and its boundary - this.concreteContext.clearRect( - x - DRAW_BORDER_OFFSET, - y + DRAW_BORDER_OFFSET, - width + DRAW_BORDER_OFFSET * 2, - height - DRAW_BORDER_OFFSET * 2 - ); + if (this.concreteContext) { + this.concreteContext.clearRect( + x - DRAW_BORDER_OFFSET, + y + DRAW_BORDER_OFFSET, + width + DRAW_BORDER_OFFSET * 2, + height - DRAW_BORDER_OFFSET * 2 + ); + } this.clearBoundary(); diff --git a/src/image/ImageAnnotator.js b/src/image/ImageAnnotator.js index 622e40a4f..d2ac20e77 100644 --- a/src/image/ImageAnnotator.js +++ b/src/image/ImageAnnotator.js @@ -3,11 +3,7 @@ import Annotator from '../Annotator'; import ImagePointThread from './ImagePointThread'; import * as annotatorUtil from '../annotatorUtil'; import * as imageAnnotatorUtil from './imageAnnotatorUtil'; -import { - CLASS_ANNOTATION_POINT_MARKER, - SELECTOR_ANNOTATION_BUTTON_POINT, - ANNOTATOR_EVENT -} from '../annotationConstants'; +import { ANNOTATOR_EVENT } from '../annotationConstants'; const IMAGE_NODE_NAME = 'img'; // Selector for image container OR multi-image container @@ -137,39 +133,8 @@ class ImageAnnotator extends Annotator { } thread = new ImagePointThread(threadParams); - this.addThreadToMap(thread); return thread; } - - /** - * Hides all annotations on the image. Also hides button in header that - * enables point annotation mode - * - * @return {void} - */ - hideAllAnnotations() { - const annotateButton = this.getAnnotateButton(SELECTOR_ANNOTATION_BUTTON_POINT); - const annotations = this.annotatedElement.getElementsByClassName(CLASS_ANNOTATION_POINT_MARKER); - for (let i = 0; i < annotations.length; i++) { - annotatorUtil.hideElement(annotations[i]); - } - annotatorUtil.hideElement(annotateButton); - } - - /** - * Shows all annotations on the image. Shows button in header that - * enables point annotation mode - * - * @return {void} - */ - showAllAnnotations() { - const annotateButton = this.getAnnotateButton(SELECTOR_ANNOTATION_BUTTON_POINT); - const annotations = this.annotatedElement.getElementsByClassName(CLASS_ANNOTATION_POINT_MARKER); - for (let i = 0; i < annotations.length; i++) { - annotatorUtil.showElement(annotations[i]); - } - annotatorUtil.showElement(annotateButton); - } } export default ImageAnnotator;