-
Notifications
You must be signed in to change notification settings - Fork 116
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Feature: drawing annotation deletion #337
Conversation
…n/box-content-preview into branch/rebaseToRemote
…/MinhHNguyen/box-content-preview into feature/drawingAnnotationScaling
src/lib/viewers/BaseViewer.js
Outdated
@@ -3,6 +3,8 @@ import EventEmitter from 'events'; | |||
import debounce from 'lodash.debounce'; | |||
import fullscreen from '../Fullscreen'; | |||
import RepStatus from '../RepStatus'; | |||
// TODO (@minhnguyen): Import controller from annotations npm module once the library is separate | |||
import DrawingController from '../annotations/drawing/DrawingController'; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I really want to avoid calling annotation specific code in BaseViewer. Is it possible to move this into Annotator or BoxAnnotations?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Unsure if it makes sense to allow external controllers. Any other annotation type that gets added would be added through the annotations codebase, so any needed controllers would also be added in the annotations codebase.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good point, I'll move it into annotator as discussed.
return; | ||
} | ||
|
||
thread.removeAllListeners('threaddeleted'); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pretty sure you can call this once without args and it will remove all listeners for the thread.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks like you're right about this, but there is a comment in annotator regarding calling unbindCustomListenersOnThread on 'threadcleanup':
// Thread should be cleaned up, unbind listeners - we don't do
// in threaddeleted listener since thread may still need to respond
// to error messages
src/lib/annotations/Annotator.js
Outdated
) { | ||
modeData.controller.bindCustomListenersOnThread(thread); | ||
modeData.controller.registerThread(thread); | ||
this.addThreadToMap(thread); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Remind me why we need to bind the thread so early and not on mode entry?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This particular binding is done on AnnotationThreads fetched from the server. This binding allows the annotator to react to the deletion of saved AnnotationThreads.
@@ -667,6 +687,7 @@ class Annotator extends EventEmitter { | |||
unbindCustomListenersOnThread(thread) { | |||
thread.removeAllListeners('threaddeleted'); | |||
thread.removeAllListeners('threadcleanup'); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If I'm right about removeAllListeners then it applies here as well.
src/lib/annotations/Annotator.js
Outdated
while (this.annotationModeHandlers.length > 0) { | ||
const handler = this.annotationModeHandlers.pop(); | ||
handler.eventObj.removeEventListener(handler.type, handler.func); | ||
const eventNames = handler.type.split(' '); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Are these for different events then the unbind func on line 55 of annotationController?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I was initially checking out a way to bind the same listener to multiple events ie. 'mousedown', 'touchstart' by simply adding it to the string in bindModeListeners.
This is done in AnnotationController (see drawingcontroller line 119 for an example) and doesn't need to be in annotator. I'll remove this.
@@ -436,7 +437,9 @@ class DocAnnotator extends Annotator { | |||
|
|||
if (this.hasTouch && this.isMobile) { | |||
document.addEventListener('selectionchange', this.onSelectionChange); | |||
document.addEventListener('touchstart', this.drawingSelectionHandler); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
can we add these to the annotated element as well?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not quite sure. I will test it out but it might be the case that mobile browsers are functioning a bit differently.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just tested it out, looks like highlight selection needs to be bound to document but the drawing selection can be bound to annotated element.
* @param {Object} data - Extra data related to the annotation event | ||
* @return {void} | ||
*/ | ||
handleAnnotationEvent(thread, data = {}) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
+1 on the comments here, nice.
} | ||
|
||
const eventBoundary = { | ||
minX: +location.x - DRAW_BORDER_OFFSET, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What do these +'s do?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sometimes location.x/y are strings (I suspect this is when they are returned from getLocation) and + (unary plus operator) ensures they are parsed as numbers. We can also use parseFloat() here if that is more clear.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It looks like +number is actually the fastest. https://jsperf.com/number-vs-plus-vs-toint-vs-tofloat/16
return; | ||
} | ||
|
||
// Randomly select a thread in case there are multiple |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
o_0
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This will change with better hit detection 👍 (using a hitmap canvas)
} | ||
|
||
/** | ||
* Select the indicated drawing thread. Deletes a drawing thread upon the second consecutive selection |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This will change once we introduce UI, correct?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, with the UI, a selection should show the GUI. A deletion will be done by clicking delete on the GUI option.
*/ | ||
updateUndoRedoButtonEls(undoCount, redoCount) { | ||
if (this.undoButtonEl) { | ||
if (undoCount === 1) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can these numbers be > 1?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, in which case we don't need to do anything because they should already be enabled. In theory, the undo and redo counts will always start at 0. When they hit 1 they will be enabled and stay enabled for values > 1. If they drop to 0 they will return to the starting state (of being disabled).
src/lib/viewers/BaseViewer.js
Outdated
@@ -3,6 +3,8 @@ import EventEmitter from 'events'; | |||
import debounce from 'lodash.debounce'; | |||
import fullscreen from '../Fullscreen'; | |||
import RepStatus from '../RepStatus'; | |||
// TODO (@minhnguyen): Import controller from annotations npm module once the library is separate | |||
import DrawingController from '../annotations/drawing/DrawingController'; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Unsure if it makes sense to allow external controllers. Any other annotation type that gets added would be added through the annotations codebase, so any needed controllers would also be added in the annotations codebase.
* @return {void} | ||
*/ | ||
registerAnnotator(annotator) { | ||
// TODO (@minhnguyen): remove the need to register an annotator. Ideally, the annotator should know about the |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍
// Get handlers | ||
handlers.push( | ||
{ | ||
type: 'mousemove touchmove', |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
maybe store this as type: ['mousemove', 'touchmove']
unbindModeListeners() { | ||
while (this.handlers.length > 0) { | ||
const handler = this.handlers.pop(); | ||
const eventNames = handler.type.split(' '); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would just save the event types as an array so you can just loop through them instead of parsing a string into an array
|
||
class AnnotationController extends EventEmitter { | ||
/** @property {Array} - The array of annotation threads */ | ||
threads = []; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So would we be storing all the annotation threads both in the annotator and the controller then? Seems redundant to have it stored in 2 places
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The goal is to have each controller manage their respective type of threads. Right now we are at an in-between state where highlight and point annotation doesn't use a controller so we still need to store threads in the annotator and controller. In addition, the threadmap is in use in other functions that I did not want to modify.
The good news is that these data structures simply store a reference to the thread so the memory overhead isn't that bad. The downside is that the thread reference needs to be removed from both (this is accomplished by listening to 'threaddeleted').
src/lib/annotations/Annotator.js
Outdated
@@ -167,6 +171,11 @@ class Annotator extends EventEmitter { | |||
|
|||
const handler = this.getAnnotationModeClickHandler(currentMode); | |||
annotateButtonEl.addEventListener('click', handler); | |||
|
|||
// TODO (@minhnguyen): Implement controller for point mode annotation and remove this check |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍
@@ -0,0 +1,146 @@ | |||
import EventEmitter from 'events'; | |||
|
|||
class AnnotationController extends EventEmitter { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Might be clearer to label this AnnotationModeController since it only applies to annotation types that would enter a 'mode'. AKA not highlight annotations
|
…yen/box-content-preview into feature/drawingDeletion
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🎉
const handlers = this.setupAndGetHandlers(); | ||
handlers.forEach((handler) => { | ||
const types = handler.type instanceof Array ? handler.type : [handler.type]; | ||
types.forEach((eventName) => handler.eventObj.addEventListener(eventName, handler.func)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
new line before forEach and maybe style it like this
types.forEach((eventName) => {
...
});
while (this.handlers.length > 0) { | ||
const handler = this.handlers.pop(); | ||
const types = handler.type instanceof Array ? handler.type : [handler.type]; | ||
types.forEach((eventName) => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
new line before forEach
src/lib/annotations/Annotator.js
Outdated
@@ -167,6 +170,11 @@ class Annotator extends EventEmitter { | |||
|
|||
const handler = this.getAnnotationModeClickHandler(currentMode); | |||
annotateButtonEl.addEventListener('click', handler); | |||
|
|||
const { CONTROLLERS } = this.options.annotator; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What if this.options.annotator
is null?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also might make sense to store each valid mode controller as this.modeControllers
so you're not constantly checking if this.options.annotator
and CONTROLLERS
exist
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This.options.annotator can't be null because of the isModeAnnotatable check above
* @return {void} | ||
*/ | ||
unbindModeListeners() { | ||
unbindModeListeners(mode) { | ||
while (this.annotationModeHandlers.length > 0) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What would be the difference between removing these listeners vs. calling CONTROLLERS[mode]. unbindModeListeners()
? Does this part just remove point annotation listeners (I assume until we create a modeController for point annotation mode as well)?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This part delegates the unbinding to the controller rather than the annotator. Meaning once a point annotation controller is in (and preferably highlight as well) this call just needs to be a call to controller[mode].unbindModeListeners()
annotatorConfig.CONTROLLERS = {}; | ||
annotatorConfig.TYPE.forEach((type) => { | ||
if (type in ANNOTATOR_TYPE_CONTROLLERS) { | ||
annotatorConfig.CONTROLLERS[type] = new ANNOTATOR_TYPE_CONTROLLERS[type](); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
annotatorConfig.CONTROLLERS[type] = new ANNOTATOR_TYPE_CONTROLLERS[type].CONSTRUCTOR();
* @param {Object} location - The location information of the pointer | ||
* @return {void} | ||
*/ | ||
handleMove(location) { | ||
if (this.drawingFlag !== DRAW_STATES.drawing) { | ||
if (this.drawingFlag !== DRAW_STATES.drawing || !location) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would this not have a valid location at some point?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It would not have a valid location upon moving the mouse on the side of the document.
* | ||
* @return {Object} The object with the boundaries and the path | ||
*/ | ||
getAABB() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
getAxisAlignedBoundingBox()
or getAlignedBoundingBox()
} | ||
); | ||
|
||
if (this.postButtonEl) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
move these into their own helper method and just call it for each button. May even make sense to move it to the base class!
addButtonHandlers(buttonEl, handler) {
if (buttonEl) {
const buttonHandler = {
type: 'click,
func: handler,
eventObj: buttonEl
};
handlers.push(buttonHandler);
}
}; | ||
|
||
// Get the threads that correspond to the point that was clicked on | ||
const intersectingThreads = this.threads |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
is drawController.threads an array?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It is an array by default (AnnotationModeController) and an RTree in DrawingModeController
|
||
// Calculate the bounding rectangle | ||
const [x, y, width, height] = this.getRectangularBoundary(); | ||
// Clear the drawn thread and destroy it |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
new line before comment
Unfortunately I didn't have the foresight to split selection and deletion into separate pull requests, but ~900 lines are tests.