Skip to content
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

Improvement and update for ImageInput #1947

Merged
merged 3 commits into from
Dec 17, 2013
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
78 changes: 57 additions & 21 deletions common/lib/capa/capa/templates/imageinput.html
Original file line number Diff line number Diff line change
@@ -1,24 +1,60 @@
<span>
<input type="hidden" class="imageinput" src="${src}" name="input_${id}" id="input_${id}" value="${value}" />
<div id="imageinput_${id}" onclick="image_input_click('${id}',event);" style = "background-image:url('${src}');width:${width}px;height:${height}px;position: relative; left: 0; top: 0;">
<img src="${STATIC_URL}green-pointer.png" id="cross_${id}" style="position: absolute;top: ${gy}px;left: ${gx}px;" />
</div>
<input
type="hidden"
class="imageinput"
src="${src}"
name="input_${id}"
id="input_${id}"
value="${value}"
/>

% if status == 'unsubmitted':
<span class="unanswered" style="display:inline-block;" id="status_${id}" aria-describedby="input_${id}">
<span class="sr">Status: unanswered</span>
</span>
% elif status == 'correct':
<span class="correct" id="status_${id}" aria-describedby="input_${id}">
<span class="sr">Status: correct</span>
</span>
% elif status == 'incorrect':
<span class="incorrect" id="status_${id}" aria-describedby="input_${id}">
<span class="sr">Status: incorrect</span>
</span>
% elif status == 'incomplete':
<span class="incorrect" id="status_${id}" aria-describedby="input_${id}">
<span class="sr">Status: incorrect</span>
</span>
% endif
<div
id="imageinput_${id}"
style="background-image: url('${src}'); width: ${width}px; height: ${height}px; position: relative; left: 0; top: 0;"
>
<img
src="${STATIC_URL}green-pointer.png"
id="cross_${id}"
style="position: absolute; top: ${gy}px; left: ${gx}px;"
/>
</div>

<script type="text/javascript" charset="utf-8">
(new ImageInput('${id}'));
</script>

% if status == 'unsubmitted':
<span
class="unanswered"
style="display: inline-block;"
id="status_${id}"
aria-describedby="input_${id}"
>
<span class="sr">Status: unanswered</span>
</span>
% elif status == 'correct':
<span
class="correct"
id="status_${id}"
aria-describedby="input_${id}"
>
<span class="sr">Status: correct</span>
</span>
% elif status == 'incorrect':
<span
class="incorrect"
id="status_${id}"
aria-describedby="input_${id}"
>
<span class="sr">Status: incorrect</span>
</span>
% elif status == 'incomplete':
<span
class="incorrect"
id="status_${id}"
aria-describedby="input_${id}"
>
<span class="sr">Status: incorrect</span>
</span>
% endif
</span>
31 changes: 31 additions & 0 deletions common/lib/xmodule/xmodule/js/fixtures/imageinput.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<!-- ${id} = 12345 -->
<!-- ${width} = 300 -->
<!-- ${height} = 400 -->

<span>
<input
type="hidden"
class="imageinput"
src=""
name="input_12345"
id="input_12345"
value=""
/>

<div
id="imageinput_12345"
style="width: 300px; height: 400px; position: relative; left: 0; top: 0; visibility: hidden;"
>
<!-- image will go here -->
</div>

<!-- status == 'unsubmitted' -->
<span
class="unanswered"
style="display: inline-block;"
id="status_12345"
aria-describedby="input_12345"
>
<span class="sr">Status: unanswered</span>
</span>
</span>
145 changes: 145 additions & 0 deletions common/lib/xmodule/xmodule/js/spec/capa/imageinput_spec.js
Original file line number Diff line number Diff line change
@@ -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 <img /> 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);
57 changes: 41 additions & 16 deletions common/lib/xmodule/xmodule/js/src/capa/imageinput.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);