diff --git a/src/lib/_boxui.scss b/src/lib/_boxui.scss
index 3b11a81eb..d20fc4790 100644
--- a/src/lib/_boxui.scss
+++ b/src/lib/_boxui.scss
@@ -83,6 +83,19 @@
}
}
+.bp-btn-plain {
+ background: transparent;
+ box-shadow: none;
+ outline: none;
+
+ &.is-disabled {
+ pointer-events: none;
+ color: #666;
+ opacity: .4;
+ background-color: $haze;
+ }
+}
+
.bp-btn {
-webkit-appearance: none;
background-color: $white;
@@ -150,14 +163,6 @@
}
}
-.bp-btn-plain,
-.bp-btn-plain:hover,
-.bp-btn-plain:active,
-.bp-btn-plain:focus {
- background: transparent;
- box-shadow: none;
- outline: none;
-}
//------------------------------------------------------------------------------
// Forms
diff --git a/src/lib/annotations/Annotator.js b/src/lib/annotations/Annotator.js
index de570d4a8..582bfe187 100644
--- a/src/lib/annotations/Annotator.js
+++ b/src/lib/annotations/Annotator.js
@@ -17,6 +17,8 @@ import {
SELECTOR_ANNOTATION_BUTTON_DRAW_CANCEL,
SELECTOR_ANNOTATION_BUTTON_DRAW_ENTER,
SELECTOR_ANNOTATION_BUTTON_DRAW_POST,
+ SELECTOR_ANNOTATION_BUTTON_DRAW_UNDO,
+ SELECTOR_ANNOTATION_BUTTON_DRAW_REDO,
TYPES
} from './annotationConstants';
@@ -385,13 +387,18 @@ class Annotator extends EventEmitter {
if (mode === TYPES.draw) {
const drawEnterEl = buttonEl.querySelector(SELECTOR_ANNOTATION_BUTTON_DRAW_ENTER);
- annotatorUtil.showElement(drawEnterEl);
-
const drawCancelEl = buttonEl.querySelector(SELECTOR_ANNOTATION_BUTTON_DRAW_CANCEL);
- annotatorUtil.hideElement(drawCancelEl);
-
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);
+
+ annotatorUtil.showElement(drawEnterEl);
+ annotatorUtil.hideElement(drawCancelEl);
annotatorUtil.hideElement(postButtonEl);
+ annotatorUtil.hideElement(undoButtonEl);
+ annotatorUtil.hideElement(redoButtonEl);
+ annotatorUtil.disableElement(undoButtonEl);
+ annotatorUtil.disableElement(redoButtonEl);
}
}
@@ -414,13 +421,16 @@ class Annotator extends EventEmitter {
if (mode === TYPES.draw) {
const drawEnterEl = buttonEl.querySelector(SELECTOR_ANNOTATION_BUTTON_DRAW_ENTER);
- annotatorUtil.hideElement(drawEnterEl);
-
const drawCancelEl = buttonEl.querySelector(SELECTOR_ANNOTATION_BUTTON_DRAW_CANCEL);
- annotatorUtil.showElement(drawCancelEl);
-
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);
+
+ annotatorUtil.hideElement(drawEnterEl);
+ annotatorUtil.showElement(drawCancelEl);
annotatorUtil.showElement(postButtonEl);
+ annotatorUtil.showElement(undoButtonEl);
+ annotatorUtil.showElement(redoButtonEl);
}
}
@@ -657,6 +667,7 @@ class Annotator extends EventEmitter {
unbindCustomListenersOnThread(thread) {
thread.removeAllListeners('threaddeleted');
thread.removeAllListeners('threadcleanup');
+ thread.removeAllListeners('annotationevent');
}
/**
@@ -670,16 +681,18 @@ class Annotator extends EventEmitter {
const handlers = [];
if (mode === TYPES.point) {
- handlers.push({
- type: 'mousedown',
- func: this.pointClickHandler,
- eventObj: this.annotatedElement
- });
- handlers.push({
- type: 'touchstart',
- func: this.pointClickHandler,
- eventObj: this.annotatedElement
- });
+ handlers.push(
+ {
+ type: 'mousedown',
+ func: this.pointClickHandler,
+ eventObj: this.annotatedElement
+ },
+ {
+ type: 'touchstart',
+ func: this.pointClickHandler,
+ eventObj: this.annotatedElement
+ }
+ );
} else if (mode === TYPES.draw) {
const drawingThread = this.createAnnotationThread([], {}, TYPES.draw);
this.bindCustomListenersOnThread(drawingThread);
@@ -689,23 +702,56 @@ class Annotator extends EventEmitter {
/* eslint-enable require-jsdoc */
const postButtonEl = this.getAnnotateButton(SELECTOR_ANNOTATION_BUTTON_DRAW_POST);
-
- handlers.push({
- type: 'mousemove',
- func: annotatorUtil.eventToLocationHandler(locationFunction, drawingThread.handleMove),
- eventObj: this.annotatedElement
- });
- handlers.push({
- type: 'mousedown',
- func: annotatorUtil.eventToLocationHandler(locationFunction, drawingThread.handleStart),
- eventObj: this.annotatedElement
- });
- handlers.push({
- type: 'mouseup',
- func: annotatorUtil.eventToLocationHandler(locationFunction, drawingThread.handleStop),
- eventObj: this.annotatedElement
+ 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',
@@ -716,6 +762,26 @@ class Annotator extends EventEmitter {
eventObj: postButtonEl
});
}
+
+ if (undoButtonEl) {
+ handlers.push({
+ type: 'click',
+ func: () => {
+ drawingThread.undo();
+ },
+ eventObj: undoButtonEl
+ });
+ }
+
+ if (redoButtonEl) {
+ handlers.push({
+ type: 'click',
+ func: () => {
+ drawingThread.redo();
+ },
+ eventObj: redoButtonEl
+ });
+ }
}
handlers.forEach((handler) => {
diff --git a/src/lib/annotations/__tests__/Annotator-test.js b/src/lib/annotations/__tests__/Annotator-test.js
index d9208fd83..68f3b9348 100644
--- a/src/lib/annotations/__tests__/Annotator-test.js
+++ b/src/lib/annotations/__tests__/Annotator-test.js
@@ -10,7 +10,9 @@ import {
CLASS_ANNOTATION_MODE,
CLASS_ACTIVE,
CLASS_HIDDEN,
- SELECTOR_BOX_PREVIEW_BTN_ANNOTATE_POINT
+ SELECTOR_ANNOTATION_BUTTON_DRAW_POST,
+ SELECTOR_ANNOTATION_BUTTON_DRAW_UNDO,
+ SELECTOR_ANNOTATION_BUTTON_DRAW_REDO
} from '../annotationConstants';
let annotator;
@@ -369,7 +371,7 @@ describe('lib/annotations/Annotator', () => {
annotator.disableAnnotationMode(TYPES.draw, btn);
expect(btn).to.not.have.class(CLASS_ACTIVE);
expect(stubs.show).to.be.called;
- expect(stubs.hide).to.be.calledTwice;
+ expect(stubs.hide).to.have.callCount(4);
});
});
@@ -401,7 +403,7 @@ describe('lib/annotations/Annotator', () => {
annotator.enableAnnotationMode(TYPES.draw, btn);
expect(btn).to.have.class(CLASS_ACTIVE);
expect(stubs.hide).to.be.called;
- expect(stubs.show).to.be.calledTwice;
+ expect(stubs.show).to.have.callCount(4);
});
});
@@ -545,6 +547,7 @@ describe('lib/annotations/Annotator', () => {
it('should unbind custom listeners from the thread', () => {
stubs.threadMock.expects('removeAllListeners').withArgs('threaddeleted');
stubs.threadMock.expects('removeAllListeners').withArgs('threadcleanup');
+ stubs.threadMock.expects('removeAllListeners').withArgs('annotationevent');
annotator.unbindCustomListenersOnThread(stubs.thread);
});
});
@@ -579,14 +582,14 @@ describe('lib/annotations/Annotator', () => {
expect(annotator.annotationModeHandlers.length).equals(2);
});
- it('should bind draw mode handlers', () => {
+ it('should bind draw mode click handlers if post button exists', () => {
sandbox.stub(annotator, 'createAnnotationThread').returns(drawingThread);
const postButtonEl = {
addEventListener: sandbox.stub(),
removeEventListener: sandbox.stub()
};
- sandbox.stub(annotator, 'getAnnotateButton').returns(null);
+ sandbox.stub(annotator, 'getAnnotateButton').returns(postButtonEl);
const locationHandler = (() => {});
sandbox.stub(annotatorUtil, 'eventToLocationHandler').returns(locationHandler);
@@ -597,22 +600,23 @@ describe('lib/annotations/Annotator', () => {
sinon.match.string,
locationHandler
).thrice;
- expect(postButtonEl.addEventListener).to.not.be.calledWith(
+ expect(postButtonEl.addEventListener).to.be.calledWith(
'click',
sinon.match.func
);
- expect(annotator.annotationModeHandlers.length).equals(3);
+ expect(annotator.annotationModeHandlers.length).equals(6);
});
- it('should bind draw mode click handlers if post button exists', () => {
+ it('should successfully bind draw mode handlers if undo and redo buttons do not exist', () => {
sandbox.stub(annotator, 'createAnnotationThread').returns(drawingThread);
const postButtonEl = {
addEventListener: sandbox.stub(),
removeEventListener: sandbox.stub()
};
- sandbox.stub(annotator, 'getAnnotateButton').returns(postButtonEl);
const locationHandler = (() => {});
+ const getAnnotateButton = sandbox.stub(annotator, 'getAnnotateButton');
+ getAnnotateButton.withArgs(SELECTOR_ANNOTATION_BUTTON_DRAW_POST).returns(postButtonEl);
sandbox.stub(annotatorUtil, 'eventToLocationHandler').returns(locationHandler);
@@ -628,6 +632,33 @@ describe('lib/annotations/Annotator', () => {
);
expect(annotator.annotationModeHandlers.length).equals(4);
});
+
+ it('should successfully bind draw mode handlers if post button does not exist', () => {
+ sandbox.stub(annotator, 'createAnnotationThread').returns(drawingThread);
+
+ const doButtonEl = {
+ addEventListener: sandbox.stub(),
+ removeEventListener: sandbox.stub()
+ };
+ const locationHandler = (() => {});
+ const getAnnotateButton = sandbox.stub(annotator, 'getAnnotateButton');
+ getAnnotateButton.withArgs(SELECTOR_ANNOTATION_BUTTON_DRAW_UNDO).returns(doButtonEl);
+ getAnnotateButton.withArgs(SELECTOR_ANNOTATION_BUTTON_DRAW_REDO).returns(doButtonEl);
+
+ sandbox.stub(annotatorUtil, 'eventToLocationHandler').returns(locationHandler);
+
+ annotator.bindModeListeners(TYPES.draw);
+
+ expect(annotator.annotatedElement.addEventListener).to.be.calledWith(
+ sinon.match.string,
+ locationHandler
+ ).thrice;
+ expect(doButtonEl.addEventListener).to.be.calledWith(
+ 'click',
+ sinon.match.func
+ );
+ expect(annotator.annotationModeHandlers.length).equals(5);
+ });
});
describe('unbindModeListeners()', () => {
diff --git a/src/lib/annotations/__tests__/annotatorUtil-test.js b/src/lib/annotations/__tests__/annotatorUtil-test.js
index 81f54ec27..d47b2ccc5 100644
--- a/src/lib/annotations/__tests__/annotatorUtil-test.js
+++ b/src/lib/annotations/__tests__/annotatorUtil-test.js
@@ -5,6 +5,8 @@ import {
getPageInfo,
showElement,
hideElement,
+ enableElement,
+ disableElement,
showInvisibleElement,
hideElementVisibility,
resetTextarea,
@@ -122,6 +124,34 @@ describe('lib/annotations/annotatorUtil', () => {
});
});
+ 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
diff --git a/src/lib/annotations/annotationConstants.js b/src/lib/annotations/annotationConstants.js
index d74ff2f8c..483869548 100644
--- a/src/lib/annotations/annotationConstants.js
+++ b/src/lib/annotations/annotationConstants.js
@@ -1,6 +1,8 @@
export const CLASS_ACTIVE = 'bp-is-active';
export const CLASS_HIDDEN = 'bp-is-hidden';
export const CLASS_INVISIBLE = 'bp-is-invisible';
+export const CLASS_DISABLED = 'is-disabled';
+
export const CLASS_ANNOTATION_BUTTON_CANCEL = 'cancel-annotation-btn';
export const CLASS_ANNOTATION_BUTTON_POST = 'post-annotation-btn';
export const CLASS_ANNOTATION_DIALOG = 'bp-annotation-dialog';
@@ -25,6 +27,8 @@ export const CLASS_ANNOTATION_LAYER_HIGHLIGHT = 'bp-annotation-layer-highlight';
export const CLASS_ANNOTATION_LAYER_DRAW = 'bp-annotation-layer-draw';
export const CLASS_ANNOTATION_LAYER_DRAW_IN_PROGRESS = 'bp-annotation-layer-draw-in-progress';
export const CLASS_ANNOTATION_BUTTON_POINT = 'bp-btn-annotate-point';
+export const CLASS_ANNOTATION_BUTTON_DRAW_UNDO = 'bp-btn-annotate-draw-undo';
+export const CLASS_ANNOTATION_BUTTON_DRAW_REDO = 'bp-btn-annotate-draw-redo';
export const CLASS_ANNOTATION_BUTTON_DRAW_POST = 'bp-btn-annotate-draw-post';
export const CLASS_ANNOTATION_BUTTON_DRAW_CANCEL = 'bp-btn-annotate-draw-cancel';
export const CLASS_ANNOTATION_BUTTON_DRAW_ENTER = 'bp-btn-annotate-draw-enter';
@@ -46,6 +50,8 @@ export const SECTION_CREATE = '[data-section="create"]';
export const SECTION_SHOW = '[data-section="show"]';
export const SELECTOR_ANNOTATION_BUTTON_POINT = `.${CLASS_ANNOTATION_BUTTON_POINT}`;
+export const SELECTOR_ANNOTATION_BUTTON_DRAW_UNDO = `.${CLASS_ANNOTATION_BUTTON_DRAW_UNDO}`;
+export const SELECTOR_ANNOTATION_BUTTON_DRAW_REDO = `.${CLASS_ANNOTATION_BUTTON_DRAW_REDO}`;
export const SELECTOR_ANNOTATION_BUTTON_DRAW_POST = `.${CLASS_ANNOTATION_BUTTON_DRAW_POST}`;
export const SELECTOR_ANNOTATION_BUTTON_DRAW_CANCEL = `.${CLASS_ANNOTATION_BUTTON_DRAW_CANCEL}`;
export const SELECTOR_ANNOTATION_BUTTON_DRAW_ENTER = `.${CLASS_ANNOTATION_BUTTON_DRAW_ENTER}`;
diff --git a/src/lib/annotations/annotatorUtil.js b/src/lib/annotations/annotatorUtil.js
index 2ad1d0ef3..04480c131 100644
--- a/src/lib/annotations/annotatorUtil.js
+++ b/src/lib/annotations/annotatorUtil.js
@@ -5,7 +5,8 @@ import {
PENDING_STATES,
CLASS_ACTIVE,
CLASS_HIDDEN,
- CLASS_INVISIBLE
+ CLASS_INVISIBLE,
+ CLASS_DISABLED
} from './annotationConstants';
const HEADER_CLIENT_NAME = 'X-Box-Client-Name';
@@ -120,6 +121,40 @@ export function hideElement(elementOrSelector) {
}
}
+/**
+ * Disables the specified element or element with specified selector.
+ *
+ * @param {HTMLElement|string} elementOrSelector - Element or CSS selector
+ * @return {void}
+ */
+export function disableElement(elementOrSelector) {
+ let element = elementOrSelector;
+ if (typeof elementOrSelector === 'string' || elementOrSelector instanceof String) {
+ element = document.querySelector(elementOrSelector);
+ }
+
+ if (element) {
+ element.classList.add(CLASS_DISABLED);
+ }
+}
+
+/**
+ * Enables the specified element or element with specified selector.
+ *
+ * @param {HTMLElement|string} elementOrSelector - Element or CSS selector
+ * @return {void}
+ */
+export function enableElement(elementOrSelector) {
+ let element = elementOrSelector;
+ if (typeof elementOrSelector === 'string' || elementOrSelector instanceof String) {
+ element = document.querySelector(elementOrSelector);
+ }
+
+ if (element) {
+ element.classList.remove(CLASS_DISABLED);
+ }
+}
+
/**
* Shows the specified element or element with specified selector.
*
diff --git a/src/lib/annotations/doc/DocAnnotator.js b/src/lib/annotations/doc/DocAnnotator.js
index 5ac9bc12c..6168f80a1 100644
--- a/src/lib/annotations/doc/DocAnnotator.js
+++ b/src/lib/annotations/doc/DocAnnotator.js
@@ -490,6 +490,10 @@ class DocAnnotator extends Annotator {
// We need to redraw highlights on the page if a thread was deleted
// since deleting 'cuts' out the highlight, which may have been
// overlapping with another
+ if (!annotatorUtil.isHighlightAnnotation(thread.type)) {
+ return;
+ }
+
thread.addListener('threaddeleted', () => {
this.showHighlightsOnPage(thread.location.page);
});
diff --git a/src/lib/annotations/doc/DocDrawingThread.js b/src/lib/annotations/doc/DocDrawingThread.js
index c58cd86f1..94b83c583 100644
--- a/src/lib/annotations/doc/DocDrawingThread.js
+++ b/src/lib/annotations/doc/DocDrawingThread.js
@@ -28,10 +28,7 @@ class DocDrawingThread extends DrawingThread {
constructor(data) {
super(data);
- this.handleStart = this.handleStart.bind(this);
- this.handleMove = this.handleMove.bind(this);
- this.handleStop = this.handleStop.bind(this);
- this.checkAndHandleScaleUpdate = this.checkAndHandleScaleUpdate.bind(this);
+ this.onPageChange = this.onPageChange.bind(this);
this.reconstructBrowserCoordFromLocation = this.reconstructBrowserCoordFromLocation.bind(this);
}
/**
@@ -41,8 +38,10 @@ class DocDrawingThread extends DrawingThread {
* @return {void}
*/
handleMove(location) {
- const pageChanged = this.hasPageChanged(location);
- if (this.drawingFlag !== DRAW_STATES.drawing || pageChanged) {
+ if (this.drawingFlag !== DRAW_STATES.drawing) {
+ return;
+ } else if (this.hasPageChanged(location)) {
+ this.onPageChange();
return;
}
@@ -60,9 +59,7 @@ class DocDrawingThread extends DrawingThread {
handleStart(location) {
const pageChanged = this.hasPageChanged(location);
if (pageChanged) {
- this.handleStop();
- this.saveAnnotation();
- this.emit('drawthreadcommited');
+ this.onPageChange();
return;
}
@@ -94,6 +91,7 @@ class DocDrawingThread extends DrawingThread {
if (this.pendingPath && !this.pendingPath.isEmpty()) {
this.pathContainer.insert(this.pendingPath);
+ this.emitAvailableActions();
this.pendingPath = null;
}
}
@@ -105,7 +103,7 @@ class DocDrawingThread extends DrawingThread {
* @return {boolean} Whether or not the thread page has changed
*/
hasPageChanged(location) {
- return this.location && this.location.page && this.location.page !== location.page;
+ return !!this.location && !!this.location.page && this.location.page !== location.page;
}
/**
@@ -116,16 +114,25 @@ class DocDrawingThread extends DrawingThread {
* @return {void}
*/
saveAnnotation(type, text) {
- super.saveAnnotation(type, text);
+ this.emit('annotationevent', {
+ type: 'drawingcommit'
+ });
this.reset();
+ // Only make save request to server if there exist paths to save
+ const { undoCount } = this.pathContainer.getNumberOfItems();
+ if (undoCount === 0) {
+ return;
+ }
+
+ super.saveAnnotation(type, text);
+
const drawingAnnotationLayerContext = docAnnotatorUtil.getContext(
this.pageEl,
CLASS_ANNOTATION_LAYER_DRAW,
PAGE_PADDING_TOP,
PAGE_PADDING_BOTTOM
);
-
if (drawingAnnotationLayerContext) {
const inProgressCanvas = this.drawingContext.canvas;
const width = parseInt(inProgressCanvas.style.width, 10);
@@ -145,12 +152,6 @@ class DocDrawingThread extends DrawingThread {
return;
}
- // Get DrawingPath objects to be shown
- const drawings = this.getDrawings();
- if (this.pendingPath && !this.pendingPath.isEmpty()) {
- drawings.push(this.pendingPath);
- }
-
this.checkAndHandleScaleUpdate();
// Get the annotation layer context to draw with
@@ -158,8 +159,8 @@ class DocDrawingThread extends DrawingThread {
if (this.state === STATES.pending) {
context = this.drawingContext;
} else {
- this.pageEl = docAnnotatorUtil.getPageEl(this.annotatedElement, this.location.page);
const config = { scale: this.lastScaleFactor };
+ this.pageEl = docAnnotatorUtil.getPageEl(this.annotatedElement, this.location.page);
context = docAnnotatorUtil.getContext(
this.pageEl,
CLASS_ANNOTATION_LAYER_DRAW,
@@ -169,15 +170,15 @@ class DocDrawingThread extends DrawingThread {
this.setContextStyles(config, context);
}
- // Draw the paths to the annotation layer canvas
- if (context) {
- context.beginPath();
- drawings.forEach((drawing) => {
- drawing.generateBrowserPath(this.reconstructBrowserCoordFromLocation);
- drawing.drawPath(context);
- });
- context.stroke();
+ // Generate the paths and draw to the annotation layer canvas
+ this.pathContainer.applyToItems((drawing) =>
+ drawing.generateBrowserPath(this.reconstructBrowserCoordFromLocation)
+ );
+ if (this.pendingPath && !this.pendingPath.isEmpty()) {
+ this.pendingPath.generateBrowserPath(this.reconstructBrowserCoordFromLocation);
}
+
+ this.draw(context, false);
}
/**
@@ -206,6 +207,18 @@ class DocDrawingThread extends DrawingThread {
this.setContextStyles(config);
}
+ /**
+ * End the current drawing and emit a page changed event
+ *
+ * @return {void}
+ */
+ onPageChange() {
+ this.handleStop();
+ this.emit('annotationevent', {
+ type: 'pagechanged'
+ });
+ }
+
/**
* Requires a DocDrawingThread to have been started with DocDrawingThread.start(). Reconstructs a browserCoordinate
* relative to the dimensions of the DocDrawingThread page element.
diff --git a/src/lib/annotations/doc/__tests__/DocAnnotator-test.js b/src/lib/annotations/doc/__tests__/DocAnnotator-test.js
index a1a833e12..8f3a655f2 100644
--- a/src/lib/annotations/doc/__tests__/DocAnnotator-test.js
+++ b/src/lib/annotations/doc/__tests__/DocAnnotator-test.js
@@ -598,8 +598,9 @@ describe('lib/annotations/doc/DocAnnotator', () => {
Object.defineProperty(Annotator.prototype, 'bindCustomListenersOnThread', { value: bindFunc });
});
- it('should call parent to bind custom listeners and also bind on threaddeleted', () => {
+ it('should call parent to bind custom listeners and also bind highlights on threaddeleted', () => {
const thread = { addListener: () => {} };
+ sandbox.stub(annotatorUtil, 'isHighlightAnnotation').returns(true);
stubs.threadMock = sandbox.mock(thread);
stubs.threadMock.expects('addListener').withArgs('threaddeleted', sinon.match.func);
diff --git a/src/lib/annotations/doc/__tests__/DocDrawingThread-test.js b/src/lib/annotations/doc/__tests__/DocDrawingThread-test.js
index b9ac5e9e0..57a9f1230 100644
--- a/src/lib/annotations/doc/__tests__/DocDrawingThread-test.js
+++ b/src/lib/annotations/doc/__tests__/DocDrawingThread-test.js
@@ -1,6 +1,7 @@
import * as docAnnotatorUtil from '../docAnnotatorUtil';
import * as annotatorUtil from '../../annotatorUtil';
import DocDrawingThread from '../DocDrawingThread';
+import AnnotationThread from '../../AnnotationThread';
import DrawingPath from '../../drawing/DrawingPath';
import {
DRAW_STATES
@@ -84,24 +85,22 @@ describe('lib/annotations/doc/DocDrawingThread', () => {
it('should commit the thread when the page changes', () => {
sandbox.stub(docDrawingThread, 'hasPageChanged').returns(true);
sandbox.stub(docDrawingThread, 'checkAndHandleScaleUpdate');
- sandbox.stub(docDrawingThread, 'handleStop');
+ sandbox.stub(docDrawingThread, 'onPageChange');
sandbox.stub(docDrawingThread, 'saveAnnotation');
- docDrawingThread.drawingFlag = DRAW_STATES.idle;
docDrawingThread.pendingPath = undefined;
docDrawingThread.handleStart(docDrawingThread.location);
docDrawingThread.location = {};
expect(docDrawingThread.hasPageChanged).to.be.called;
- expect(docDrawingThread.handleStop).to.be.called;
- expect(docDrawingThread.saveAnnotation).to.be.called;
+ expect(docDrawingThread.onPageChange).to.be.called;
expect(docDrawingThread.checkAndHandleScaleUpdate).to.not.be.called;
- expect(docDrawingThread.drawingFlag).to.equal(DRAW_STATES.idle);
});
});
describe('handleStop()', () => {
it("should set the state to 'idle' and clear the pendingPath", () => {
+ sandbox.stub(docDrawingThread, 'emitAvailableActions');
docDrawingThread.drawingFlag = DRAW_STATES.drawing;
docDrawingThread.pendingPath = {
isEmpty: () => false
@@ -112,13 +111,27 @@ describe('lib/annotations/doc/DocDrawingThread', () => {
docDrawingThread.handleStop();
+ expect(docDrawingThread.emitAvailableActions).to.be.called;
expect(docDrawingThread.drawingFlag).to.equal(DRAW_STATES.idle);
expect(docDrawingThread.pendingPath).to.be.null;
});
});
+ describe('onPageChange()', () => {
+ it('should emit an annotationevent of type pagechanged and stop a pending drawing', (done) =>{
+ sandbox.stub(docDrawingThread, 'handleStop');
+ docDrawingThread.addListener('annotationevent', (data) => {
+ expect(docDrawingThread.handleStop).to.be.called;
+ expect(data.type).to.equal('pagechanged');
+ done();
+ });
+
+ docDrawingThread.onPageChange();
+ });
+ });
+
describe('checkAndHandleScaleUpdate()', () => {
- it('should update the scale factor when the scale has changed', () => {
+ it('should update the drawing information when the scale has changed', () => {
sandbox.stub(docDrawingThread, 'setContextStyles');
sandbox.stub(annotatorUtil, 'getScale').returns(1.4);
sandbox.stub(docAnnotatorUtil, 'getPageEl');
@@ -165,4 +178,134 @@ describe('lib/annotations/doc/DocDrawingThread', () => {
});
})
});
+
+ describe('saveAnnotation()', () => {
+ const resetValue = AnnotationThread.prototype.saveAnnotation;
+
+ beforeEach(() => {
+ Object.defineProperty(AnnotationThread.prototype, 'saveAnnotation', { value: sandbox.stub() });
+ });
+
+ afterEach(() => {
+ Object.defineProperty(AnnotationThread.prototype, 'saveAnnotation', { value: resetValue });
+ });
+
+ it('should clean up without committing when there are no paths to be saved', () => {
+ sandbox.stub(docDrawingThread.pathContainer, 'getNumberOfItems').returns({
+ undoCount: 0,
+ redoCount: 1
+ });
+
+ docDrawingThread.saveAnnotation('draw');
+ expect(docDrawingThread.pathContainer.getNumberOfItems).to.be.called;
+ expect(AnnotationThread.prototype.saveAnnotation).to.not.be.called;
+ });
+
+ it('should clean up and commit in-progress drawings when there are paths to be saved', () => {
+ docDrawingThread.drawingContext = {
+ canvas: {
+ style: {
+ width: 10,
+ height: 15
+ }
+ },
+ width: 20,
+ height: 30,
+ clearRect: sandbox.stub()
+ };
+ const context = {
+ drawImage: sandbox.stub()
+ };
+
+ sandbox.stub(docAnnotatorUtil, 'getContext').returns(context);
+ sandbox.stub(docDrawingThread.pathContainer, 'getNumberOfItems').returns({
+ undoCount: 1,
+ redoCount: 0
+ });
+
+ docDrawingThread.saveAnnotation('draw');
+ expect(docAnnotatorUtil.getContext).to.be.called;
+ expect(docDrawingThread.pathContainer.getNumberOfItems).to.be.called;
+ expect(docDrawingThread.drawingContext.clearRect).to.be.called;
+ expect(context.drawImage).to.be.called;
+ expect(AnnotationThread.prototype.saveAnnotation).to.be.called;
+ });
+ });
+
+ describe('hasPageChanged()', () => {
+ it('should return false when there is no location', () => {
+ const value = docDrawingThread.hasPageChanged(undefined);
+ expect(value).to.be.falsy;
+ });
+
+ it('should return false when there is a location but no page', () => {
+ const location = {
+ page: undefined
+ };
+ const value = docDrawingThread.hasPageChanged(location);
+ expect(value).to.be.falsy;
+ });
+
+ it('should return false when the given location page is the same as the thread location', () => {
+ docDrawingThread.location = {
+ page: 2
+ };
+ const location = {
+ page: docDrawingThread.location.page
+ };
+ const value = docDrawingThread.hasPageChanged(location);
+ expect(value).to.be.falsy;
+ });
+
+ it('should return true when the given location page is different from the thread location', () => {
+ docDrawingThread.location = {
+ page: 2
+ };
+ const location = {
+ page: (docDrawingThread.location.page + 1)
+ };
+ const value = docDrawingThread.hasPageChanged(location);
+ expect(value).to.be.true;
+ });
+ });
+
+ describe('show()', () => {
+ beforeEach(() => {
+ sandbox.stub(docAnnotatorUtil, 'getPageEl');
+ sandbox.stub(docAnnotatorUtil, 'getContext');
+ sandbox.stub(docDrawingThread, 'checkAndHandleScaleUpdate');
+ sandbox.stub(docDrawingThread, 'setContextStyles');
+ sandbox.stub(docDrawingThread, 'draw');
+ docDrawingThread.pathContainer = {
+ applyToItems: sandbox.stub()
+ };
+ });
+
+ it('should do nothing when no element is assigned to the DocDrawingThread', () => {
+ docDrawingThread.annotatedElement = undefined;
+ docDrawingThread.location = 'loc';
+ docDrawingThread.show();
+ expect(docDrawingThread.checkAndHandleScaleUpdate).to.not.be.called;
+ });
+
+ it('should do nothing when no location is assigned to the DocDrawingThread', () => {
+ docDrawingThread.annotatedElement = 'annotatedEl';
+ docDrawingThread.location = undefined;
+ docDrawingThread.show();
+ expect(docDrawingThread.checkAndHandleScaleUpdate).to.not.be.called;
+ });
+
+ it('should draw the paths in the thread', () => {
+ docDrawingThread.annotatedElement = 'annotatedEl';
+ docDrawingThread.location = 'loc';
+ docDrawingThread.state = 'not pending';
+
+ docDrawingThread.show()
+ expect(docAnnotatorUtil.getPageEl).to.be.called;
+ expect(docAnnotatorUtil.getContext).to.be.called;
+ expect(docDrawingThread.checkAndHandleScaleUpdate).to.be.called;
+ expect(docDrawingThread.setContextStyles).to.be.called;
+ expect(docDrawingThread.draw).to.be.called;
+ });
+ });
});
diff --git a/src/lib/annotations/drawing/DrawingContainer.js b/src/lib/annotations/drawing/DrawingContainer.js
new file mode 100644
index 000000000..9c7dc0d4f
--- /dev/null
+++ b/src/lib/annotations/drawing/DrawingContainer.js
@@ -0,0 +1,100 @@
+class DrawingContainer {
+ //--------------------------------------------------------------------------
+ // Typedef
+ //--------------------------------------------------------------------------
+
+ /**
+ * @typedef {Object} AvailableItemData
+ * @property {number} undo Number of undoable items
+ * @property {number} redo Number of redoable items
+ */
+
+ //--------------------------------------------------------------------------
+ // Public
+ //--------------------------------------------------------------------------
+
+ /** @property {Array} - The item history stack for undo operations */
+ undoStack = [];
+
+ /** @property {Array} - The item history stack for redo operations */
+ redoStack = [];
+
+ /**
+ * Insert an item into the drawing container. Clears any redoable items.
+ *
+ * @param {Object} item - An object to be contained in the data structure.
+ * @return {void}
+ */
+ insert(item) {
+ this.undoStack.push(item);
+ this.redoStack = [];
+ }
+
+ /**
+ * Move an item from the undo stack into the redo stack if an item exists.
+ *
+ * @return {boolean} Whether or not an undo was done.
+ */
+ undo() {
+ if (this.undoStack.length === 0) {
+ return false;
+ }
+
+ const latestUndone = this.undoStack.pop();
+ this.redoStack.push(latestUndone);
+ return true;
+ }
+
+ /**
+ * Move an item from the redo stack into the undo stack if an item exists.
+ *
+ * @return {boolean} Whether or not a redo was done.
+ */
+ redo() {
+ if (this.redoStack.length === 0) {
+ return false;
+ }
+
+ const latestRedone = this.redoStack.pop();
+ this.undoStack.push(latestRedone);
+ return true;
+ }
+
+ /**
+ * Retrieve a JSON blob containing the number of undo and redo in each stack.
+ *
+ * @return {AvailableItemData} The number of undo and redo items available.
+ */
+ getNumberOfItems() {
+ return {
+ undoCount: this.undoStack.length,
+ redoCount: this.redoStack.length
+ };
+ }
+
+ /**
+ * Retrieve the visible items on the undo stack.
+ *
+ * @return {Array} A copy of the undoStack as an array.
+ */
+ getItems() {
+ return this.undoStack.slice();
+ }
+
+ /**
+ * Apply a function to the items in the container.
+ *
+ * @param {Function} fn - The function to apply to the items.
+ * @param {boolean} [includeHiddenItems] - Whether or not to apply the function to items hidden on the redo stack.
+ * @return {void}
+ */
+ applyToItems(fn, includeHiddenItems = false) {
+ this.undoStack.forEach(fn);
+
+ if (includeHiddenItems) {
+ this.redoStack.forEach(fn);
+ }
+ }
+}
+
+export default DrawingContainer;
diff --git a/src/lib/annotations/drawing/DrawingThread.js b/src/lib/annotations/drawing/DrawingThread.js
index c4d27196f..01b0939b9 100644
--- a/src/lib/annotations/drawing/DrawingThread.js
+++ b/src/lib/annotations/drawing/DrawingThread.js
@@ -1,19 +1,16 @@
-import rbush from 'rbush';
import AnnotationThread from '../AnnotationThread';
import DrawingPath from './DrawingPath';
+import DrawingContainer from './DrawingContainer';
import { DRAW_STATES, DRAW_RENDER_THRESHOLD } from '../annotationConstants';
-const RTREE_WIDTH = 5; // Lower number - faster search, higher - faster insert
const BASE_LINE_WIDTH = 3;
class DrawingThread extends AnnotationThread {
/** @property {number} - Drawing state */
drawingFlag = DRAW_STATES.idle;
- /** @property {rbush} - Rtree path container */
- /* eslint-disable new-cap */
- pathContainer = new rbush(RTREE_WIDTH);
- /* eslint-enable new-cap */
+ /** @property {DrawingContainer} - The path container supporting undo and redo */
+ pathContainer = new DrawingContainer();
/** @property {DrawingPath} - The path being drawn but not yet finalized */
pendingPath;
@@ -45,6 +42,7 @@ class DrawingThread extends AnnotationThread {
this.handleMove = this.handleMove.bind(this);
this.handleStop = this.handleStop.bind(this);
+ // Recreate stored paths
if (data && data.location && data.location.drawingPaths instanceof Array) {
data.location.drawingPaths.forEach((drawingPathData) => {
const pathInstance = new DrawingPath(drawingPathData);
@@ -70,20 +68,11 @@ class DrawingThread extends AnnotationThread {
this.drawingContext.clearRect(0, 0, canvas.width, canvas.height);
}
- this.reset();
super.destroy();
+ this.reset();
this.emit('threadcleanup');
}
- /**
- * Get all of the DrawingPaths in the current thread.
- *
- * @return {void}
- */
- getDrawings() {
- return this.pathContainer.all();
- }
-
/* eslint-disable no-unused-vars */
/**
* Handle a pointer movement
@@ -153,27 +142,38 @@ class DrawingThread extends AnnotationThread {
}
const elapsed = timestamp - (this.lastRenderTimestamp || 0);
- if (elapsed < DRAW_RENDER_THRESHOLD || !this.drawingContext) {
- return;
+ if (elapsed >= DRAW_RENDER_THRESHOLD && this.draw(this.drawingContext, true)) {
+ this.lastRenderTimestamp = timestamp;
}
+ }
- this.lastRenderTimestamp = timestamp;
-
- const canvas = this.drawingContext.canvas;
- const drawings = this.getDrawings();
- if (this.pendingPath && !this.pendingPath.isEmpty()) {
- drawings.push(this.pendingPath);
+ /**
+ * Overturns the last drawing stroke if it exists. Emits the number of undo and redo
+ * actions available if an undo was executed.
+ *
+ * @return {void}
+ */
+ undo() {
+ const executedUndo = this.pathContainer.undo();
+ if (executedUndo) {
+ this.draw(this.drawingContext, true);
+ this.emitAvailableActions();
}
+ }
- /* OPTIMIZE (@minhnguyen): Render only what has been obstructed by the new drawing
- * rather than every single line in the thread. If we do end
- * up splitting saves into multiple requests, we can buffer
- * the amount of re-renders onto a temporary in-progress canvas.
- */
- this.drawingContext.clearRect(0, 0, canvas.width, canvas.height);
- this.drawingContext.beginPath();
- drawings.forEach((drawing) => drawing.drawPath(this.drawingContext));
- this.drawingContext.stroke();
+ /**
+ * Replays the last undone drawing stroke if it exists. Emits the number of undo and redo
+ * actions available if a redraw was executed.
+ *
+ * @return {void}
+ *
+ */
+ redo() {
+ const executedRedo = this.pathContainer.redo();
+ if (executedRedo) {
+ this.draw(this.drawingContext, true);
+ this.emitAvailableActions();
+ }
}
//--------------------------------------------------------------------------
@@ -191,11 +191,58 @@ class DrawingThread extends AnnotationThread {
*/
createAnnotationData(type, text) {
const annotation = super.createAnnotationData(type, text);
- const drawings = this.getDrawings();
+ const paths = this.pathContainer.getItems();
- annotation.location.drawingPaths = drawings.map(DrawingPath.extractDrawingInfo);
+ annotation.location.drawingPaths = paths.map(DrawingPath.extractDrawingInfo);
return annotation;
}
+
+ /**
+ * Draws the paths in the thread onto the given context.
+ *
+ * @protected
+ * @param {CanvasContext} context - The context to draw on
+ * @param {boolean} [clearCanvas] - A flag to clear the canvas before drawing.
+ * @return {void}
+ */
+ draw(context, clearCanvas = false) {
+ if (!context) {
+ return;
+ }
+
+ /* OPTIMIZE (@minhnguyen): Render only what has been obstructed by the new drawing
+ * rather than every single line in the thread. If we do end
+ * up splitting saves into multiple requests, we can buffer
+ * the amount of re-renders onto a temporary memory canvas.
+ */
+ if (clearCanvas) {
+ const canvas = context.canvas;
+ context.clearRect(0, 0, canvas.width, canvas.height);
+ }
+
+ context.beginPath();
+ this.pathContainer.applyToItems((drawing) => drawing.drawPath(context));
+ if (this.pendingPath && !this.pendingPath.isEmpty()) {
+ this.pendingPath.drawPath(context);
+ }
+
+ context.stroke();
+ }
+
+ /**
+ * Emit an event containing the number of undo and redo actions that can be done.
+ *
+ * @protected
+ * @return {void}
+ */
+ emitAvailableActions() {
+ const availableActions = this.pathContainer.getNumberOfItems();
+ this.emit('annotationevent', {
+ type: 'availableactions',
+ undo: availableActions.undoCount,
+ redo: availableActions.redoCount
+ });
+ }
}
export default DrawingThread;
diff --git a/src/lib/annotations/drawing/__tests__/DrawingContainer-test.js b/src/lib/annotations/drawing/__tests__/DrawingContainer-test.js
new file mode 100644
index 000000000..e8484bd30
--- /dev/null
+++ b/src/lib/annotations/drawing/__tests__/DrawingContainer-test.js
@@ -0,0 +1,118 @@
+import DrawingContainer from '../DrawingContainer';
+
+let drawingContainer;
+const sandbox = sinon.sandbox.create();
+
+describe('lib/annotations/drawing/DrawingContainer', () => {
+ before(() => {
+ fixture.setBase('src/lib');
+ });
+
+ beforeEach(() => {
+ drawingContainer = new DrawingContainer();
+ });
+
+ afterEach(() => {
+ sandbox.verifyAndRestore();
+ drawingContainer = null;
+ });
+
+ describe('insert()', () => {
+ it('should insert an item into the undoStack and clear the redo stack', () => {
+ drawingContainer.redoStack = [1,2,3];
+ expect(drawingContainer.undoStack.length).to.equal(0);
+ drawingContainer.insert(4);
+ expect(drawingContainer.undoStack.length).to.equal(1);
+ expect(drawingContainer.redoStack.length).to.equal(0);
+ });
+ });
+
+ describe('undo()', () => {
+ it('should not undo when the undo stack is empty', () => {
+ expect(drawingContainer.undoStack.length).to.equal(0);
+ const val = drawingContainer.undo();
+ expect(val).to.be.falsy;
+ });
+
+ it('should move an item from the top of the undo stack to the top of the redo stack', () => {
+ drawingContainer.undoStack = [1,2,3];
+ drawingContainer.redoStack = [4,5,6];
+ const lengthBefore = drawingContainer.undoStack.length;
+ const topUndo = drawingContainer.undoStack[lengthBefore - 1];
+ const val = drawingContainer.undo();
+
+ expect(val).to.be.truthy;
+ expect(drawingContainer.undoStack.length).to.equal(lengthBefore - 1);
+ expect(drawingContainer.redoStack.length).to.equal(lengthBefore + 1);
+ expect(drawingContainer.redoStack[lengthBefore]).to.equal(topUndo);
+ });
+ });
+
+ describe('redo()', () => {
+ it('should not redo when the redo stack is empty', () => {
+ expect(drawingContainer.redoStack.length).to.equal(0);
+ const val = drawingContainer.redo();
+ expect(val).to.be.falsy;
+ });
+
+ it('should move an item from the top of the redo stack to the top of the undo stack', () => {
+ drawingContainer.undoStack = [1,2,3];
+ drawingContainer.redoStack = [4,5,6];
+ const lengthBefore = drawingContainer.redoStack.length;
+ const topRedo = drawingContainer.redoStack[lengthBefore - 1];
+ const val = drawingContainer.redo();
+
+ expect(val).to.be.truthy;
+ expect(drawingContainer.redoStack.length).to.equal(lengthBefore - 1);
+ expect(drawingContainer.undoStack.length).to.equal(lengthBefore + 1);
+ expect(drawingContainer.undoStack[lengthBefore]).to.equal(topRedo);
+ });
+ });
+
+ describe('getNumberOfItems()', () => {
+ it('should return the number of items on the undo stack and redo stack', () => {
+ drawingContainer.undoStack = [1,2,3,4];
+ drawingContainer.redoStack = [1,2];
+ const val = drawingContainer.getNumberOfItems();
+
+ expect(val.undoCount).to.equal(drawingContainer.undoStack.length);
+ expect(val.redoCount).to.equal(drawingContainer.redoStack.length);
+ });
+ });
+
+ describe('getItems()', () => {
+ it('should get the items on the undoStack', () => {
+ drawingContainer.undoStack = [1,2,3,4];
+ drawingContainer.redoStack = [1,2];
+ const val = drawingContainer.getItems();
+
+ expect(val).to.not.deep.equal(drawingContainer.redoStack);
+ expect(val).to.not.equal(drawingContainer.undoStack);
+ expect(val).to.deep.equal(drawingContainer.undoStack);
+ });
+ });
+
+ describe('applyToItems()', () => {
+ it('should apply the function only to items on the undo stack', () => {
+ const counter = {
+ count: 0
+ };
+ drawingContainer.undoStack = [counter, counter, counter, counter];
+ drawingContainer.redoStack = [counter];
+ drawingContainer.applyToItems((item) => item.count = item.count + 1);
+
+ expect(counter.count).to.equal(drawingContainer.undoStack.length);
+ });
+
+ it('should apply the function to items on the undo and redo stack', () => {
+ const counter = {
+ count: 0
+ };
+ drawingContainer.undoStack = [counter, counter, counter, counter];
+ drawingContainer.redoStack = [counter, counter];
+ drawingContainer.applyToItems((item) => item.count = item.count + 1, true);
+
+ expect(counter.count).to.equal(drawingContainer.undoStack.length + drawingContainer.redoStack.length);
+ });
+ });
+});
diff --git a/src/lib/annotations/drawing/__tests__/DrawingThread-test.js b/src/lib/annotations/drawing/__tests__/DrawingThread-test.js
index 779a136bc..7d8fd80aa 100644
--- a/src/lib/annotations/drawing/__tests__/DrawingThread-test.js
+++ b/src/lib/annotations/drawing/__tests__/DrawingThread-test.js
@@ -5,6 +5,7 @@ import {
} from '../../annotationConstants'
let drawingThread;
+let stubs;
const sandbox = sinon.sandbox.create();
describe('lib/annotations/drawing/DrawingThread', () => {
@@ -13,6 +14,7 @@ describe('lib/annotations/drawing/DrawingThread', () => {
});
beforeEach(() => {
+ stubs = {};
drawingThread = new DrawingThread({
annotatedElement: document.querySelector('.annotated-element'),
annotations: [],
@@ -60,25 +62,6 @@ describe('lib/annotations/drawing/DrawingThread', () => {
})
});
- describe('getDrawings()', () => {
- it('should return all items inserted into the container', () => {
- drawingThread.pathContainer.insert('not a test');
- drawingThread.pathContainer.insert('not a secondary test');
-
- const allDrawings = drawingThread.getDrawings();
-
- assert.ok(allDrawings instanceof Array);
- assert.equal(allDrawings.length, 2);
- });
-
- it('should return an empty array when no items are inserted into the container', () => {
- const allDrawings = drawingThread.getDrawings();
-
- assert.ok(allDrawings instanceof Array);
- assert.equal(allDrawings.length, 0);
- })
- });
-
describe('setContextStyles()', () => {
it('should set configurable context properties', () => {
drawingThread.drawingContext = {
@@ -107,59 +90,165 @@ describe('lib/annotations/drawing/DrawingThread', () => {
});
describe('render()', () => {
+ beforeEach(() => {
+ sandbox.stub(drawingThread, 'draw');
+ });
+
it('should draw the pending path when the context is not empty', () => {
- const timeElapsed = 20000;
- const drawingArray = [];
+ const timeStamp = 20000;
+ drawingThread.render(timeStamp);
+ expect(drawingThread.draw).to.be.called;
+ });
+
+ it('should do nothing when the timeElapsed is less than the refresh rate', () => {
+ const timeStamp = 100;
+ drawingThread.lastRenderTimestamp = 100;
+ drawingThread.render(timeStamp);
+ expect(drawingThread.draw).to.not.be.called;
+ });
+ });
+
+ describe('createAnnotationData()', () => {
+ it('should create a valid annotation data object', () => {
+ const pathStr = 'path';
+ const path = {
+ map: sandbox.stub().returns(pathStr)
+ };
+ sandbox.stub(drawingThread.pathContainer, 'getItems').returns(path);
+ drawingThread.annotationService = {
+ user: { id: '1' }
+ };
+
+ const placeholder = "String here so string doesn't get fined";
+ const annotationData = drawingThread.createAnnotationData('draw', placeholder);
+
+ expect(drawingThread.pathContainer.getItems).to.be.called;
+ expect(path.map).to.be.called;
+ expect(annotationData.fileVersionId).to.equal(drawingThread.fileVersionId);
+ expect(annotationData.threadID).to.equal(drawingThread.threadID);
+ expect(annotationData.user.id).to.equal('1');
+ expect(annotationData.location.drawingPaths).to.equal(pathStr);
+ });
+ });
+
+ describe('undo()', () => {
+ beforeEach(() => {
+ stubs.draw = sandbox.stub(drawingThread, 'draw');
+ stubs.emitAvailableActions = sandbox.stub(drawingThread, 'emitAvailableActions');
+ stubs.containerUndo = sandbox.stub(drawingThread.pathContainer, 'undo');
+ });
- sandbox.stub(drawingThread, 'getDrawings')
- .returns(drawingArray);
+ it('should do nothing when the path container fails to undo', () => {
+ stubs.containerUndo.returns(false);
+ drawingThread.undo();
+ expect(stubs.containerUndo).to.be.called;
+ expect(stubs.draw).to.not.be.called;
+ expect(stubs.emitAvailableActions).to.not.be.called;
+ });
+
+ it('should draw when the path container indicates a successful undo', () => {
+ stubs.containerUndo.returns(true);
+ drawingThread.undo();
+ expect(stubs.containerUndo).to.be.called;
+ expect(stubs.draw).to.be.called;
+ expect(stubs.emitAvailableActions).to.be.called;
+ });
+ });
+
+ describe('redo()', () => {
+ beforeEach(() => {
+ stubs.draw = sandbox.stub(drawingThread, 'draw');
+ stubs.emitAvailableActions = sandbox.stub(drawingThread, 'emitAvailableActions');
+ stubs.containerRedo = sandbox.stub(drawingThread.pathContainer, 'redo');
+ });
+
+ it('should do nothing when the path container fails to redo', () => {
+ stubs.containerRedo.returns(false);
+ drawingThread.redo();
+ expect(stubs.containerRedo).to.be.called;
+ expect(stubs.draw).to.not.be.called;
+ expect(stubs.emitAvailableActions).to.not.be.called;
+ });
+ it('should draw when the path container indicates a successful redo', () => {
+ stubs.containerRedo.returns(true);
+ drawingThread.redo();
+ expect(stubs.containerRedo).to.be.called;
+ expect(stubs.draw).to.be.called;
+ expect(stubs.emitAvailableActions).to.be.called;
+ });
+ });
+
+ describe('draw()', () => {
+ let context;
+
+ beforeEach(() => {
drawingThread.pendingPath = {
- drawPath: sandbox.stub(),
- isEmpty: sandbox.stub().returns(false)
+ isEmpty: sandbox.stub(),
+ drawPath: sandbox.stub()
};
- drawingThread.drawingContext = {
+ stubs.applyToItems = sandbox.stub(drawingThread.pathContainer, 'applyToItems');
+ stubs.pendingEmpty = drawingThread.pendingPath.isEmpty;
+ stubs.pendingDraw = drawingThread.pendingPath.drawPath;
+ context = {
+ clearRect: sandbox.stub(),
beginPath: sandbox.stub(),
stroke: sandbox.stub(),
- clearRect: sandbox.stub(),
canvas: {
- width: 2,
+ width: 1,
height: 2
}
};
- drawingThread.render(timeElapsed);
+ });
- expect(drawingThread.getDrawings).to.be.called;
- expect(drawingThread.drawingContext.clearRect).to.be.called;
- expect(drawingThread.drawingContext.beginPath).to.be.called;
- expect(drawingThread.drawingContext.stroke).to.be.called;
- expect(drawingThread.pendingPath.drawPath).to.be.called;
- expect(drawingThread.pendingPath.isEmpty).to.be.called;
+ it('should do nothing when context is null or undefined', () => {
+ context = undefined;
+ drawingThread.draw(context);
+ context = null;
+ drawingThread.draw(context);
+ expect(stubs.applyToItems).to.not.be.called;
});
- it('should do nothing when the context is empty', () => {
- const timeElapsed = 20000;
+ it('should draw the items in the path container when given a valid context', () => {
+ stubs.pendingEmpty.returns(false);
+ drawingThread.draw(context);
+ expect(context.beginPath).to.be.called;
+ expect(stubs.applyToItems).to.be.called;
+ expect(stubs.pendingEmpty).to.be.called;
+ expect(stubs.pendingDraw).to.be.called;
+ expect(context.stroke).to.be.called;
+ });
- sandbox.stub(drawingThread, 'getDrawings');
- drawingThread.context = null;
- drawingThread.render(timeElapsed);
+ it('should clear the canvas when the flag is true', () => {
+ drawingThread.draw(context, true);
+ expect(context.clearRect).to.be.called;
+ });
- expect(drawingThread.getDrawings).to.not.be.called;
+ it('should not clear the canvas when the flag is true', () => {
+ drawingThread.draw(context, false);
+ expect(context.clearRect).to.not.be.called;
});
});
- describe('createAnnotationData()', () => {
- it('should create a valid annotation data object', () => {
- drawingThread.annotationService = {
- user: { id: '1' }
- };
+ describe('emitAvailableActions()', () => {
+ afterEach(() => {
+ drawingThread.removeAllListeners('annotationevent');
+ });
- const placeholder = "String here so string doesn't get fined";
- const annotationData = drawingThread.createAnnotationData('draw', placeholder);
+ it('should trigger an annotationevent with the number of available undo and redo actions', (done) => {
+ const numItems = {
+ undoCount: 3,
+ redoCount: 2
+ };
+ sandbox.stub(drawingThread.pathContainer, 'getNumberOfItems').returns(numItems);
+ drawingThread.addListener('annotationevent', (data) => {
+ expect(data.type).to.equal('availableactions');
+ expect(data.undo).to.equal(numItems.undoCount);
+ expect(data.redo).to.equal(numItems.redoCount);
+ done();
+ });
- expect(annotationData.fileVersionId).to.equal(drawingThread.fileVersionId);
- expect(annotationData.threadID).to.equal(drawingThread.threadID);
- expect(annotationData.user.id).to.equal('1');
+ drawingThread.emitAvailableActions();
});
});
});
diff --git a/src/lib/shell.html b/src/lib/shell.html
index f4f92d267..ede8f51c5 100644
--- a/src/lib/shell.html
+++ b/src/lib/shell.html
@@ -7,6 +7,16 @@