Can't find the bug - too many firings - javascript

(edited some spelling errors)
So here's a piece of code that repeats itself with every answer the user gives to an MPC-question:
It fires the question, generates 4 answers, binds 'click' and 'mouseover' to the answers, and waits for the user to actually click on one of them.
When he does, it checks if it was right or wrong, displays that to the user, and then waits for another input (anywhere in the document, this time) before it proceeds and repeats.
Now, the idea is that a user can either click with the cursor, OR use QWER to answer. And as I'm am quite new to manipulation of an event and its data, I found a (flawed, apparently) way to separate the two, as you'll find below.
However, when a user 'answers' by clicking, and 'proceeds' by QWER, it waltzes through the whole thing too fast, by actually firing the question as intended, and then immediately answering it AND firing another question AGAIN. (And after extracting the time it takes for this into an array, I found that it sometimes does this 3-6 times in a row, with no apparent reason for this how many times at all...)
Now, as mentioned, I am new when it comes to events, so there may be redundancies and/or wrong uses of (e), but bear with me. I expect the bug to be related to this, of course. Also, some functions are irrelevant here, because they lead back to whatever function their call is in (right(); for example, does not fire anything after itself).
It's the loop that's the point here.
Thanks in advance!
Here's the code:
function keyAns() {
answered="no";
draw(0);
}
function keyNotAns (A) {
B = $(A).children('.answer');
if ($(B).hasClass('right')) {
// do something
}
else if ($(B).hasClass('wrong')) {
// do something
}
answered = "yes";
}
function waitForInput() {
// MOUSE CLICK
$(document).click(function() {
if (answered == "yes") {
answered = "no";
draw(0);
}
});
$('.answer').click(function(e) {
$('.answer').unbind('click');
if (answered == "no") {
calcTime(1);
e.stopPropagation();
if ($(this).hasClass('right')) {
// do something
}
else if ($(this).hasClass('wrong')) {
// do something
}
answered = "yes";
}
});
// KEYPRESSES
$(document).bind('keyup', function(e){
$(document).unbind('keyup');
if (answered == "yes") {
e.stopPropagation();
keyAns();
}
else if ( answered == "no") {
calcTime(1); // irrelevant
if(e.which == 81 || e.keyCode == 81) { // Q
AAA = '#ansQ';
e.stopPropagation();
keyNotAns(AAA);
}
else if(e.which == 87 || e.keyCode == 87) { // W
AAA = '#ansW';
e.stopPropagation();
keyNotAns(AAA);
}
else if(e.which == 69 || e.keyCode == 69) { // E
AAA = '#ansE';
e.stopPropagation();
keyNotAns(AAA);
}
else if(e.which == 82 || e.keyCode == 82) { // R
AAA = '#ansR';
e.stopPropagation();
keyNotAns(AAA);
}
else {
}
waitForInput();
}
});
}

Every time that there's a keypress event, you rebind everything.
This means that if you push 10 keys, you will have 10 onclick listeners. So when a user then clicks, the callback will run 10 times in a row.
The code that you posted doesn't include the original call to the waitForInput function, but you only need to call it once, so you can delete it from this code.
A quick introduction to jquery events:
When you bind, every time that event occurs (on the element you put it to), the callback function you provided will run.
Another tip is that in more recent versions of jQuery, there in an alternative to bind named one. It does the same, but it will only run the first time. Although in this situation, you don't need it.

Your problem is that inside your keybind, you're calling waitForInput.
Asynchronous programming takes some getting used to, but what the functions do inside waitForInput is set up event listeners, and any time the event happens, those listeners fire.
The problem you're seeing is that after handling an event, you're adding more event listeners, and next time the event fires, the listener will fire multiple times.
Simply take the waitForInput() line out of the function, and put it at the bottom of your code. Then it will run only once, and you'll be fine. (It wouldn't hurt to rename it to something like setupEventListeners, to avoid confusion.)

Found it!
All thanks to Scott Mermelstein and ColBeseder, both equally invaluable to my solution! Not only did they find the flaws, they made me understand how events and binds work. So thanks a million, you two!
Indeed:
binds were being stacked, therefore IF it fired, it would fire excessively, also;
the self-call of waitForinput() was indeed unneeded, however;
one e.stopPropagation() was also needed, to prevent the loop from working with wrong event-data, which lead the function to interpret it as the next answer and fire itself again.
Plus: due to laziness - I didn't feel like too much trial and error - I wanted to stay on the safe side, so I added all unbind()'s as first-thing when the function fires.
The result, which works flawlessly, for those who're interested:
function keyNotAns (A) {
B = $(A).children('.answer');
if ($(B).hasClass('right')) {
// do something
}
else if ($(B).hasClass('wrong')) {
// do something
}
answered = "yes";
}
function waitForInput() {
$(document).unbind('keyup');
$(document).unbind('click');
$('.answer').unbind('click');
// MOUSE CLICKS
$(document).click(function() {
if (answered == "yes") {
answered = "no";
draw(0);
}
});
$('.answer').click(function(e) {
if (answered == "no") {
calcTime(1);
if ($(this).hasClass('right')) {
// do something
}
else if ($(this).hasClass('wrong')) {
// do something
}
e.stopPropagation();
answered = "yes";
}
});
// KEYPRESSES
$(document).bind('keyup', function(e){
if (answered == "yes") {
answered="no";
draw(0);
}
else if (answered == "no") {
if(e.which == 81 || e.keyCode == 81) { // Q
keyNotAns('#ansQ');
}
else if(e.which == 87 || e.keyCode == 87) { // W
keyNotAns('#ansW');
}
else if(e.which == 69 || e.keyCode == 69) { // E
keyNotAns('#ansE');
}
else if(e.which == 82 || e.keyCode == 82) { // R
keyNotAns('#ansR');
}
}
});
}

There are two ways to find such bugs. The first one is using the debugger - which can be pretty tedious when you have loops.
The second approach is logging: Write a report / log of all the important actions that you can read later to find out why it failed at some point in the past.
Have a look at console.log() or a JavaScript logging framework.

Related

Function starts being repeated over and over again

I've been developing a simple system that is supposed to change between two different scenes when you press a button.
gameOne();
var game = 1;
function gameOne() {
game = 1;
console.log("Game 1");
$( "body" ).keyup(function( event ) {
if ( event.which == 49 && game == 1) { // Number 1 key
gameTwo();
}
});
}
function gameTwo() {
game = 2;
console.log("Game 2");
$( "body" ).keyup(function( event ) {
if ( event.which == 49 && game == 2) { // Number 1 key
gameOne();
}
});
}
Expected behaviour - I want it to say Game 1, after after pressing the 1 key and then Game 2 after pressing the 1 key again, and then repeat this as I press 1.
Actual behaviour - It does the expected behaviour a few times, and then it starts repeating 1 and 2 over and over again, to the point it lags the browser out.
JSFiddle: https://jsfiddle.net/a0npotm8/10/
I'm really sorry if this is a basic question or anything, I'm still fairly new to Javascript and JQuery and this is really confusing me currently.
All help is appreciated.
Thank you :)
The problem here is that you are rebinding the keyup event recuresively inside the keyup callback, so it ends up by breaking the browser.
What you need to do is to get the keyup binding code out of the two functions:
gameOne();
var game = 1;
$("body").keyup(function(event) {
if (event.which == 49 && game == 1) { // Number 1 key
gameTwo();
} else if (event.which == 49 && game == 2) { // Number 1 key
gameOne();
}
});
function gameOne() {
game = 1;
console.log("Game 1");
}
function gameTwo() {
game = 2;
console.log("Game 2");
}
what about something like:
let game = 1;
document.onkeyup = ev => {
if (ev.which === 49) {
console.log(`Game ${game}`);
game = game === 1 ? 2 : 1;
}
};
will it solve your issue?
You can use a delegate event handler to control actions like this, so you do not have to juggle event bindings around.
var $container = $('#container').focus();
$(document.body)
.on('keyup', '#container.game1', function(e){
if (e.which == 49) {
console.log('Game 1');
$container.removeClass('game1').addClass('game2');
}
})
.on('keyup', '#container.game2', function(e){
if (e.which == 49) {
console.log('Game 2');
$container.removeClass('game2').addClass('game1');
}
});
#container {
min-width: 100vw;
min-height: 100vh;
background-color: rgb(128, 128, 128);
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<div id="container" class="game1" tabindex="0">
</div>
This logic creates two different delegate event handlers for the body. Both filter out events for the #container child element of the body, but also filter based on an additional class on the container; game1 and game2. Depending on which class the element has, only one of the event handlers will process.
Whenever you call keyup on an element, you attach another event handler. To catch events, you only need to call it once. The callback functions that handle the event will fire every time the event happens.

jQuery sending event as parameter

I want to check the amount of typing characters in an input using keyup and keydown.
I've done it earlier but not as separate function and it was a big mess in a code. I use
event and when I did it without separate function everything worked great, but now I have a problem with send it through the functions. Maybe it's caused by totally wrong method to do this, so please give me some advice, how to do it properly.
This is my separate function:
function char_check($input_id, $div_id, $char_max, event)
{
if(($char_max-$($input_id).val().length)<=0)
{
var key = event.keyCode || event.charCode;
if(key!=8)
{
event.preventDefault();
}
}
if(($char_max-$($input_id).val().length)<0)
{
$($div_id).text("Your data will be cut short!");
}
else
{
$($div_id).text($char_max-$($input_id).val().length);
}
}
And this is the way I call this function:
$('input[name=author]').keydown(function(e){
char_check($(this), "#chars_auth", 50, e);
});
Problem: Event doesn't work.
As I understand it the code does not work, though I am not sure what does not work. Here are some suggestions. First I would create a function that builds your "check function". Something like:
var createCharCheck = function (div_id, char_max) {
return function (event)  {
if ((char_max - $(this).val().length) <= 0) {
var key = event.keyCode || event.charCode;
if(key != 8) {
event.preventDefault();
}
}
if((char_max - $(this).val().length) < 0) {
$(div_id).text("Your data will be cut short!");
} else {
$(div_id).text(char_max - $(this).val().length);
}
}
}
$('input[name=author]').keydown(createCharCheck('#chars_auth', 50));
This is not to be fancy, but save you some code and take advantage of closure. Now you have a function that you can use on any input. Hope this helps.

How to open popover with keyboard shortcut? [duplicate]

This question already has answers here:
Closed 10 years ago.
Possible Duplicate:
Keyboard shortcuts with jQuery
I want to display a popover window using a shortcut key instead of clicking the icon on the toolbar.
Do you have any good idea?
Thank you for your help.
Abody97's answer tells you how to determine if a certain key combo has been pressed. If you're not sure how to get that key combo to show the popover, this is what you need. Unfortunately, Safari makes this needlessly complicated.
In the global script, you'll need a function like the following to show a popover, given its ID and the ID of the toolbar item that should show it:
function showPopover(toolbarItemId, popoverId) {
var toolbarItem = safari.extension.toolbarItems.filter(function (button) {
return button.identifier == toolbarItemId && button.browserWindow == safari.application.activeBrowserWindow;
})[0];
var popover = safari.extension.popovers.filter(function (popover) {
return popover.identifier == popoverId;
})[0];
toolbarItem.popover = popover;
toolbarItem.showPopover();
}
You'll also need code to call this function in your global script's message listener, like the following (this sample does not assume you already have a message listener in place):
safari.application.addEventListener('message', function (e) {
if (e.name == 'Show Popover') {
showPopover(e.message.toolbarItemId, e.message.popoverId);
}
}, false);
Finally, in your injected script, the function that listens for the key combo needs to call dispatchMessage, as below:
safari.self.tab.dispatchMessage('Show Popover', {
toolbarItemId : 'my_pretty_toolbar_item',
popoverId : 'my_pretty_popover'
});
(Stick that in place of showPopUp() in Abody97's code sample.)
Note: If you only have one toolbar item and one popover (and never plan to add more), then it becomes much simpler. Assuming you've already assigned the popover to the toolbar item in Extension Builder, you can just use
safari.extension.toolbarItems[0].showPopover();
in place of the call to showPopover in the global message listener, and omit the message value in the call to dispatchMessage in the injected script.
Assuming your shortcut is Ctrl + H for instance, this should do:
var ctrlDown = false;
$(document).keydown(function(e) {
if(e.keyCode == 17) ctrlDown = true;
}).keyup(function(e) {
if(e.keyCode == 17) ctrlDown = false;
});
$(document).keydown(function(e) {
if(ctrlDown && e.keyCode == 72) showPopUp(); //72 is for h
});
Here's a reference for JavaScript keyCodes: little link.
Here's a little demo: little link. (It uses Ctrl + M to avoid browser-hotkey conflicts).
I believe this could help you: http://api.jquery.com/keypress/
In the following example, you check if "return/enter" is pressed (which has the number 13).
$("#whatever").keypress(function(event) {
if( event.which == 13 ) {
alert("Return key was pressed!");
}
});

Javascript IFrame/designmode and onkeydown event question

Apologize if this is answered already. Went through some of the related questions and google, but ultimately failed to see why this isn't working.
My code is as follows
<iframe id="editor"></iframe>
editorWindow = document.getElementById('editor').contentWindow;
isCtrlDown = false;
function loadEditor()
{
editorWindow.document.designMode = "on";
editorWindow.document.onkeyup = function(e) {
if (e.which == 91) isCtrlDown = false;
}
editorWindow.document.onkeydown = handleKeyDown;
}
function handleKeyDown(e)
{
if (e.which == 91) isCtrlDown = true;
if (e.which == 66 && isCtrlDown) editFont('bold');
if (e.which == 73 && isCtrlDown) editFont('italic');
}
function editFont(a,b)
{
editorWindow.document.execCommand(a,false,b);
editorWindow.focus();
}
This code works perfectly in Chrome, but the keyboard shortcuts do not work in Firefox. In fact, in Firefox it does not seem to register the events for keyup/keydown at all.
Am I doing something grossly wrong here that is mucking up Firefox?
For editable documents, you need to use addEventListener to attach key events rather than DOM0 event handler properties:
editorWindow.document.addEventListener("keydown", handleKeyDown, false);
If you care about IE 6-8, you will need to test for the existence addEventListener and add the attachEvent equivalent if it is missing.
Try using:
editorWindow = document.getElementById('editor').frameElement;
I'm not sure this will solve the issue, it may also be:
editorWindow = document.getElementById('editor').contentDocument;
Or even possibly:
editorWindow = document.getElementById('editor').frameElement.contentDocument;
One thing you can do is put the entire string in a try statement to catch any errors and see if the content is being grabbed from within the iframe.
try { editorWindow = document.getElementById('editor').contentWindow; } catch(e) { alert(e) };
The only other thought I have is that you're typing into a textbox which is within an iframe, and you may possibly have to add the onkeydown event to that specific item, such as:
var editorWindow = document.getElementById('editor').contentDocument;
var textbox = editorWindow.getElementById('my_textbox');
function loadEditor()
{
editorWindow.document.designMode = "on";
textbox.onkeydown = function(e) {
alert('hello there');
}
}
I hope one of these is the solution. I often find when it comes to cross-platform functionality it often boils down to a little trial and error.
Good Luck!

how to re-enable default after doing event.preventDefault()

I know this exact question was asked here, but the answer didn't work for what I needed to do so I figured I'd give some example code and explain a bit...
$(document).keypress(
function (event) {
// Pressing Up or Right: Advance to next video
if (event.keyCode == 40 || event.keyCode == 39) {
event.preventDefault();
$(".current").next().click();
}
// Pressing Down or Left: Back to previous video
else if (event.keyCode == 38 || event.keyCode == 37) {
event.preventDefault();
$(".current").prev().click();
}
}
);
It basically disables the arrow keys to use them for something else, but doing:
$(document).keypress(function () { });
doesn't enable the default function again... I need it to scroll the page without having to create a scroll function for it...
Any ideas?
Thanks,
Matt
Adding a new handler doesn't replace the previous one, it adds a new one. You may be looking for jQuery#unbind if you're trying to remove the previous handler, but if you're going to be turning this on and off a lot, you probably would be better off with a flag telling you whether to prevent the default or not in your existing handler.
Adding, and later removing, a handler looks like this:
function keypressHandler() { /* ... */};
$('#thingy').keypress(keypressHandler);
// ...elsewhere...
$('#thingy').unbind('keypress', keypressHandler);
I'm not sure this is the right way to handle it.
A better way to approach this problem would be to put some kind of check inside your document.keypress instructions.. like..
var enableKeys = false;
$(document).keypress(
function (event) {
// Pressing Up or Right: Advance to next video
if (event.keyCode == 40 || event.keyCode == 39 && enableKeys) {
event.preventDefault();
$(".current").next().click();
}
// Pressing Down or Left: Back to previous video
else if (event.keyCode == 38 || event.keyCode == 37 && enableKeys) {
event.preventDefault();
$(".current").prev().click();
}
}
);
Then control the enablekeys wherever you feel necessary, either with a hover, or something along those lines.
function(e){ e.preventDefault(); }
and its opposite
function(e){ return true; }
Why not just wrap a condition around event.preventDefault(); in your current code?
Try to unbind the keypress event from document.
I don't know of any other ways to do it.
HTH

Categories