diff --git a/common/lib/capa/capa/templates/imageinput.html b/common/lib/capa/capa/templates/imageinput.html
index 394f180d3e93..77977a8d1221 100644
--- a/common/lib/capa/capa/templates/imageinput.html
+++ b/common/lib/capa/capa/templates/imageinput.html
@@ -1,24 +1,60 @@
-
-
+
- % if status == 'unsubmitted':
-
- Status: unanswered
-
- % elif status == 'correct':
-
- Status: correct
-
- % elif status == 'incorrect':
-
- Status: incorrect
-
- % elif status == 'incomplete':
-
- Status: incorrect
-
- % endif
+
+
+
+
+ % if status == 'unsubmitted':
+
+ Status: unanswered
+
+ % elif status == 'correct':
+
+ Status: correct
+
+ % elif status == 'incorrect':
+
+ Status: incorrect
+
+ % elif status == 'incomplete':
+
+ Status: incorrect
+
+ % endif
diff --git a/common/lib/xmodule/xmodule/js/fixtures/imageinput.html b/common/lib/xmodule/xmodule/js/fixtures/imageinput.html
new file mode 100644
index 000000000000..00a109473354
--- /dev/null
+++ b/common/lib/xmodule/xmodule/js/fixtures/imageinput.html
@@ -0,0 +1,31 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Status: unanswered
+
+
diff --git a/common/lib/xmodule/xmodule/js/spec/capa/imageinput_spec.js b/common/lib/xmodule/xmodule/js/spec/capa/imageinput_spec.js
new file mode 100644
index 000000000000..d539108a88a9
--- /dev/null
+++ b/common/lib/xmodule/xmodule/js/spec/capa/imageinput_spec.js
@@ -0,0 +1,145 @@
+/**
+ * "Beware of bugs in the above code; I have only proved it correct, not tried
+ * it."
+ *
+ * ~ Donald Knuth
+ */
+
+(function ($, ImageInput, undefined) {
+ describe('ImageInput', function () {
+ var state;
+
+ beforeEach(function () {
+ var el;
+
+ loadFixtures('imageinput.html');
+ el = $('#imageinput_12345');
+
+ el.append(createTestImage('cross_12345', 300, 400, 'red'));
+
+ state = new ImageInput('12345');
+
+ spyOn(state, 'clickHandler').andCallThrough();
+ });
+
+ it('initialization', function () {
+ expect(state.elementId).toBe('12345');
+
+ // Check that object's properties are present, and that the DOM
+ // elements they reference exist.
+ expect(state.el).toBeDefined();
+ expect(state.el).toExist();
+
+ expect(state.crossEl).toBeDefined();
+ expect(state.crossEl).toExist();
+
+ expect(state.inputEl).toBeDefined();
+ expect(state.inputEl).toExist();
+
+ // Check that the click handler has been attached to the `state.el`
+ // element. Note that `state.clickHandler()` method is called from
+ // within the attached handler. That is why we can't use
+ // Jasmine-jQuery `toHandleWith()` method.
+ state.el.click();
+ expect(state.clickHandler).toHaveBeenCalled();
+ });
+
+ it('cross becomes visible after first click', function () {
+ expect(state.crossEl.css('visibility')).toBe('hidden');
+
+ state.el.click();
+
+ expect(state.crossEl.css('visibility')).toBe('visible');
+ });
+
+ it('coordinates are updated [offsetX is set]', function () {
+ var event, posX, posY, cssLeft, cssTop;
+
+ // Set up of 'click' event.
+ event = jQuery.Event(
+ 'click',
+ {offsetX: 35.3, offsetY: 42.7}
+ );
+
+ // Calculating the expected coordinates.
+ posX = event.offsetX;
+ posY = event.offsetY;
+
+ // Triggering 'click' event.
+ jQuery(state.el).trigger(event);
+
+ // Getting actual (new) coordinates, and testing them against the
+ // expected.
+ cssLeft = stripPx(state.crossEl.css('left'));
+ cssTop = stripPx(state.crossEl.css('top'));
+
+ expect(cssLeft).toBeCloseTo(posX - 15, 1);
+ expect(cssTop).toBeCloseTo(posY - 15, 1);
+ expect(state.inputEl.val()).toBe(
+ '[' + Math.round(posX) + ',' + Math.round(posY) + ']'
+ );
+ });
+
+ it('coordinates are updated [offsetX is NOT set]', function () {
+ var event, posX, posY, cssLeft, cssTop;
+
+ // Set up of 'click' event.
+ event = jQuery.Event(
+ 'click',
+ {
+ offsetX: undefined, offsetY: undefined,
+ pageX: 35.3, pageY: 42.7
+ }
+ );
+ state.el[0].offsetLeft = 12;
+ state.el[0].offsetTop = 3;
+
+ // Calculating the expected coordinates.
+ posX = event.pageX - state.el[0].offsetLeft;
+ posY = event.pageY - state.el[0].offsetTop;
+
+ // Triggering 'click' event.
+ jQuery(state.el).trigger(event);
+
+ // Getting actual (new) coordinates, and testing them against the
+ // expected.
+ cssLeft = stripPx(state.crossEl.css('left'));
+ cssTop = stripPx(state.crossEl.css('top'));
+
+ expect(cssLeft).toBeCloseTo(posX - 15, 1);
+ expect(cssTop).toBeCloseTo(posY - 15, 1);
+ expect(state.inputEl.val()).toBe(
+ '[' + Math.round(posX) + ',' + Math.round(posY) + ']'
+ );
+ });
+ });
+
+ // Instead of storing an image, and then including it in the template via
+ // the
tag, we will generate one on the fly.
+ //
+ // Create a simple image from a canvas. The canvas is filled by a colored
+ // rectangle.
+ function createTestImage(id, width, height, fillStyle) {
+ var canvas, ctx, img;
+
+ canvas = document.createElement('canvas');
+ canvas.width = width;
+ canvas.height = height;
+
+ ctx = canvas.getContext('2d');
+ ctx.fillStyle = fillStyle;
+ ctx.fillRect(0, 0, width, height);
+
+ img = document.createElement('img');
+ img.src = canvas.toDataURL('image/png');
+ img.id = id;
+
+ return img;
+ }
+
+ // Strip the trailing 'px' substring from a CSS string containing the
+ // `left` and `top` properties of an element's style.
+ function stripPx(str) {
+ return str.substring(0, str.length - 2);
+ }
+}).call(this, window.jQuery, window.ImageInput);
diff --git a/common/lib/xmodule/xmodule/js/src/capa/imageinput.js b/common/lib/xmodule/xmodule/js/src/capa/imageinput.js
index 518cd91bba95..d5df6e2ba61d 100644
--- a/common/lib/xmodule/xmodule/js/src/capa/imageinput.js
+++ b/common/lib/xmodule/xmodule/js/src/capa/imageinput.js
@@ -13,24 +13,49 @@
* ~ Chinese Proverb
*/
-window.image_input_click = function (id, event) {
- var iiDiv = document.getElementById('imageinput_' + id),
+window.ImageInput = (function ($, undefined) {
+ var ImageInput = ImageInputConstructor;
- posX = event.offsetX ? event.offsetX : event.pageX - iiDiv.offsetLeft,
- posY = event.offsetY ? event.offsetY : event.pageY - iiDiv.offsetTop,
+ ImageInput.prototype = {
+ constructor: ImageInputConstructor,
+ clickHandler: clickHandler
+ };
- cross = document.getElementById('cross_' + id),
+ return ImageInput;
- // To reduce differences between values returned by different kinds of
- // browsers, we round `posX` and `posY`.
- //
- // IE10: `posX` and `posY` - float.
- // Chrome, FF: `posX` and `posY` - integers.
- result = '[' + Math.round(posX) + ',' + Math.round(posY) + ']';
+ function ImageInputConstructor(elementId) {
+ var _this = this;
- cross.style.left = (posX - 15) + 'px';
- cross.style.top = (posY - 15) + 'px';
- cross.style.visibility = 'visible';
+ this.elementId = elementId;
- document.getElementById('input_' + id).value = result;
-};
+ this.el = $('#imageinput_' + this.elementId);
+ this.crossEl = $('#cross_' + this.elementId);
+ this.inputEl = $('#input_' + this.elementId);
+
+ this.el.on('click', function (event) {
+ _this.clickHandler(event);
+ });
+ }
+
+ function clickHandler(event) {
+ var posX = event.offsetX ?
+ event.offsetX : event.pageX - this.el[0].offsetLeft,
+ posY = event.offsetY ?
+ event.offsetY : event.pageY - this.el[0].offsetTop,
+
+ // To reduce differences between values returned by different kinds
+ // of browsers, we round `posX` and `posY`.
+ //
+ // IE10: `posX` and `posY` - float.
+ // Chrome, FF: `posX` and `posY` - integers.
+ result = '[' + Math.round(posX) + ',' + Math.round(posY) + ']';
+
+ this.crossEl.css({
+ left: posX - 15,
+ top: posY - 15,
+ visibility: 'visible'
+ });
+
+ this.inputEl.val(result);
+ }
+}).call(this, window.jQuery);