Skip to content

Commit

Permalink
Feature: drawing annotation deletion (#337)
Browse files Browse the repository at this point in the history
* Fix: thread save incorrectly rejects when no dialogue exists
* Update: drawing annotations now rescale correctly
* Fix: annotator is now loaded with the correct initial scale
* Fix: update annotator tests for constructor change
* Update: tests for updating local annotations on annotationthreads
* Update: fix tests
* Update: tests for docdrawingthread
* Update: undo redo drawing container
* Fix: pull request feedback
* New: drawing annotation undo and redo container
* Update: undo and redo grey out when applicable
* Fix: update tests with new fn calls
* Chore: update variable name
* Chore: actually update the draw states variable
* Update: drawing annotation svgs
* Update: remove draw from drawstates
* Update: handle page change with two toggles
* Update: merge refactor from master
* Fix: bind cleanup listeners on drawing thread and fix resize
* Fix: remove global test variable
* Fix: scale annotations in redo stack
* Update: drawing method cleanup
* Update: remove unused class
* Update: undo redo unit tests
* Update: remove isTypeEnabled
* Update: fix tests again
* Update: added test for DocDrawingThreadShow
* Update: PR changes
* Update: custom thread cleanup for drawing annotationevent
* New: drawing deletion
* Update: separate drawing methods
* Update: Change how controllers are instantiated
* Update: bind draw selection to annotated element
  • Loading branch information
Minh-Ng authored Aug 30, 2017
1 parent 70f5b81 commit 34b9da1
Show file tree
Hide file tree
Showing 23 changed files with 1,700 additions and 300 deletions.
172 changes: 172 additions & 0 deletions src/lib/annotations/AnnotationModeController.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
import EventEmitter from 'events';

class AnnotationModeController extends EventEmitter {
/** @property {Array} - The array of annotation threads */
threads = [];

/** @property {Array} - The array of annotation handlers */
handlers = [];

/**
* [constructor]
*
* @return {AnnotationModeController} Annotation controller instance
*/
constructor() {
super();

this.handleAnnotationEvent = this.handleAnnotationEvent.bind(this);
}

/**
* 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);
}

/**
* 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('annotationevent', (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('threaddeleted');
thread.removeAllListeners('threadcleanup');
thread.removeAllListeners('annotationsaved');
thread.removeAllListeners('annotationevent');
}

/**
* 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
});
}
}

export default AnnotationModeController;
4 changes: 3 additions & 1 deletion src/lib/annotations/AnnotationThread.js
Original file line number Diff line number Diff line change
Expand Up @@ -187,7 +187,7 @@ class AnnotationThread extends EventEmitter {

// 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) {
if (this.isMobile && this.dialog) {
this.dialog.removeAnnotation(annotationID);
this.dialog.hideMobileDialog();
}
Expand Down Expand Up @@ -411,6 +411,8 @@ class AnnotationThread extends EventEmitter {
this.dialog.addAnnotation(savedAnnotation);
this.dialog.removeAnnotation(tempAnnotation.annotationID);
}

this.emit('annotationsaved');
}

/**
Expand Down
124 changes: 32 additions & 92 deletions src/lib/annotations/Annotator.js
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,9 @@ class Annotator extends EventEmitter {
this.hasTouch = data.hasTouch;
this.modeButtons = data.modeButtons;
this.annotationModeHandlers = [];

const { CONTROLLERS } = this.options.annotator || {};
this.modeControllers = CONTROLLERS || {};
}

/**
Expand All @@ -84,7 +87,10 @@ class Annotator extends EventEmitter {
Object.keys(this.modeButtons).forEach((type) => {
const handler = this.getAnnotationModeClickHandler(type);
const buttonEl = this.container.querySelector(this.modeButtons[type].selector);
buttonEl.removeEventListener('click', handler);

if (buttonEl) {
buttonEl.removeEventListener('click', handler);
}
});

this.unbindDOMListeners();
Expand Down Expand Up @@ -167,6 +173,10 @@ class Annotator extends EventEmitter {

const handler = this.getAnnotationModeClickHandler(currentMode);
annotateButtonEl.addEventListener('click', handler);

if (this.modeControllers[currentMode]) {
this.modeControllers[currentMode].registerAnnotator(this);
}
}
}

Expand Down Expand Up @@ -536,6 +546,17 @@ 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);
}
});

this.emit('annotationsfetched');
Expand Down Expand Up @@ -667,6 +688,7 @@ class Annotator extends EventEmitter {
unbindCustomListenersOnThread(thread) {
thread.removeAllListeners('threaddeleted');
thread.removeAllListeners('threadcleanup');
thread.removeAllListeners('annotationsaved');
thread.removeAllListeners('annotationevent');
}

Expand All @@ -693,99 +715,12 @@ class Annotator extends EventEmitter {
eventObj: this.annotatedElement
}
);
} else if (mode === TYPES.draw) {
const drawingThread = this.createAnnotationThread([], {}, TYPES.draw);
this.bindCustomListenersOnThread(drawingThread);

/* eslint-disable require-jsdoc */
const locationFunction = (event) => this.getLocationFromEvent(event, TYPES.point);
/* eslint-enable require-jsdoc */

const postButtonEl = this.getAnnotateButton(SELECTOR_ANNOTATION_BUTTON_DRAW_POST);
const undoButtonEl = this.getAnnotateButton(SELECTOR_ANNOTATION_BUTTON_DRAW_UNDO);
const redoButtonEl = this.getAnnotateButton(SELECTOR_ANNOTATION_BUTTON_DRAW_REDO);

// NOTE (@minhnguyen): Move this logic to a new controller class
const that = this;
drawingThread.addListener('annotationevent', (data = {}) => {
switch (data.type) {
case 'drawcommit':
drawingThread.removeAllListeners('annotationevent');
break;
case 'pagechanged':
drawingThread.saveAnnotation(TYPES.draw);
that.unbindModeListeners();
that.bindModeListeners(TYPES.draw);
break;
case 'availableactions':
if (data.undo === 1) {
annotatorUtil.enableElement(undoButtonEl);
} else if (data.undo === 0) {
annotatorUtil.disableElement(undoButtonEl);
}

if (data.redo === 1) {
annotatorUtil.enableElement(redoButtonEl);
} else if (data.redo === 0) {
annotatorUtil.disableElement(redoButtonEl);
}
break;
default:
}
});

handlers.push(
{
type: 'mousemove',
func: annotatorUtil.eventToLocationHandler(locationFunction, drawingThread.handleMove),
eventObj: this.annotatedElement
},
{
type: 'mousedown',
func: annotatorUtil.eventToLocationHandler(locationFunction, drawingThread.handleStart),
eventObj: this.annotatedElement
},
{
type: 'mouseup',
func: annotatorUtil.eventToLocationHandler(locationFunction, drawingThread.handleStop),
eventObj: this.annotatedElement
}
);

if (postButtonEl) {
handlers.push({
type: 'click',
func: () => {
drawingThread.saveAnnotation(mode);
this.toggleAnnotationHandler(mode);
},
eventObj: postButtonEl
});
}

if (undoButtonEl) {
handlers.push({
type: 'click',
func: () => {
drawingThread.undo();
},
eventObj: undoButtonEl
});
}

if (redoButtonEl) {
handlers.push({
type: 'click',
func: () => {
drawingThread.redo();
},
eventObj: redoButtonEl
});
}
} else if (mode === TYPES.draw && this.modeControllers[mode]) {
this.modeControllers[mode].bindModeListeners();
}

handlers.forEach((handler) => {
handler.eventObj.addEventListener(handler.type, handler.func);
handler.eventObj.addEventListener(handler.type, handler.func, false);
this.annotationModeHandlers.push(handler);
});
}
Expand Down Expand Up @@ -836,13 +771,18 @@ class Annotator extends EventEmitter {
* Unbinds event listeners for annotation modes.
*
* @protected
* @param {string} mode - Annotation mode to be unbound
* @return {void}
*/
unbindModeListeners() {
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();
}
}

/**
Expand Down
Loading

0 comments on commit 34b9da1

Please sign in to comment.