I'm having a problem with an <audio>
element's timeupdate
event in jQuery (I'm using version 1.7.1). My goal is to bind a function to it, then unbind it, then bind it again. It's not behaving the way I expect it to in Firefox, Google Chrome, and Internet Explorer 10. Firefox and Google Chrome behave the same, but Internet Explorer 10 behaves differently (surprise!). I'd, of course, like it to work in all three.
I think the problem is best described with the code itself (the specifics of the problem are in the HTML's second <p>
):
HTML
<p>Click the circle on the right whose color matches the circle on the left. Do this four times.</p>
<p>Clicking the incorrect color will play audio. As soon as the singing starts, a message is logged to the console. This works the first time, but after that, it starts happening twice.</p>
<div class="goal"></div>
<div class="choices"></div>
<audio id="audio-try_again" preload="auto">
<source src="try_again.ogg">
<source src="try_again.mp3">
</audio>
CSS
.goal, .choices {
float: left;
width: 50%;
}
JavaScript
$.randomize = function(arr) {
for (var j, x, i = arr.length; i; j = parseInt(Math.random() * i, 10), x = arr[--i], arr[i] = arr[j], arr[j] = x);
return arr;
};
function factorial(num) {
var rval = 1;
for (var i = 2; i <= num; i++) {
rval = rval * i;
}
return rval;
}
function generate_objects() {
this_pair = get_object_pair();
if (used_pairs.length < total_permutations) {
while ($.inArray(this_pair, used_pairs) > -1) {
this_pair = get_object_pair();
}
}
else {
used_pairs = [];
}
used_pairs.push(this_pair);
objects = this_pair.split('|');
object_to_find = objects[0];
other_object = objects[1];
$('.goal').html('<a href="javascript:void(0);"><img src="' + images[object_to_find] + '" alt=""></a>');
$('.choices').html('');
choices = [
$('<a href="javascript:void(0);" class="correct"><img src="' + images[object_to_find] + '" alt=""></a>'),
$('<a href="javascript:void(0);" class="incorrect"><img src="' + images[other_object] + '" alt=""></a>')
];
$.randomize(choices);
$.each(choices, function(index, value) {
$('.choices').append(value);
});
$('#audio-try_again').on('timeupdate', function() {
if (try_again_audio_element.currentTime >= 5) {
$(this).off('timeupdate');
console.log('Highlight the correct image.');
$('.correct').animate({opacity: 0.25}, 200, function() {
$(this).animate({opacity: 1}, 200, function() {
$('.correct').animate({opacity: 0.25}, 200, function() {
$(this).animate({opacity: 1}, 200);
});
});
});
}
});
$('.goal, .choices').fadeIn();
}
function get_object_pair() {
object_to_find = other_object = get_random_index();
while (other_object == object_to_find) {
other_object = get_random_index();
}
return object_to_find + '|' + other_object;
}
function get_random_index() {
return Math.floor(Math.random() * total_images);
}
function question_answered() {
if (correct_answers == 4) {
alert('You have correctly answered four questions.');
}
else {
$('.goal, .choices').fadeOut(function() {
$('#audio-try_again').off('timeupdate');
setTimeout(generate_objects, 200);
});
}
}
$(document).ready(function() {
correct_answers = 0;
images = [
'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAcUlEQVR42qXT0QnAIAxFUUfJNJlOp+gwWUfIAOkTUpBAC/V9nI9KuKDV1iIqhQEGM1muaZ3fPwQ6OMQLzxmpAYGrDH+5QPZAh/ipPwEFPwg4aMvDiUNjBYwI2ApMIjDpAL0F+hDp30heJP4q84+Jfs43txnFT7FsW/cAAAAASUVORK5CYII=',
'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAcUlEQVR42qWT0QnAIAxEHSXTZDqdosNknYADpCekIJZW2kPeh3I8TIwl7ktBAwY8sTzTNT1vBFTQQTzQMyOrQMCRoS2ZlVlQQXykXgJ9u/amHC3ZnPhJGwIjBDYETgicFtAl0E2kn5EcJH6U+c9Ef+cTtijFT/BuGVoAAAAASUVORK5CYII=',
'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAcUlEQVR42qWT0QnAIAxEHSXTOF2dosNkHcEB0hMOwUAp9j7eh3J5YExKRMlU0ICDTpx3NWW3g4ELDBAvDGYsCwzcDH3CrC0BrXECa9abx6mANbWwOfGTNgUuCHwKuiDoskB+gtxE+RvFQdJHWV8meZ0ftTfFT731dloAAAAASUVORK5CYII=',
'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAa0lEQVR42qXT0QnAIAwE0IziNE6XTOEwWSeQAex9hFKKbaknvB+JB1Eji9XBwCGK116Xl9VAIWE+yKppq8PjLPw27iEK8ye99pwbAVlnxWBuMhAnAhwkiICgA+gW6Eukn5H8SPxX5oeJHucDimXHMWd8ov0AAAAASUVORK5CYII='
];
total_images = images.length;
total_permutations = factorial(total_images) / factorial(total_images - 2);
try_again_audio_element = $('#audio-try_again')[0];
$(document).on('click', '.correct', function() {
if (!$(this).hasClass('disabled')) {
$('.correct, .incorrect').addClass('disabled');
correct_answers++;
question_answered();
}
}).on('click', '.incorrect', function() {
if (!$(this).hasClass('disabled')) {
$('.correct, .incorrect').addClass('disabled');
try_again_audio_element.play();
}
});
$('#audio-try_again').on('ended', function() {
question_answered();
});
used_pairs = [];
generate_objects();
});
Demo: http://jsfiddle.net/j8LXE/
Any ideas?
Looks like the problem is this chunk:
$('.goal, .choices').fadeOut(function() {
$('#audio-try_again').off('timeupdate');
setTimeout(generate_objects, 200);
});
The selector $('.goal, .choices')
will return a jQuery collection containing two elements, which means the callback function is invoked two times (once per matched element).
A quick and dirty way to overcome this, for your specific application, would be:
$('.goal, .choices').fadeOut(function() {
if ( $(this).is('.goal') ) return;
$('#audio-try_again').off('timeupdate');
setTimeout(generate_objects, 200);
});
Which will, essentially, only execute the stuff inside that callback once.