Variable Scope in JavaScript AddEventListner - javascript

For context, I am writing a plugin for omniture (adobe) sitecatalyst for video tracking. Before I write the plugin in the sitecatalyst format, I want to confirm it is working. I have tested the same code but using jQuery, and it works fine with how jQuery handles variables/scope. But doing it with Javascript directly is proving to be a little more difficult. Here is where I am at:
var vsa = new Array();
var vp = new Array();
vsa = document.getElementsByTagName('video');
if(vsa.length>0){
for(var vvv=0;vvv<vsa.length;vvv++) {
vsa[vvv].addEventListener('seeked',function () { if(vp[vsa[vvv].id]) { s.Media.play(vsa[vvv].id,vsa[vvv].currentTime); }},false);
vsa[vvv].addEventListener('seeking',function () { if(vp[vsa[vvv].id]) { s.Media.play(vsa[vvv].id,vsa[vvv].currentTime); }},false);
vsa[vvv].addEventListener('play',function () {
if(!vp[vsa[vvv].id]) {
vp[vsa[vvv].id] = true;
s.Media.open(vsa[vvv].id,vsa[vvv].duration,s.Media.playerName);
s.Media.play(vsa[vvv].id,vsa[vvv].currentTime);
} else {
s.Media.play(vsa[vvv].id,vsa[vvv].currentTime);
}},false);
vsa[vvv].addEventListener('pause',function () { if(vp[vsa[vvv].id]) { s.Media.stop(vsa[vvv].id,vsa[vvv].currentTime); }},false);
vsa[vvv].addEventListener('ended',function () { vp[vsa[vvv].id] = false; s.Media.stop(vsa[vvv].id,vsa[vvv].currentTime); s.Media.close(vsa[vvv].id); },false);
if (typeof vsa[vvv].error != 'undefined' && vsa[vvv].error) {
var scvt_msg = 'Error Not Captured';
if(typeof vsa[vvv].error.code != 'undefined') {
switch (vsa[vvv].error.code) {
case MEDIA_ERR_ABORTED:
scvt_msg = 'vsa[vvv]eo stopped before load.';
break;
case MEDIA_ERR_NETWORK:
scvt_msg = 'Network error';
break;
case MEDIA_ERR_DECODE:
scvt_msg = 'vsa[vvv]eo is broken';
break;
case MEDIA_ERR_SRC_NOT_SUPPORTED:
scvt_msg = 'Codec is unsupported by this browser';
break;
}
}
s.tl(this,'o','video: ' + scvt_msg);
}
}
}
On load, there is no error (meaning the eventlisteners are attaching correctly). When I press play on the video, I get a "vsa[vvv] is undefined". on the line of code that starts with
if(!vp[vsa[vvv].id])
Any ideas how to get the "global" vars of vsa, vp, and s accessible from the event listener function?
Thank you!

The variables are accessible. The problem is that you fell into the (very common) closures inside for loops trap.
Basically, all the event listeners are sharing the same vvv variable. By the time the event listeners run, vvv has gone through all the loop and is set to vsa.length, making vsa[vvv] undefined. (Check this out by adding a console.log(vvv) call to your event listeners)
The usual fix for this is creating the event listeners in a function outside the loop, to make each event listener get its own reference to a videotag variable.
function addMyEvents(node){
//the inner functions here get their own separate version of "node"
// instead of sharing the vvv
node.addEventListener('seeked',function () { if(vp[node.id]) { s.Media.play(node.id, node.currentTime); }},false);
node.addEventListener('seeking',function () { if(vp[node.id]) { s.Media.play(node.id, node.currentTime); }},false);
//...
}
for(var vvv=0;vvv<vsa.length;vvv++) {
addMyEvents(vsa[vvv]);
}
By the way, you don't need the if(vsa.length) test in the beginning since the loop wont run if the length is zero anyway...
To aboid this error in the future, run your code through a linter like JSHint or JSLint. They give warnings if you create functions inside a for loop.

Related

setting variables to be used in another function

Using edge animate, I don't seem to have the control over orders of operations that I am looking for. I have a function that operates a switch and another that tests the conditions of one or many switches. The problem I am running into is edge keeps wanting to run the test before the switch.
I can have the switch launch the test but run into the issue that I load the objects in an array inside of edge. What I am thinking is I need a way of pre-loading variables that the function can use but they don't need to be global since they are only used in this one function.
Here is what I have so far.
Inside Edge Animate:
twoPhaseSwitch('.btn1','on'); //sets up switch 1
twoPhaseSwitch('.swtch1','off'); //sets up switch 2
conditionsArray(['.btn1','.swtch1']); // tells the test script what the buttons are
In JavaScript file:
function twoPhaseSwitch(object,state)
{
var obj = $(object);
stage.getSymbol(obj).stop(state);
obj.data('state',state);
obj.mousedown(function(e)
{
if(obj.state == 'off')
{
stage.getSymbol(obj).stop('on');
obj.state = 'on';
obj.data('state','on');
}else{
stage.getSymbol(obj).stop('off');
obj.state = 'off';
obj.data('state','off');
};
});
};
function conditionsArray(obj)
{
var answers = ['on','on'];
// compare lengths
if (obj.length != answers.length)
return 'Argument Miscount';
var challengResults = challangeArray();
if (challengResults == true)
{
lightOn('.LED1','on');
}else if(challengResults == false)
{
lightOn('.LED1','off');
};
console.log(challengResults);
function challangeArray()
{
for (var i = 0, l=obj.length; i < l; i++)
{
if ($(obj[i]).data('state') != answers[i]) {
return false;
}
}
return true;
};
};
function lightOn(lightOBJ,state)
{
lightOBJ = $(lightOBJ);
stage.getSymbol(lightOBJ).stop(state);
};
I use mousedown and mouseup currently to fake the order of operations but it brings some pretty unacceptable issues so I am trying to do this right.
did you try wrapping your code in
$( document ).ready(){
your code here
}
to prevent any javascript from running until the page loads?

How do I use an array instead of the variables in this repetitive function?

So, I have this little code in my js file:
window.onload = function Equal() {
var a = 'b1'
var b = 'box1'
var bookstorname = localStorage.getItem(a)
if (bookstorname == 1) {
document.getElementById(b).setAttribute('checked','checked');
}
if (bookstorname == 0) {
document.getElementById(b).removeAttribute('checked','checked');
}
var a = 'b2'
var b = 'box2'
var bookstorname = localStorage.getItem(a)
if (bookstorname == 1) {
document.getElementById(b).setAttribute('checked','checked');
}
if (bookstorname == 0) {
document.getElementById(b).removeAttribute('checked','checked');
}
}
The function itself is not important (it equals checkboxvalues set in the localstorage), but I execute it 2 times. First time with var a & b set to 'b1' & 'box1'. Then I run the script again (same script), but with var a & b set to 'b2' & 'box2'. Now, this code works, but my question is if there is a shorter way to write this? I can imagine some sort of array with a loop, but I could not get it to work for some reason. The 2 variables are pairs, and I know this might be a dumb question, but I can't find the answer anywhere.
You can use a second function which will accept the local storage key and the checkbox id like
window.onload = function Equal() {
setCheckboxState('box1', 'b1');
setCheckboxState('box2', 'b2');
}
function setCheckboxState(id, key) {
document.getElementById(id).checked = 1 == localStorage.getItem(key);
}
You might separate common logic into another function
window.onload = function Equal() {
function extractFromStorage(a, b) {
var bookstorname = localStorage.getItem(a)
if (bookstorname == 1) {
document.getElementById(b).setAttribute('checked','checked');
}
if (bookstorname == 0) {
document.getElementById(b).removeAttribute('checked','checked');
}
}
extractFromStorage('b1', 'box1');
extractFromStorage('b2', 'box2');
}
function doTheStuff(a, b) {
var bookstorname = localStorage.getItem(a)
if (bookstorname == 1) {
document.getElementById(b).setAttribute('checked','checked');
}
if (bookstorname == 0) {
document.getElementById(b).removeAttribute('checked','checked');
}
}
window.onload = function Equal() {
doTheStuff('b1', 'box1');
doTheStuff('b2', 'box2');
}
?
This is how I would do it.
There are several problems with your code.
You do not check that the element you are stetting an attribute to
exists. You do not check if the localStorage item you get is
defined.
You pollute the global name space with the function name Equal.
That function should not be named with a capital as it is not a Object generator.
There is no need to use setAttribute and removeAttribute, in
fact removeAttribute makes no sense in this case as you can not
remove the checked attribute from the element. BTW why use setAttribute here and not for window.onload?
The checked attribute is either true or false, it does not use the
string "checked"
Binding the load event via the onload attribute is not safe as you may
block 3rd party code, or worse 3rd party code may block you.
There is no error checking. DOM pages are dynamic environments, pages
have adverts and content from many places that can interfer with your
code. Always code with this in mind. Check for possible errors and deal with them in a friendly way for the end user. In this case I used an alert, not friendly for a normal user but for you the coder.
My solution.
// add an event listener rather than replace the event listener
window.addEventListener(
"load", // for the load event
function(){
// the update function that is called for each item;
var update = function(item){
// the right hand side equates to true if the localstorage
// is equal to "1". LocalStorage allways returns a string or
// undefined if the key is not defined.
item.element.checked = localStorage[item.storageName] === "1";
}
// safe element getter
var getElement = function(eId){
var e = document.getElementById(eId); // try and get the element
if(e === null){ // does it exist?
throw "Missing element:"+eId; // no then we can not continue
// the program stops here unless
// you catch the error and deal with
// it gracefully.
}
return e; //ok return the element.
}
// Item creator. This creates a new item.
// sName is the local storage name
// eId id the element ID
var item = function(sName, eId){
return {
storageName: sName, // set the loaclStorage name
element:getElement(eId); // get the element and check its safe
};
}
// make it all safe
try{
// create an array of items.
var items = [
item("b1","box1"),
item("b2","box2")
];
// for each item update the element status
items.forEach(update);
}catch(e){
alert("Could not update page?");
}
}
);

Variables are being bound to the callback of a getCurrentLocation function. Why isn't this closure working?

I apologize for the amount of code, but I think this is actually a problem with AppMobi's getCurrentLocation function. Basically what happens is I delegate a tap event to each list element. Then when you tap it, it runs an asynchronous getCurrentLocation and updates some stuff. Then the next time you tap another list element, the variables bound in the callback of the getCurrentLocation Function only refer to the first time it was called. Why doesn't this work??
app = { events: [{text: "foo", time: new Date()}, {text: "bar", time: new Date()}] };
$(document).ready(refreshEvents);
function refreshEvents() {
for (var index in app.events) {
insertEventHTML(app.events[index]);
}
}
function insertEventHTML(event) {
var text = event.text;
var time = event.time;
var new_element = $('<li class="event_element"></li>');
var new_checkin_element = $('<div class="check_in_button"></div>');
new_checkin_element.bind('tap', function(e) {
check_in(e);
fade($(this), 1.0);
});
new_element.append(new_checkin_element);
var text_element = $('<div class="text_element">' + text + '</div>');
new_element.append(text_element);
var time_element = $('<div class="time_element">' + time + '</div>');
new_element.append(time_element);
$('#your_events').append(new_element);
}
function check_in(e) {
$(e.target).siblings('.time_element').text('just now');
var time = new Date(); // time and event_index are the trouble variables here
var event_index = getEventIndex(e); // the first time this function runs, event_index is correct
// then each subsequent time, it remains the same value as the first
if (!app.settings.use_location) {
app.events[event_index].times.unshift({time: time, location: null});
} else {
AppMobi.geolocation.getCurrentPosition(onLocationFound, errorFunction);
}
function onLocationFound(response) {
var lat = response.coords.latitude;
var lon = response.coords.longitude;
var last_time = app.events[event_index].times[0];
if (last_time != undefined && last_time.time == time) {
// event_index and time here will only ever refer to the first time it was called. WHY???
add_checkin(response, event_index, time);
}else{
console.log('onLocationFound was called twice');
}
}
function errorFunction(error) {
$.ui.popup({title: 'geolocation error', message: 'Geolocation error. Turn off location services in settings.'});
}
function add_checkin(response, event_index, time) {
// and then of course event_index and time are wrong here as well. I don't get it.
app.events[event_index].times.unshift(
{
time: time,
location: {
latitude: response.coords.latitude,
longitude: response.coords.longitude
}
});
AppMobi.cache.setCookie('app', JSON.stringify(app), -1);
}
}
function getEventIndex(e) {
var target = $(e.target).parent();
var siblings = target.parent().children('li');
for (var i = 0; i < siblings.length; i++) {
if ($(target)[0].offsetTop == $(siblings[i])[0].offsetTop) {
return i;
}
}
}
Well, your issue seems to be that you are declaring a private variable event_index inside the check_in function and try to resolve it's value by accessing a global event_index variable inside onLocationFound.
Here's what you could do instead:
AppMobi.geolocation.getCurrentPosition(function (response) {
onLocationFound(response, event_index);
}, errorFunction);
function onLocationFound(response, event_index) { //... }
EDIT:
it is declared within check_in...
You are right, I totally missed that somehow. Well in that case it's very unlikely that the event_index variable inside onLocationFound isin't the same as in check_in. Do a console.log(event_index) inside onLocationFound and it should be the same. The only way it could be different is if you modify the local variable before the handler is called, which you doesn't seem to do, or if getCurrentPosition stores the first handler somehow and ignores subsequent handlers, but this API wouldn't make any sense.
EDIT 2:
As we suspect the handler might not be registered correctly the second time, I would suggest to check this way:
function check_in() {
if (!check_in.called) {
check_in.called = true;
check_in.onLocationFound = onLocationFound;
}
//...
function onLocationFound() {
console.log(arguments.callee === check_in.onLocationFound);
}
}
You can also simple do onLocationFound.version = new Date() and check arguments.callee.version to see if it stays the same.
I know you marked this answered already but....it might not actually be a problem with the library. Can I direct you to a post by #Joel Anair where he has posted an article and the example number five seems to be the "gotcha" which might have gotcha ;)
How do JavaScript closures work?
Basically in the for loop they are all being set to the same reference of i so event_index will all be the same value/reference. (which explains why they're all the same).

Difficulty developing unique dynamic onclick events in Javascript

I am working with a decent sized set of data relating to objects on the page and some objects need links applied to them onclick. The link to connect to is part of the dataset and I build a string for the link with the variable linkTarget and apply it like so.
if (dataTag[i][3]==true){
if(prepend==undefined || prepend=="undefined"){
var linkTarget=ResultsJSON["targetUrl"];
ele.onclick = function(){
window.open(linkTarget);
};
} else {
var linkTarget=prepend+ResultsJSON["targetUrl"];
ele.onclick = function(){
window.open(linkTarget);
};
}
ele refers to an element picked up with getElementByID. Now I am going through quite a few objects and the problem I have is the onclick for every object is the last value of linkTarget. This is all contained in a function and link target is a local variable so I have no idea why. I have tried using an array with something like
ele.onclick=function(){window.open(linkTarget[linkTarget.length-1]);};
and even
ele.onclick=function(){window.open(linkTarget.valueOf());};
with the same results. I am at a loss now and would appreciate any help.
Use Array.forEach() to iterate your data and watch your troubles melt away.
dataTag.forEach(function (item) {
if (item[3]==true) {
var linkTarget = "";
if (prepend==undefined || prepend=="undefined") {
linkTarget = prepend;
}
linkTarget += ResultsJSON.targetUrl;
ele.onclick = function () {
window.open(linkTarget);
};
}
});
See this compatibility note for using Array.forEach() in older browsers.
You're in a loop — therefore, you need to put your things-to-be-executed in another function, like so:
if(dataTag[i][3]) {
if(prepend) {
(function(linkTarget) {
ele.onclick = function() {
window.open(linkTarget);
};
})(ResultsJSON.targetUrl);
} else {
(function(linkTarget) {
ele.onclick = function() {
window.open(linkTarget);
};
})(ResultsJSON.targetUrl);
}
I also made some general corrections.

How to detect creation of new global variables?

I want watch for the creation of new global variables in Javascript so that, anytime a global variable is created, an event is fired.
I've heard of the watch() function but that is only for watching for specific variable names. I want a catchall.
If you already know which names pollute your global namespace (see Intercepting global variable definition in javascript), you can use this trick to figure out when does it actually happen:
window.__defineSetter__('someGlobalVar', function() {
debugger;
});
Be sure to have your developer tools open when you run this.
Obviously works only if your browser supports __defineSetter__ but that's true for modern browsers. Also, don't forget to remove your debug code after you've finished.
Found here.
I don't know how to make this work "on demand" as soon as a var is created, but I can suggest a polling approach. In a browser window, all global become a member of the global "window" object. (Because technically, "window" is the "global object"). So you could do something like the following:
1) enumerate all the properties on a window
window.proplist = window.proplist || {};
for (propname in window) {
if (propname !== "proplist") {
window.proplist[propname] = true;
}
}
2) Set a timer to periodically "poll" window for new properties
setInterval(onTimer, 1000);
3) Wake up on the timer callback and look for new props
function onTimer() {
if (!window.proplist) {
return;
}
for (propname in window) {
if (!(window.proplist[propname])) {
window.proplist[propname] = true;
onGlobalVarCreated(propname);
}
}
}
Afaik, .watch() is only SpiderMonkey (Firefox).
I played around with a polling function, I finally came up with this:
var mywatch = (function() {
var last = {
count: 0,
elems: {}
};
return function _REP(cb) {
var curr = {
count: 0,
elems: {}
},
diff = {};
for(var prop in window) {
if( window.hasOwnProperty(prop) ) {
curr.elems[prop] = window[prop]; curr.count++;
}
}
if( curr.count > last.count ) {
for(var comp in curr.elems) {
if( !(comp in last.elems) ) {
diff[comp] = curr.elems[comp];
}
}
last.count = curr.count;
last.elems = curr.elems;
if(typeof cb === 'function')
cb.apply(null, [diff]);
}
setTimeout(function() {
_REP(cb);
}, 400);
};
}());
And then use it like:
mywatch(function(diff) {
console.log('NEW GLOBAL(s): ', diff);
});
Be aware that this only handles new globals. But you can easily extend this for the case last.count > curr.count. That would indicate that global variables were deleted.
You cant have an event fired when some script does var v = 10, but as selbie said, you can poll the window object... I meant to suggest the same, but he beat me to it. Here's my other example... you count how many window objects are there, and execute GlobalVarCreated() function:
var number_of_globals = 0; //last known globals count
var interval = window.setInterval(function(){
var new_globals_count = 0; //we count again
for(var i in window) new_globals_count++; //actual counting
if(number_of_globals == 0) number_of_globals = new_globals_count; //first time we initialize old value
else{
var number_of_new_globals = new_globals_count - number_of_globals; //new - old
if(number_of_new_globals > 0){ //if the number is higher then 0 then we have some vars
number_of_globals = new_globals_count;
for(var i = 0; i<number_of_new_globals; i++) GlobalVarCreated(); //if we have 2 new vars we call handler 2 times...
}
}
},300); //each 300ms check is run
//Other functions
function GlobalVarCreated(){}
function StopInterval(){window.clearInterval(interval);}
You can load up that code in Chrome or FF console only change: function GlobalVarCreated(){console.log("NEW VAR CREATED");} and test it:
var a = 10
b = 10
String NEW VAR CREATED is displayed 2 times.

Categories