PDA

View Full Version : Assigning a click handler to dynamic elements



tonybabb
03-15-2015, 01:30 PM
Good morning John,
Several months ago you kindly helped me create code for a smartphone app that played a short sound file using jQuery when a phrase was tapped on the screen, this is working perfectly – thanks.

I have now added a search function which searches the app and copies matching phrases to the search results page and this is almost working. The problem I’m having is that when I tap a phrase on the search results page the app invokes the system sound player which displays an audio control bar.

I think the problem is that the click handler is assigned at document ready and of course the search results are not defined and so a click handler is not assigned to the search results.

I have been digging around and found several similar issues which were resolved by using .on() and assigning it to the <body> tag or any parent element. When I tried this I found that all taps invoked the system sound player….sigh.

I would really appreciate your assistance with this. Below is your original code which was invoked at document ready and you will see commented out attempts to use the .on() solution


jQuery(function($){
var highlight = 'yellow', origcolor = 'transparent', curSnd = null,
istouch = !!('ontouchstart' in window) || !!('ontouchstart' in document.documentElement) || !!window.ontouchstart || (!!window.Touch && !!window.Touch.length) || !!window.onmsgesturechange || (window.DocumentTouch && window.document instanceof window.DocumentTouch);
function playstop(e){
e.preventDefault();
var $this = $(this);
if(curSnd && curSnd.sound){
if(this === curSnd.tag){
curSnd.sound.stop();
return;
}
curSnd.sound.stop();
}
$this.stop(true, true).css({backgroundColor: highlight});
var filename = this.href.substring(this.href.lastIndexOf('/')), myMedia = new Media(
this.href,
function() {
myMedia && myMedia.release();
curSnd = myMedia = null;
$this.stop(true, true).animate({backgroundColor: origcolor}, 500);
},
function(e) {
myMedia && myMedia.release();
curSnd = myMedia = null;
$this.stop(true, true).animate({backgroundColor: origcolor}, 500);
window.console && console.log ("Audio play error - " + filename + "\ncode: " + e.code + "\nmessage: " + e.message);
}
);
curSnd = {tag: this, sound: myMedia};
curSnd.sound.play();
} // End playstop
// jourdan $('body').on('click', '.spanish', function(e){e.preventDefault();}).each(function(i, snd){
$('.spanish').click(function(e){e.preventDefault();}).each(function(i, snd){
//$('body').on('click', '.spanish', function(i){
if(istouch){
var $snd = $(snd);
//var $snd = $(i.currentTarget);
snd.addEventListener('touchstart', function(e){$snd.data({tstart: new Date().getTime(), tx: e.pageX, ty: e.pageY});}, false);
//i.currentTarget.addEventListener('touchstart', function(e){$snd.data({tstart: new Date().getTime(), tx: e.pageX, ty: e.pageY});}, false);
//i.currentTarget.addEventListener('touchend', function(e){
snd.addEventListener('touchend', function(e){
var ds = $snd.data('tstart');
if(!ds){return;}
var vx = Math.abs(e.pageX - $snd.data('tx')), vy = Math.abs(e.pageY - $snd.data('ty'));
if(vx < 20 && vy < 20 && new Date().getTime() - ds < 400){
//playstop.apply(i.currentTarget, [e]);
playstop.apply(snd, [e]);
$snd.data({tstart: null, tx: null, ty: null});
}
}, false);
}
});
});

Thanks for any assistance you can provide.

Tony

jscheuer1
04-04-2015, 06:59 PM
Well, it's important to know which version of jQuery you are using. The on() function did not become available until version 1.7.x prior to that the function was live() and it had a different syntax. I think live() continued to work until jQuery 1.9.x when it was dropped.

Do you know which jQuery version you are using?

Another issue is that, if you are using a touch device, there is no intrinsic touch event in jQuery, so you will need to use something other than pure jQuery to capture that anyway.

Finally, for now, the code you posted above looks broken to me - that is, I can't see it working at all for any device, as the closures (denoted by the { and } brackets) do not appear to correspond to the basic rules of javascript functionality.

I'll have a closer look at it, it's possible I'm wrong about that last thing and/or can see how to fix that part of it. I also should be able to come up with a basic routine to use regular javascript to do an on() or live() type of thing for touch devices.

In the meantime, do you have a working copy of the code without the attempts at on() in it? If so, let me see that. Oh and let me know the jQuery version you are using.

Oh, one other thing - when exactly was I last working with you on this? Knowing that will make it relatively easy for me to locate my copies of the work at that time, which may come in handy.

jscheuer1
04-04-2015, 08:12 PM
OK, hmm, looks like the code you posted was OK I think. I still need to know the jQuery version. But as long as it is 1.7 or later and I've made no errors, this should take care of things (replace the code you posted with this):


jQuery(function($){
var highlight = 'yellow', origcolor = 'transparent', curSnd = null,
istouch = !!('ontouchstart' in window) || !!('ontouchstart' in document.documentElement) || !!window.ontouchstart || (!!window.Touch && !!window.Touch.length) || !!window.onmsgesturechange || (window.DocumentTouch && window.document instanceof window.DocumentTouch);
function playstop(e){
e.preventDefault();
var $this = $(this);
if(curSnd && curSnd.sound){
if(this === curSnd.tag){
curSnd.sound.stop();
return;
}
curSnd.sound.stop();
}
$this.stop(true, true).css({backgroundColor: highlight});
var filename = this.href.substring(this.href.lastIndexOf('/')), myMedia = new Media(
this.href,
function() {
myMedia && myMedia.release();
curSnd = myMedia = null;
$this.stop(true, true).animate({backgroundColor: origcolor}, 500);
},
function(e) {
myMedia && myMedia.release();
curSnd = myMedia = null;
$this.stop(true, true).animate({backgroundColor: origcolor}, 500);
window.console && console.log ("Audio play error - " + filename + "\ncode: " + e.code + "\nmessage: " + e.message);
}
);
curSnd = {tag: this, sound: myMedia};
curSnd.sound.play();
} // End playstop
$('body').on('click', '.spanish', function(e){e.preventDefault();});
if(istouch){
document.body.addEventListener('touchstart', function(e){
var $snd = $(e.target);
if(!$snd.hasClass('spanish')){return;}
$snd.data({tstart: new Date().getTime(), tx: e.pageX, ty: e.pageY});
}, false);
document.body.addEventListener('touchend', function(e){
var $snd = $(e.target);
if(!$snd.hasClass('spanish')){return;}
var ds = $snd.data('tstart');
if(!ds){return;}
var vx = Math.abs(e.pageX - $snd.data('tx')), vy = Math.abs(e.pageY - $snd.data('ty'));
if(vx < 20 && vy < 20 && new Date().getTime() - ds < 400){
playstop.apply($snd.get(0), [e]);
$snd.data({tstart: null, tx: null, ty: null});
}
}, false);
}

});

Let's see what happens.

I've been looking at this a bit more and reading up on the jQuery .on() function. This should also work and might be a little easier to follow:


jQuery(function($){
var highlight = 'yellow', origcolor = 'transparent', curSnd = null;
function playstop(e){
e.preventDefault();
var $this = $(this);
if(curSnd && curSnd.sound){
if(this === curSnd.tag){
curSnd.sound.stop();
return;
}
curSnd.sound.stop();
}
$this.stop(true, true).css({backgroundColor: highlight});
var filename = this.href.substring(this.href.lastIndexOf('/')), myMedia = new Media(
this.href,
function() {
myMedia && myMedia.release();
curSnd = myMedia = null;
$this.stop(true, true).animate({backgroundColor: origcolor}, 500);
},
function(e) {
myMedia && myMedia.release();
curSnd = myMedia = null;
$this.stop(true, true).animate({backgroundColor: origcolor}, 500);
window.console && console.log ("Audio play error - " + filename + "\ncode: " + e.code + "\nmessage: " + e.message);
}
);
curSnd = {tag: this, sound: myMedia};
curSnd.sound.play();
} // End playstop
$('body').on('click touchstart touchend', '.spanish', function(e){
if(e.type === 'click'){
e.preventDefault();
} else if (e.type === 'touchstart'){
$(this).data({tstart: new Date().getTime(), tx: e.pageX, ty: e.pageY});
} else {
var $snd = $(this), ds = $snd.data('tstart'), vx, vy;
if(!ds){return;}
vx = Math.abs(e.pageX - $snd.data('tx')); vy = Math.abs(e.pageY - $snd.data('ty'));
if(vx < 20 && vy < 20 && new Date().getTime() - ds < 400){
playstop.apply(this, [e]);
$snd.data({tstart: null, tx: null, ty: null});
}
}
});
});

tonybabb
04-06-2015, 12:29 PM
Hello John,

Thanks for the code. To answer your question, I'm using jQuery 2.1.3 and we last worked on this in the summer of 2014.

Since I originally posted this question I have made some progress. I was able to get the .on() solution to assign click handlers on touchstart/end on the dynamically created elements - comparing your code with mine I have to admit my solution is not as elegant, but it does seem to work. I will also try substituting your code. My problems are not completely over though.

You may recall we had to decide if this was a touch or swipe and it does this by identifying the x/y coordinates on touchstart/end and also calculating the duration, movement in x/y <20px and duration < 400ms was assumed to be a touch. I am able to access the time on touchstart/end and calculate the duration but I have been unable to access the x/y coordinates.

The problem I think is that the original handler used js and I am now using jQuery so e.pageX and e.pageY does not work.
Below is the click handler as it is now and below that a summary showing console log results of my various attempts to get the x/y coordinates.

Current click handler - note several sections are commented out just to get it to work, sorry if that makes it harder to read.


jQuery(function($){
var highlight = 'yellow', origcolor = 'transparent', curSnd = null,
istouch = !!('ontouchstart' in window) || !!('ontouchstart' in document.documentElement) || !!window.ontouchstart || (!!window.Touch && !!window.Touch.length) || !!window.onmsgesturechange || (window.DocumentTouch && window.document instanceof window.DocumentTouch);
function playstop(e){
console.log("Start playstop");
e.preventDefault();
var $this = $(this);
if(curSnd && curSnd.sound){
if(this === curSnd.tag){
curSnd.sound.stop();
return;
}
curSnd.sound.stop();
}
$this.stop(true, true).css({backgroundColor: highlight});
var filename = this.href.substring(this.href.lastIndexOf('/')), myMedia = new Media(
this.href,
function() {
myMedia && myMedia.release();
curSnd = myMedia = null;
$this.stop(true, true).animate({backgroundColor: origcolor}, 500);
},
function(e) {
myMedia && myMedia.release();
curSnd = myMedia = null;
$this.stop(true, true).animate({backgroundColor: origcolor}, 500);
window.console && console.log ("Audio play error - " + filename + "\ncode: " + e.code + "\nmessage: " + e.message);
}
);
console.log("playstop - Start playing sound")
curSnd = {tag: this, sound: myMedia};
curSnd.sound.play();
} // End playstop
$(".wrapper") //i'm attaching all event handlers to body, so everything with .spanish class will have them attached
.on("click", ".spanish", function(e) {
e.preventDefault();
})
.on("touchstart", ".spanish", function(e) {
if (istouch) {
console.log("touch Start detected");
$sndStartT = new Date().getTime();
console.log("touchstart T = " + $sndStartT);
$sndStartX = event.pageX;
console.log("touchstart X = " + $sndStartX);
$sndStartY = event.pageY;
console.log("touchstart Y = " + $sndStartY);

/* $sndStart = $(this);
$sndStart.data({
tstart: new Date().getTime(),
tx: e.pageX,
ty: e.pageY
}); */
}
})
.on("touchend", ".spanish", function(e) {
console.log("touch End detected");
$sndEndT = new Date().getTime();
console.log("touchend T = " + $sndEndT);
$sndEndX = event.pageX;
console.log("touchend X = " + $sndEndX);
$sndEndY = event.pageY;
console.log("touchend Y = " + $sndEndY);
if (istouch) {
console.log("touchend detected istouch val = " + istouch);
var $snd = $(this);
var snd = this;
var ds = $sndStartT; //$sndStart.data('tstart');
console.log("touchend ds = " + ds);
if (!ds) {
return;
}
/* var vx = Math.abs(e.originalEvent.touches[0].pageX - $sndStart.data('tx'));
console.log("touchend vx = " + vx);
var vy = Math.abs(e.originalEvent.touches[0].pageY - $sndStart.data('ty'));
Math.abs(e.originalEvent.touches[0].pageY - $sndStart.data('ty'));
console.log("touchend vx= "+ vx +" vy= " + vy + " ds = " + ds); */
/* TEMPORARILY REMOVE THE CALCULATION
var vx = Math.abs($sndStartX - $sndEndX);
console.log("touchend vx = " + vx);
var vy = Math.abs($sndStartY - $sndEndY);
console.log("touchend vy = " + vy);
var vt = Math.abs($sndStartT - $sndEndT);
console.log("touchend vt = " + vt); */
vt = 200;
vx = 10;
vy = 10;
if (vx < 20 && vy < 20 && vt < 400 /*new Date().getTime() - ds < 400 */) {
console.log("touchend tap detected and about to call playstop");
playstop.apply(snd, [e]);
$sndStart.data({
tstart: null,
tx: null,
ty: null
});
}
}
});
});

Within the touchstart and touchend functions I tried various versions to try and get pageX and pageY values, none of them worked but in case it helps here's a summary of what I tried and the console log results, in all cases the clickhandlers were triggered correctly and the start/ end times were displayed correctly.

$sndStartX = e.pageX; // returns "undefined"
$sndStartX = event.pageX; // returns "0"
$sndStartX = event.originalEvent.touches[0].pageX; // returns "Cannot read property 'touches' of undefined"
$sndStartX = event.originalEvent.pageX; // returns "Cannot read property 'pageX' of undefined

Thanks,

Tony

jscheuer1
04-06-2015, 03:39 PM
pageX and pageY are event properties in jQuery. The problem appears to be that you misunderstand the meaning of the word event. Unlike in early IE, there is no free floating global event. To access the event properties one must obtain a reference to the event as an argument/parameter to the handler function.

Try my code, I do this without thinking. I know, for you it's harder. You have used e here:


.on("touchstart", ".spanish", function(e) {

and here:


.on("touchend", ".spanish", function(e) {

to represent the event. The problem is that here:


$sndStartX = event.pageX;

you abandon e and use the currently undefined word 'event'.

Again, agreement between the argument/parameter used in the function parenthesis (e) in this case is now the event, not some abstract and undefined in this case word as 'event' that you're using.change to (change here and similar):


$sndStartX = e.pageX;

And, as long as no other errors are impacting it, things will work.

Still, I highly recommend that you try my code (either the 1st or 2nd one), I think you will be happier with it in the long run. It may need to be tweaked, but as far as I can tell in testing and reading up on it, it should be fine as is.

tonybabb
04-07-2015, 12:33 PM
Hi John,

Thanks so much for the explanation and code, I went ahead and installed the second code sample then ran a test which produced no sound when tapped, so I added a few console.log statements and tested again. Below is the console log output and below that is the code showing where I added console.log statements.

Console log - note e.pageX and e.pageY show as undefined, also $snd.data('tx') and $snd.data('ty').
Check for click, touchstart, touchend index.html:54
e.type = touchstart index.html:55
touchstart e.type = touchstart index.html:60
Check for click, touchstart, touchend index.html:54
e.type = click index.html:55
Click e.type = click index.html:57
Check for click, touchstart, touchend index.html:54
e.type = touchend index.html:55
touchend e.type = touchend index.html:63
ds = 1428409175059 index.html:66
e.pageX = undefined index.html:67
e.pageY = undefined index.html:68
$snd.data('tx') = undefined index.html:69
$snd.data('ty') = undefined index.html:70
vx = NaN index.html:72
vy = NaN

Here's the script showing the console.log statements


jQuery(function($){
var highlight = 'yellow', origcolor = 'transparent', curSnd = null;
function playstop(e){
console.log("playstop entered");
e.preventDefault();
var $this = $(this);
if(curSnd && curSnd.sound){
if(this === curSnd.tag){
curSnd.sound.stop();
return;
}
curSnd.sound.stop();
}
$this.stop(true, true).css({backgroundColor: highlight});
var filename = this.href.substring(this.href.lastIndexOf('/')), myMedia = new Media(
this.href,
function() {
myMedia && myMedia.release();
curSnd = myMedia = null;
$this.stop(true, true).animate({backgroundColor: origcolor}, 500);
},
function(e) {
myMedia && myMedia.release();
curSnd = myMedia = null;
$this.stop(true, true).animate({backgroundColor: origcolor}, 500);
window.console && console.log ("Audio play error - " + filename + "\ncode: " + e.code + "\nmessage: " + e.message);
}
);
curSnd = {tag: this, sound: myMedia};
curSnd.sound.play();
} // End playstop
$('body').on('click touchstart touchend', '.spanish', function(e){
console.log("Check for click, touchstart, touchend");
console.log("e.type = " + e.type);
if(e.type === 'click'){
console.log("Click e.type = " + e.type);
e.preventDefault();
} else if (e.type === 'touchstart'){
console.log("touchstart e.type = " + e.type);
$(this).data({tstart: new Date().getTime(), tx: e.pageX, ty: e.pageY});
} else {
console.log("touchend e.type = " + e.type);
var $snd = $(this), ds = $snd.data('tstart'), vx, vy;
if(!ds){return;}
console.log("ds = " + ds);
console.log("e.pageX = " + e.pageX);
console.log("e.pageY = " + e.pageY);
console.log("$snd.data('tx') = " + $snd.data('tx'));
console.log("$snd.data('ty') = " + $snd.data('ty'));
vx = Math.abs(e.pageX - $snd.data('tx')); vy = Math.abs(e.pageY - $snd.data('ty'));
console.log("vx = " + vx);
console.log("vy = " + vy);
if(vx < 20 && vy < 20 && new Date().getTime() - ds < 400){
playstop.apply(this, [e]);
$snd.data({tstart: null, tx: null, ty: null});
}
}
});
});

In case it helps here's a sample html section showing an English phrase followed by a Spanish phrase


<div class="english">
How much does she weigh?
</div> <!-- End English -->
<div class="wrapper">
<a class="spanish" data-ignore="true" href="ageWeightFemale.mp3">Cuánto pesa ella?</a>
</div> <!-- End .wrapper -->

pageX and pageY are event properties in jQuery. The problem appears to be that you misunderstand the meaning of the word event. Unlike in early IE, there is no free floating global event. To access the event properties one must obtain a reference to the event as an argument/parameter to the handler function.

Try my code, I do this without thinking. I know, for you it's harder. You have used e here:


.on("touchstart", ".spanish", function(e) {

and here:


.on("touchend", ".spanish", function(e) {

to represent the event. The problem is that here:


$sndStartX = event.pageX;

you abandon e and use the currently undefined word 'event'.

Again, agreement between the argument/parameter used in the function parenthesis (e) in this case is now the event, not some abstract and undefined in this case word as 'event' that you're using.change to (change here and similar):


$sndStartX = e.pageX;

And, as long as no other errors are impacting it, things will work.

Still, I highly recommend that you try my code (either the 1st or 2nd one), I think you will be happier with it in the long run. It may need to be tweaked, but as far as I can tell in testing and reading up on it, it should be fine as is.

jscheuer1
04-07-2015, 05:26 PM
Well, before we go running around thinking this is something we can fix in jQuery, let's first see if it's jQuery's fault or not. To do that - try the first code. If that works fine and I have an idea how to fix jQ. If that doesn't work, there may be some other problem, or we may need a different approach altogether, one which initializes these items in the way items already on the page were initialized before we started all this. That can be done, we just have to wait until the search is complete and the results posted, then we can initialize them. A unique container may be needed for the search results so that the original items aren't initialized twice.

But, first try the first code from my previous post:


jQuery(function($){
var highlight = 'yellow', origcolor = 'transparent', curSnd = null,
istouch = !!('ontouchstart' in window) || !!('ontouchstart' in document.documentElement) || !!window.ontouchstart || (!!window.Touch && !!window.Touch.length) || !!window.onmsgesturechange || (window.DocumentTouch && window.document instanceof window.DocumentTouch);
function playstop(e){
e.preventDefault();
var $this = $(this);
if(curSnd && curSnd.sound){
if(this === curSnd.tag){
curSnd.sound.stop();
return;
}
curSnd.sound.stop();
}
$this.stop(true, true).css({backgroundColor: highlight});
var filename = this.href.substring(this.href.lastIndexOf('/')), myMedia = new Media(
this.href,
function() {
myMedia && myMedia.release();
curSnd = myMedia = null;
$this.stop(true, true).animate({backgroundColor: origcolor}, 500);
},
function(e) {
myMedia && myMedia.release();
curSnd = myMedia = null;
$this.stop(true, true).animate({backgroundColor: origcolor}, 500);
window.console && console.log ("Audio play error - " + filename + "\ncode: " + e.code + "\nmessage: " + e.message);
}
);
curSnd = {tag: this, sound: myMedia};
curSnd.sound.play();
} // End playstop
$('body').on('click', '.spanish', function(e){e.preventDefault();});
if(istouch){
document.body.addEventListener('touchstart', function(e){
var $snd = $(e.target);
if(!$snd.hasClass('spanish')){return;}
$snd.data({tstart: new Date().getTime(), tx: e.pageX, ty: e.pageY});
}, false);
document.body.addEventListener('touchend', function(e){
var $snd = $(e.target);
if(!$snd.hasClass('spanish')){return;}
var ds = $snd.data('tstart');
if(!ds){return;}
var vx = Math.abs(e.pageX - $snd.data('tx')), vy = Math.abs(e.pageY - $snd.data('ty'));
if(vx < 20 && vy < 20 && new Date().getTime() - ds < 400){
playstop.apply($snd.get(0), [e]);
$snd.data({tstart: null, tx: null, ty: null});
}
}, false);
}

});

tonybabb
04-08-2015, 11:09 AM
Thanks John, I tried the first code example and it worked perfectly. Once again I'm in your debt.

Tony

jscheuer1
04-08-2015, 01:56 PM
Great! You can just use that. And, though the problem before could be something else, I think I know what that means and how to fix the code that didn't work. In jQuery there is the jQuery passed event and the original event. Sometimes the jQuery passed event doesn't have all of the properties of the original event, and sometimes it has more. In this case I think it likely is dropping the pageX and pageY properties from the touch events. Usually jQuery justifies doing something like that because a property is non-standard for the event like, I think it is wheel movement for mousewheel events. However, according to the W3C standards, touch events are supposed to have pageX and pageY, so I'm not sure the logic here. In the code that does work, we are not using jQuery to process the event, and so it does have the standard pageX and pageY. We could probably still use the jQuery processing if we were to query the original event object instead of the jQuery passed one - Much easier to do than explain. If you're interested, I can setup a demo for that.

In any case, you can continue using what's working now. I'm just curious if my guess about the problem before was is right or not, though it almost has to be.

tonybabb
04-09-2015, 10:38 AM
Hello John,

Yes I'd like to try and understand what's happening with jQuery. Thanks for your explanation, I get the idea. Before I reached out to you I had tried several different attempts to get the pageX and pageY using jQuery. Here are the statements I tried and the console log results are shown in comments on each line.


$sndStartX = e.pageX; // returns "undefined"
$sndStartX = event.pageX; // returns "0"
$sndStartX = event.originalEvent.touches[0].pageX; // returns "Cannot read property 'touches' of undefined"
$sndStartX = event.originalEvent.pageX; // returns "Cannot read property 'pageX' of undefined

Tony

jscheuer1
04-09-2015, 11:20 AM
Those examples (with the exception of event.originalEvent.pageX) don't seem to include what I'm thinking of + I'm not sure if e and/or event was defined properly in them (including the one that looks sort of right to me) because I cannot see the parameter used for the passed event. When everything is right with that, the 'touches' are just where the fingers that already have contact are, not what we want and not anything in an event that starts with the first touch. The eventsignifier.originalEvent.pageX should be (if we have the right event signifier - e or event, whatever) sufficient for whatever device you are using because that's the jQuery version of what we used in the code that works. But my research shows me that perhaps some touch devices (Apple, perhaps others, even maybe some Android) require the eventsignifier.originalEvent.chanfedTouches[0].pageX - which may be why jQuery decided to treat this as non-standard, even though there is a standard for it (again eventsignifier.pageX is the W3C standard). But I think jQuery sometimes goes more with practices than standards in judging if an event has standard attributes or not.

In any case, after a bit of research I came up with this:


jQuery(function($){
var highlight = 'yellow', origcolor = 'transparent', curSnd = null;
function playstop(e){
e.preventDefault();
var $this = $(this);
if(curSnd && curSnd.sound){
if(this === curSnd.tag){
curSnd.sound.stop();
return;
}
curSnd.sound.stop();
}
$this.stop(true, true).css({backgroundColor: highlight});
var filename = this.href.substring(this.href.lastIndexOf('/')), myMedia = new Media(
this.href,
function() {
myMedia && myMedia.release();
curSnd = myMedia = null;
$this.stop(true, true).animate({backgroundColor: origcolor}, 500);
},
function(e) {
myMedia && myMedia.release();
curSnd = myMedia = null;
$this.stop(true, true).animate({backgroundColor: origcolor}, 500);
window.console && console.log ("Audio play error - " + filename + "\ncode: " + e.code + "\nmessage: " + e.message);
}
);
curSnd = {tag: this, sound: myMedia};
curSnd.sound.play();
} // End playstop
$('body').on('click touchstart touchend', '.spanish', function(e){
var oetouch = e.originalEvent.changedTouches? e.originalEvent.changedTouches[0] : e.originalEvent;
if(e.type === 'click'){
e.preventDefault();
} else if (e.type === 'touchstart'){
$(this).data({tstart: new Date().getTime(), tx: oetouch.pageX, ty: oetouch.pageY});
} else {
var $snd = $(this), ds = $snd.data('tstart'), vx, vy;
if(!ds){return;}
vx = Math.abs(oetouch.pageX - $snd.data('tx')); vy = Math.abs(oetouch.pageY - $snd.data('ty'));
if(vx < 20 && vy < 20 && new Date().getTime() - ds < 400){
playstop.apply(this, [e]);
$snd.data({tstart: null, tx: null, ty: null});
}
}
});
});

Which should work as long as the device has the original event changedTouches[0].pageX or the W3C standard original event pageX.

If you want more on the theory part (actual usages and their meanings), see:

http://www.javascriptkit.com/javatutors/touchevents.shtml

which I'm not certain is correct, but most likely is.

Also, if my code in this post works, it might be interesting to see whether it's also the changedTouches[0] or just the original event that carries the info (pageX, pageY). We know it is the latter at least because that, as I say, is what the code that currently works is using. Though that might only be valid for the specific device you are testing on. Also, if by some chance that device has changedTouches but not changedTouches[0].pageX, then this new code will likely fail. However, that circumstance (having changedTouches but not changedTouches[0].pageX) seems unlikely.

But if this code doesn't work we can change that part of it.

Also, now that we have working code, we can always go back to it if need be.

tonybabb
04-11-2015, 02:29 PM
Hello John,
The revised code worked perfectly, thank you again. I first tested in my Intel XDK development environment and then did a build/ download to my Samsung Galaxy S4 running Android and again it worked perfectly.

So, I thought I'd try and find where the pageX/Y info is carried so I added some console.log messages here


$('body').on('click touchstart touchend', '.spanish', function(e){
var oetouch = e.originalEvent.changedTouches? e.originalEvent.changedTouches[0] : e.originalEvent;
console.log("e.originalEvent.changedTouches = " + e.originalEvent.changedTouches);
console.log("e.originalEvent.changedTouches[0] = " + e.originalEvent.changedTouches[0]);
console.log("e.originalEvent = " + e.originalEvent);
console.log("oetouch = " + oetouch);
if(e.type === 'click'){
e.preventDefault();

For some reason when I tried it in my Intel XDK development environment sounds played using the default sound player - with an audio control bar. So, I did a build/ download and running on the device I get the same results - default sound player. I can't imagine how adding a few console.log statements can cause this.

I can try doing one at a time but didn't really understand

var oetouch = e.originalEvent.changedTouches? e.originalEvent.changedTouches[0] : e.originalEvent;

If I tried it one a time should I use

var oetouch = e.originalEvent.changedTouches[0] : e.originalEvent;

and then


var oetouch = e.originalEvent.changedTouches : e.originalEvent;

I found that "?" means optional but I couldn't understand where the ":" came in - still learning....

Tony

jscheuer1
04-11-2015, 03:49 PM
As long as there were no other errors, this probably means that Android follows the W3C recommendation (e.originalEvent.pageX in this case) and also doesn't have changedTouches (which may be Apple only, or at least not Android). If that's the case, your innocent enough looking code would error on (if I'm right, and there are no other errors, getting rid of this line will make the code work again):


console.log("e.originalEvent.changedTouches[0] = " + e.originalEvent.changedTouches[0]);

Because that would be asking for a property/index:


[0]

of an object (e.originalEvent.changedTouches) which doesn't exist. Code can usually, but not always, recover from one level deep of an undefined, but here neither changedTouches or [0] would exist, that's two deep. That doesn't have to be a fatal error (stops further processing), but it can be, and often is. Obviously, if it is the problem here, it's fatal in this case.



By way of explanation of the construct:


something = somethingElse? somethingElse[0] : someOtherThing;

The something on the left side is a variable we are either defining or redefining from earlier. The somethingElse? is a conditional. It's like saying, if there is a somethingElse, something will be defined as its (somethingElse's) [0] index, otherwise something will be defined as someOtherThing. All three members on the right hand side are required -

the condition? the result if the condition is satisfied : the result if it's not;

More formally and also valid for this:

if(somethingElse){something = somethingElse[0];} else {something = someOtherThing;}

The condition and the result if it's satisfied do not have to be related, but they usually are. This is just shorthand for an if/else chain and can be quite complex and/or nested with other constructs of the same or other types.

tonybabb
04-12-2015, 01:05 PM
Hi John,
I tried removing the line you suggested and - of course - it worked. Below is the console.log statements and below that the resulting console log from tapping on a dynamic element


$('body').on('click touchstart touchend', '.spanish', function(e){
var oetouch = e.originalEvent.changedTouches? e.originalEvent.changedTouches[0] : e.originalEvent;
console.log("e.originalEvent.changedTouches = " + e.originalEvent.changedTouches);
// console.log("e.originalEvent.changedTouches[0] = " + e.originalEvent.changedTouches[0]);
console.log("e.originalEvent = " + e.originalEvent);
console.log("oetouch = " + oetouch);
if(e.type === 'click'){




e.originalEvent.changedTouches = [object TouchList] index.html:54
e.originalEvent = [object TouchEvent] index.html:56
oetouch = [object Touch] index.html:57
e.originalEvent.changedTouches = undefined index.html:54
e.originalEvent = [object Event] index.html:56
oetouch = [object Event] index.html:57
e.originalEvent.changedTouches = [object TouchList] index.html:54
e.originalEvent = [object TouchEvent] index.html:56
oetouch = [object Touch]

You had mentioned that "changedTouches[0] may be used by apple devices, as my app will eventually be running on iPhones also I think I shoud just leave your original code there, it does no harm on Androids and may enable iPhones. Does that sound reasonable?

Thanks,

Tony

jscheuer1
04-12-2015, 03:54 PM
From what you have there, sometimes changedTouches is defined and sometimes not. This depends upon the event type. Certainly it would not be defined for a click event. In fact, every time changedTouches is defined, originalEvent is a touchEvent. When changedTouches is undefined, originalEvent is just an Event.

So I would say that the click handler is firing at some point, and that would certainly have caused the error with that line we removed. The rest of the time, looks like the code is using changedTouches[0] because it's there. However, our experience before with the non-jQuery approach demonstrates that changedTouches[0].pageX is not required. The e.pageX itself is enough, for Android at least. If we were to want to get more information out of the touch event we could use touches, changedTouches, and/or targetTouches. Each of these have different implications/meanings. Also, only with one of those can you get a list of touches in their category if one exists. The event itself contains only the pageX of the initial touch index of the event, just as one would expect from changedTouches[0]. If we were looking for a swipe or a pinch or reverse pinch, we would need more data on pageX and pageY than the event itself would have, then one or more of these touchLists would be required to interpret the event in its most useful way.

Here we are using chnagedTouches[0] instead of the event itself, but could be using either - at least for Android.

So, for a slightly different reason, I would agree we should keep the code as is to make sure it's compatible with other touch devices which just might not have the initial pageX in the event itself.