escape from setInterval at end of game level - javascript

I'm writing a game using HTML5 canvas and Javascript. I'm using setInterval to animate the game and check for events at regular intervals. I've written multiple levels for the game, but I can't seem to escape the setInterval when a given level ends in order to start the next one. The code accurately detects when a level is won or lost and successfully clears the interval and renders the button, but the button does not fire.
Adding a button was my latest idea. I've also tried removing the canvas using jQuery and inserting a new one. I've also tried using clearRect on the canvas but it doesn't fire either.
Given that I can't return a value from setInterval, what are my options, if any? Is there another way to accomplish the same thing? Is there a separate error with my code that I've overlooked? Thanks!
Game.prototype.win = function(int) {
clearInterval(int);
var content = "<p style='color:white;'>You win</p><br><button id='next-level'>Next Level</button></menu>"
$('#controls').append(content)
};
Game.prototype.lose = function(int) {
clearInterval(int);
var content = "<p style='color:white;'>You Lose</p><br><button id='next-level'>Start Over?</button></menu>"
$('#controls').append(content)
};
Game.prototype.run = funtion () {
$('#start').click( function () {
$('#controls').empty();
var waveOne = new window.CrystalQuest.Wave(this.X_DIM, this.Y_DIM, this, Game.WAVE_ONE)
var game = this
var int = setInterval( function () {
if (waveOne.step() === "lost" ) {
game.lose(int);
} else if (waveOne.step() === "won") {
game.win(int);
}
waveOne.draw(this.ctx)
}, 20)
this.bindKeyHandlers(waveOne);
}.bind(this));
$('#next-level').click( function () {
$('#controls').empty();
...more code...
});
};

To stop a setInterval() you have to store the returned value from the original call to setInterval() in some persistent location and then call clearInterval() on that value.
Because you declared your interval with var as in var int, it was local only to that method and was not available anywhere else in the code.
There are a number of ways to do that in your code. I would probably suggest storing it as an instance variable like this:
Game.prototype.run = funtion () {
$('#start').click( function () {
$('#controls').empty();
var waveOne = new window.CrystalQuest.Wave(this.X_DIM, this.Y_DIM, this, Game.WAVE_ONE)
var game = this;
this.stop();
this.interval = setInterval( function () {
if (waveOne.step() === "lost" ) {
game.lose(int);
} else if (waveOne.step() === "won") {
game.win(int);
}
waveOne.draw(this.ctx)
}, 20)
this.bindKeyHandlers(waveOne);
}.bind(this));
$('#next-level').click( function () {
$('#controls').empty();
...more code...
});
};
Then, you can make a method that will stop the interval like this:
Game.prototype.stop = function() {
if (this.interval) {
clearInterval(this.interval);
this.interval = null;
}
}
And, change your other methods like this:
Game.prototype.win = function(int) {
this.stop();
var content = "<p style='color:white;'>You win</p><br><button id='next-level'>Next Level</button></menu>"
$('#controls').append(content)
};
Game.prototype.lose = function(int) {
this.stop();
var content = "<p style='color:white;'>You Lose</p><br><button id='next-level'>Start Over?</button></menu>"
$('#controls').append(content)
};
For your event handling issues, if you are destroying and recreating a button, then you will lose any event handlers that were attached to any DOM elements that got replaced.
You have two choices for how to fix it:
After you create the new DOM elements, you can again set the event handlers on the new DOM elements.
You can use "delegated event handling". This attaches the event handlers to a parent DOM object that is itself not replaced. The click event bubbles up to the parent and is handled there. The child can be replaced as many times as you want and the event handler will still work.
See these references for how to use delegated event handling with jQuery:
Does jQuery.on() work for elements that are added after the event handler is created?
jQuery .live() vs .on() method for adding a click event after loading dynamic html
JQuery Event Handlers - What's the "Best" method

Related

Javascript closures and onload

I am learning to use closures, and I'm not sure about what is the best approach for my situation. I edited my code for clarity and it contains dummy code. I have to access to the handleCannon function if the user clicks on the test element. If I declare handleCannon within the onload event it won't work. If I declare both functions outside of the onload event the handlebullet function won't be able to get the test1 element because it's not loaded yet. Would the following code be acceptable? What is the best way to do this?
let handleCannon;
window.onload = function() {
handleCannon = (function(el) {
return function(el) {
el.innerHTML = "bananas";
handleBullet.reset();
}
})();
let handleBullet = (function() {
let test1 = document.getElementsByClassName("test1")[0];
let rect = test1.getBoundingClientRect();
console.log(rect);
return {
reset: function(){
console.log("reset");
},
}
})();
}
and the body contains:
<div class="test" onclick="handleCannon(this)"></div>
<div class="test1"></div>
Rather than using inline attribute handlers (which are hard to manage and generally considered to be pretty poor practice), attach the handler using Javascipt instead, inside the onload:
window.onload = function() {
const test = document.querySelector('.test');
// handleCannon:
test.addEventListener('click', () => {
test.textContent = 'bananas';
handleBullet.reset();
});
// ...
Note that assigning to onload will mean that this handler will be overwritten if anything else assigns to onload (similarly, another handler already on onload when this script runs will be overwritten) - you might consider using addEventListener instead, allowing your scripts to listen for the load event in multiple places, which can be useful with larger or multiple scripts:
window.addEventListener('load', () => {
const test = document.querySelector('.test');
// handleCannon:
test.addEventListener('click', () => {
test.textContent = 'bananas';
handleBullet.reset();
});
// ...
});

Javascript: addEventListener only working in setTimeout

I'm trying to set event listeners but it's only working if I set them within setTimeout.
Doesn't work:
WebApp.setController('jobs', function() {
WebApp.setView('header', 'header');
WebApp.setView('nav', 'nav');
WebApp.setView('jobs', 'main');
var jobs = document.querySelectorAll('.jobs-category');
for(let i = 0; i < jobs.length; i++)
{
console.log('events added');
jobs[i].addEventListener("dragover", function( event ) {
console.log('drag over');
event.preventDefault();
});
jobs[i].addEventListener('drop', function(event) {
event.preventDefault();
console.log('dropped');
}, false);
}
});
Does work:
WebApp.setController('jobs', function() {
WebApp.setView('header', 'header');
WebApp.setView('nav', 'nav');
WebApp.setView('jobs', 'main');
window.setTimeout(function() {
var jobs = document.querySelectorAll('.jobs-category');
for(let i = 0; i < jobs.length; i++)
{
console.log('events added');
jobs[i].addEventListener("dragover", function( event ) {
console.log('drag over');
event.preventDefault();
});
jobs[i].addEventListener('drop', function(event) {
event.preventDefault();
console.log('dropped');
}, false);
}
}, 1);
});
(only setTimout is different/additionally)
setController() saves the function and executes it if the route get requested.
setView() binds HTML5-templates to DOM:
var Template = document.querySelector('#' + Name);
var Clone = document.importNode(Template.content, true);
var CloneElement = document.createElement('div');
CloneElement.appendChild(Clone);
CloneElement = this.replacePlaceholders(CloneElement);
document.querySelector(Element).innerHTML = CloneElement.innerHTML;
Why does this only work in setTimeout? I thought javascript is synchronous.
Addition: This is a single page app, which gets already loaded after DOM is ready.
Where is your Javascript within the page?
My guess is that the HTML is not ready yet when you try to register the event, this is why it only works with setTimeout.
Try either including your javascript at the bottom of the page (after the HTML) or listening to the page loaded event. See this question for info how to do that - $(document).ready equivalent without jQuery.
Per your assumption of synchronous javascript - yes it is (mostly) but also the way that the browser renders the page is (unless using async loading of scripts). Which means that HTML that is after a javascript will not be available yet.
UPDATE - per your comment of using a single page app, you must listen to the load complete/done/successful event. Another way you could bypass it will be listening to the events on the parent element (which is always there).
Hope this helps.
document.addEventListener("DOMContentLoaded", function(event) {
//do work
});
Most likely you are loading your JS file before your html content. You can't run your Javascript functions until the DOM is ready.

Catching all audio end events from dynamic content in jquery

I have seen similar questions - but not that fix my problem!
I have audio on my page and when one ends, I want the next to start, but I can't even get the ended to trigger...
I cut the code down to this:
function DaisyChainAudio() {
$().on('ended', 'audio','' ,function () {
alert('done');
});
}
This is called from my page/code (and is executed, setting a break point shows that).
As far as I understand this should set the handler at the document level, so any 'ended' events from any 'audio' tag (even if added dynamically) should be trapped and show me that alert...
But it never fires.
edit
With some borrowing from Çağatay Gürtürk's suggestion so far have this...
function DaisyChainAudio() {
$(function () {
$('audio').on('ended', function (e) {
$(e.target).load();
var next = $(e.target).nextAll('audio');
if (!next.length) next = $(e.target).parent().nextAll().find('audio');
if (!next.length) next = $(e.target).parent().parent().nextAll().find('audio');
if (next.length) $(next[0]).trigger('play');
});
});
}
I'd still like to set this at the document level so I don't need to worry about adding it when dynamic elements are added...
The reason it does not fire is, media events( those specifically belonging to audio or video like play, pause, timeupdate, etc) do not get bubbled. you can find the explanation for that in the answer to this question.
So using their solution, I captured the ended event, and this would allow setting triggers for dynamically added audio elements.
$.createEventCapturing(['ended']); // add all the triggers for which you like to catch.
$('body').on('ended', 'audio', onEnded); // now this would work.
JSFiddle demo
the code for event capturing( taken from the other SO answer):
$.createEventCapturing = (function () {
var special = $.event.special;
return function (names) {
if (!document.addEventListener) {
return;
}
if (typeof names == 'string') {
names = [names];
}
$.each(names, function (i, name) {
var handler = function (e) {
e = $.event.fix(e);
return $.event.dispatch.call(this, e);
};
special[name] = special[name] || {};
if (special[name].setup || special[name].teardown) {
return;
}
$.extend(special[name], {
setup: function () {
this.addEventListener(name, handler, true);
},
teardown: function () {
this.removeEventListener(name, handler, true);
}
});
});
};
})();
Try this:
$('audio').on('ended', function (e) {
alert('done');
var endedTag=e.target; //this gives the ended audio, so you can find the next one and play it.
});
Note that when you create a new audio dynamically, you should assign the events. A quick and dirty solution would be:
function bindEvents(){
$('audio').off('ended').on('ended', function (e) {
alert('done');
var endedTag=e.target; //this gives the ended audio, so you can find the next one and play it.
});
}
and run bindEvents whenever you create/delete an audio element.

Call function once from multiple events

I've been working on webpages for a range of touch screen devices, and one of the most consistent problems is how touch events are handled.
Is there a nice way to only call a function once even when multiple (roughly) simultaneous events call it?
e.g.
$("body").on("mousedown touchstart MSPointerDown", function () {
alert("This message will appear multiple times on some devices.");
})
I've thought about using a timeout so the function can only be called once every 200 milliseconds or something similar (off the top of my head and untested):
var allowed = true;
$("body").on("mousedown touchstart MSPointerDown", function () {
if(allowed){
allowed = false;
alert("This message will hopefully only appear once!");
setTimeout(function () { allowed = true }, 200);
}
})
(For this question, I am NOT looking for plugin suggestions, I am aware there are lots of touch event plugins)
Is there a proper/nicer way to use multiple events as possible triggers for a single function? Could I alias the events in some way without breaking their other uses?
In effect, you're looking to take only the first event type that comes through and ignore all the others. This will still fire for future clicks/touches. Enter closures.
$(document).ready(function() {
function alertClosure() {
var eventType = null;
function doAlert(e) {
if (!eventType) {
eventType = e.type; // only the first eventType we get will be registered
}
if (e.type == eventType) {
alert("This message will hopefully only appear once!: " + e.type);
}
}
return doAlert;
}
$("body").on( "mousedown touchstart MSPointerDown", alertClosure() );
});
http://plnkr.co/edit/oz48d3
You could use $.one (rather than $.on)
Here : $.one documentation on jquery.com
If you want it to be subsequently called then you could rebind the handler on a timeout, something like this:
function handler(){
var called = false;
return function(ev){
if(!called){
called = true;
$("ul#messages").append($("<li>").text("event"));
setTimeout(bind, 1000); // rebind after a suitable pause
}
}
}
function bind(){
$("ul#messages").one("click", new handler())
};
$(function(){
bind();
});
https://jsfiddle.net/p3t6xo48/5/
This allows each bound handler to be run once, and once only, for multiple events, then it's rebound after a suitable pause.

Detecting a form.submit() performed via JavaScript

In my page there is a frame that belongs to the same domain. The content of this frame is varied and relatively unpredictable. Whenever a user clicks a button (inside the frame) that performs a post, I need to execute a function that performs some UI tasks. The problem is that I cannot edit the source of these frames for reasons beyond my control. Some of these buttons are simple form submit buttons, but others do not directly submit the form, but instead have an onclick handler that performs some checks and might submit.
Here is the problem: How do I detect if one of these onclick handlers called form.submit()? If there's no handler, then obviously I can set up a handler for onsubmit(), but is not the case for all of these buttons.
This is my code so far:
function addEventBefore(element, type, before, after) {
var old = element['on' + type] || function() {};
before = before || function() {};
after = after || function() {};
element['on' + type] = function () {
before();
old();//I can't modify this old onclick handler
after();
};
}
function setup() {
console.log('setup');
}
function takedown() {
// In this method, I want to know if old() caused a form submit
console.log('takedown');
}
function $includeFrames(jQuery, selector) {
return jQuery(selector).add(jQuery('iframe').contents().find(selector));
}
var a = $includeFrames($, 'input[type="submit"], input[type="button"]').each(function() {
var elem = $(this)[0];
addEventBefore(elem, 'click', setup, takedown);
});
In the onload event of the iframe you'll need to hook up an event listener to each form in the iframed page. You need to do this on every load, as each fresh page needs new listeners.
$("#someIframe").on('load',function() {
$(this).contents().find("form").each(function() {
$(this).on('submit',function() {... your code...})
})
}
The solution that worked for me came from a friend of mine. The solution is to shim the form.submit() function.
$(function() {
var el = document.getElementById('myform');
el.submit = function(fn) {
return function() {
myFunctionGoesHere();
fn.apply(this, arguments);
};
}(el.submit);
});
Here is a working example: http://jsfiddle.net/hW6Z4/9/

Categories

Resources