Imbricated loops + timeout sometimes fails - javascript

I made a little script to practice and it seems I have a problem with my loops and their timeouts.
Here's the link to my script : http://codepen.io/JulienBarreira/pen/EWNoxJ
Sometimes when a word is writing, one or two letters are wrong. For example, instead of "cheeseburger", I get "chkesebxrger".
I found a little trick so it fails less, but i don't know why at all.
function charsAnim(i, word, j) {
setTimeout(function() {
var count = j;
if (j < steps) {
randomChar(i, word, count, j);
} else {
goodChar(i, word, count, j);
}
/* seems it fails less if I divide j, don't know why */
}, (speed/steps)*(j / 1.8));
}
The problems appears more often when others scripts are running on the computer (for example in my profile page).
Feel free to give me any advices about my code even if it's not about my problem. There's problably an easier way to do the same thing and i'm here to progress.
Thanks :)
Edit : I added 3 iframes in a snippet to show you the problem, when you start the snippet, the first word fails most of the time.
var words = [
'unicorn',
'cheeseburger',
'pizza',
'pineapple',
'popsicle',
'bubbles',
'seagull',
'doodle',
'goggles',
'artichoke',
'potato',
'carrot',
'vegeta'
];
var letters = "abcdefghijklmnopqrstuvwxyz#%&^+=-";
var speed = 250;
var steps = 4;
function getRandomWord() {
var randomWord = words[Math.floor(Math.random() * words.length)];
return randomWord;
}
function getRandomLetter() {
var randomLetter = letters[Math.floor(Math.random() * letters.length)];
return randomLetter;
}
function randomWordLoop() {
var word = getRandomWord();
var textLength = word.length;
for(i = 0; i < textLength; i++) {
letterAppear(i, word);
}
function letterAppear(i, word) {
setTimeout(function() {
randomLetters(i, word);
}, speed*i);
}
function randomLetters(i, word) {
for (j = 0; j <= steps; j++) {
charsAnim(i, word, j);
}
}
function charsAnim(i, word, j) {
setTimeout(function() {
var count = j;
if (j < steps) {
randomChar(i, word, count, j);
} else {
goodChar(i, word, count, j);
}
/* seems it fails less if I divide j, don't know why */
}, (speed/steps)*(j / 1.8));
}
function randomChar(i, word, count, j) {
var letter = getRandomLetter();
if (j > 0) {
var oldText = $('#loader').text().slice(0, -1);
} else {
var oldText = $('#loader').text();
}
$('#loader').text(oldText + letter);
}
function goodChar(i, word, count, j) {
var oldText = $('#loader').text().slice(0, -1);
$('#loader').text(oldText + word[i]);
if (i == textLength - 1 ) {
removeWord();
}
}
function removeWord() {
setTimeout(function() {
for (k = 0; k < textLength; k++) {
removeLetters(k);
}
}, speed*2);
}
function removeLetters(k) {
setTimeout(function() {
removeLetter(k);
}, 75*k);
}
function removeLetter(k) {
var actualText = $('#loader').text().slice(0, -1);
$('#loader').text(actualText);
if (k == textLength - 1) {
randomWordLoop();
}
}
}
randomWordLoop();
body {
background-color: #010101;
}
.loader {
width: 300px;
color: #0c9c73;
text-align: left;
font-size: 50px;
font-family: Roboto Mono;
font-weight: 700;
text-transform: uppercase;
}
.loader:after {
content:'_';
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<link href="https://fonts.googleapis.com/css?family=Roboto+Mono:400,700" rel="stylesheet">
<div class="loader" id="loader"></div>
<iframe width="560" height="315" src="https://www.youtube.com/embed/GquEnoqZAK0?autoplay=1" frameborder="0" allowfullscreen></iframe>
<iframe width="560" height="315" src="https://www.youtube.com/embed/GquEnoqZAK0?autoplay=1" frameborder="0" allowfullscreen></iframe>
<iframe width="560" height="315" src="https://www.youtube.com/embed/GquEnoqZAK0?autoplay=1" frameborder="0" allowfullscreen></iframe>

You are combining this loop, inside randomWordLoop()
for(i = 0; i < textLength; i++) {
letterAppear(i, word);
}
... with the setTimeout() inside letterAppear(). Basically, when letterAppear() executes inside letterAppear, i variable no longer has the same value as when you set the timeout. It has it's global value, which might have already been set to a completely different value by any other function in your page that might use i.
Also, please note the proper way to set your for would be not to use i globally, but rather set it as a local var of your function: for(var i = 0; i < textLength; i++) {...}.
You're not able to properly see it, as your function outputs random letters and there's no visual clue letting you know it runs on wrong values of i, but I believe your function is wrong most of the time.
To fix this, you need a closure in letterAppear() which will pass the correct values of i and word to the setTimeout() inside randomLetters(), irrespective of their global value when the contents of the setTimeout() executes:
for(var i = 0; i < textLength; i++) {
(function(i,word){
letterAppear(i, word);
})(i,word)
}
Looking closer to your code, you might need closures in more than one place (if it's important that the values you pass to the functions are the same at code execution) and you should also define the for iterators (i and j) locally, using var, like I did above.
Don't forget your best JavaScript friend ever:
console.log(this, arguments);

Related

JQuery - How can I check my cards in memory game?

I have a course where I learn JS+JQuery and I have to write a memory game. I want to check if the turned cards are in the same classes so I can delete them and count the points, but I just cannot do it. I tried a lot of ways but I cannot figure it out.
I tried with .is(), === but nothing...
Here's my code:
let gameArea;
let size = 6;
let card_size = 600 / size;
let images = ['arbalest', 'armored', 'arms', 'cannoner', 'cataphract', 'cavalier', 'centurion', 'champion', 'composite',
'conquistador', 'eagle', 'heavy', 'heavyhorse', 'hussar', 'knight', 'legion', 'paladin', 'skirmisher'
];
$(function() {
gameArea = $('<div></div>');
gameArea.appendTo('body');
gameArea.attr('id', 'gameArea');
drawMap();
gameArea.on("click", "div", function(e) {
if ($(e.target).is(".unturned")) {
$(e.target).removeClass("unturned");
}
let unturnedCardNum = $("#gameArea").find("div:not(.unturned)").length;
if (unturnedCardNum === 3) {
$("#gameArea").find("div:not(.unturned)").addClass("unturned");
}
//I'd like to check here
});
});
function generate() {
let generatedImage = images[Math.floor(Math.random() * images.length)];
if ($("#gameArea").find(`.${generatedImage}`).length === 2) {
images = images.filter(function(e) {
return e !== generatedImage
});
generatedImage = generate();
}
return generatedImage;
}
function drawMap() {
for (let i = 0; i < size; i++) {
for (let j = 0; j < size; j++) {
let pic = $('<div></div>');
let shuffledImages = generate();
pic.addClass(shuffledImages);
pic.addClass("unturned");
pic.css({
width: card_size,
height: card_size,
top: i * card_size,
left: j * card_size
});
pic.appendTo(gameArea);
}
}
}
Assuming that the string in the images array will have been assigned to the associated card as a class name and that, after removing the "unturned" class, this will be the only class name left then maybe the following might help you:
Inside the $("#gameArea").on("click"...) function replace the lines
let unturnedCardNum = $("#gameArea").find("div:not(.unturned)").length;
if (unturnedCardNum === 3) {
$("#gameArea").find("div:not(.unturned)").addClass("unturned");
}
with
let unturnedCards = $("#gameArea").find("div:not(.unturned)");
switch(unturnedCards.length){
case 3:
unturnedCards.addClass("unturned");
break;
case 2:
if (unturnedCards[0].className===
unturnedCards[1].className){
console.log("you found a pair!");
// score[currentPlayer]++;
}
}
Further hint:
Instead of calling the generate() function recursively, you could simply shuffle an array made up of two copies of the images array by using a Fisher-Yates algorithm:
function shuffle(a,n){ // shuffle array a in place (Fisher-Yates)
let m=a.length;
n=n||m-1;
for(let i=0,j;i<n;i++){
j=Math.floor(Math.random()*(m-i)+i);
if (j-i) [ a[i],a[j] ] = [ a[j],a[i] ]; // swap 2 array elements
}
}

Rainbowcolored-string from left to right

I just recently startet with Javascript and had the following Idea:
I would like to have short text (maybe just a headline) on my website which single chars will have a rainbow-color that travells from left to right.
So I wrote this short script.
var Count = 6;
setInterval(function RainbowColorFunction()
{
var Rainbow_Colors = ["#FFFF00","#FF7F00","#FF0000","#9400D3","#4B0082","#0000FF","#00FF00"];
var Color_Element = document.getElementById("RainbowColorText");
var Color_String = Color_Element.textContent;
var Letter = "";
var NewText = "";
var RainbowCount = Count;
var Stringlenght = Color_String.length;
Color_String = reverse(Color_String);
for (var i = Stringlenght, min = 0; i > min; i--)
{
Letter = Color_String.charAt(i -1);
if(Letter == " ")
{
NewText += Letter;
continue;
}
NewText += Letter.fontcolor(Rainbow_Colors[RainbowCount]);
RainbowCount--;
if(RainbowCount < 0){RainbowCount = 6;}
}
Count--;
if(Count < 0){Count = 6;}
Color_Element.innerHTML=NewText;
}, 60);
function reverse(s) {
return (s === '') ? '' : reverse(s.substr(1)) + s.charAt(0);
}
My Issue is now that the text changes colour from right to left. But I want it the other way around. Without the reverse Function my text is a big mess, but I am quite sure thats the point where I have to change things.
Short answer: just increment instead of decrement Count:
Count++;
if(Count>6 ){Count = 0;}
Other observations:
it's "length" not "lenght"
try to use singlequotes in Javascript and doublequotes for html
attributes. You will see that writing html in javascript and the
reverse become simple.
also the naming conventions for Javascript usually use lowercase
Camel variables, uppercase is reserved for class names, public
members maybe, etc. Whatever you use, be consistent after you choose.
you can move a lot of the function outside the interval, like the
part that gets the element.
you don't need to name the function that you give setInterval as an
attribute. Alternately you can name it then use
setInterval(functionName,500) on a separate line.
If I would do it, I would try to encapsulate it better. Here is my 5 minute effort:
function RainbowColor(elem)
{
this.Colors=["#FFFF00","#FF7F00","#FF0000","#9400D3","#4B0082","#0000FF","#00FF00"];
this.Speed=16.66;
this.Direction=1;
this._offset=0;
this._elem=elem;
this._originalContent=elem.innerHTML;
}
RainbowColor.prototype={
Colorize:function() {
var self=this;
function mod(v,l) {
var result=v%l;
return result<0?result+l:result;
}
function rainbowColorFunction() {
var text=self._elem.textContent;
var result='';
var k=self._offset;
for (var i=0; i<text.length; i++) {
var letter=text.charAt(i);
if (!letter) continue;
result+=letter.fontcolor(self.Colors[mod(k,self.Colors.length)]);
k-=self.Direction;
}
self._elem.innerHTML=result;
self._offset++;
}
this._interval=setInterval(rainbowColorFunction,1000/self.Speed);
},
Stop:function() {
if (this._interval) clearInterval(this._interval);
},
Restore:function() {
this._elem.innerHTML=this._originalContent;
}
}
var colorizer=new RainbowColor(document.getElementById('RainbowColorText'));
colorizer.Colorize();
setTimeout(function() { colorizer.Direction=-1; },5000);
setTimeout(function() { colorizer.Colors=['lightgray','gray','black'] },10000);
setTimeout(function() { colorizer.Speed=10; },15000);
setTimeout(function() { colorizer.Stop(); },20000);
setTimeout(function() { colorizer.Restore(); },23000);
I also looked into using CSS to create a text gradient, but I couldn't find any cross-browser solution (More details : How do I use a gradient as a font color in CSS?)
Speaking of cross-browser, perhaps you should use jQuery to get and set HTML content in order for this to work everywhere correctly.

For Loop refusing to Loop; Works Perfectly Fine if Run Manually

Alright, so, hopefully this is a simple enough question. I'm sure that it has to be something simple and stupid that I am not doing right. I'm building an app to program my exercise routine into so I can keep track of it every week (and to learn app development using Cordova). Within the code, each time I come back to the exersetup screen I want to read the saved exercise file, parse it back to an array of objects, convert those objects back into exercise objects, then display them on the screen. I have a loop that is supposed to check each element of the array to process the information, then display, then repeat until it has done this for each element in the array. It works perfectly fine for the first item, then stops. If I manually run the loop multiple times, it functions perfectly. Not listed is the code for the function addRow(), which simply adds a row of formatted dropdown selectors for input, then increments rowNum to keep track of which row it is operating on. Any help you guys can offer is appreciated. If I need to submit more information I would be happy to do so. Here's the loop in question:
EDIT: Fixed some things to the suggestions in the comments below. Code still functions the same. Works perfectly fine on first run through loop, then stops. If I continue to run the loop manually (declaring it over and over again in console) it works fine, but it will not, for some reason, run the "for" loop. I've also included the rest of the function that the for loop is nested in, in case that helps.
function readRout()
{
console.log("Reading routine...");
document.addEventListener('deviceready', getFile, false);
function getFile()
{
window.resolveLocalFileSystemURL(cordova.file.dataDirectory, function(dir)
{
console.log("got main dir",dir);
dir.getFile("exerciseroutine.txt", {create:true}, function(file)
{
console.log("got the file", file);
exerLog = file;
console.log(exerLog);
});
});
document.addEventListener('deviceready', logOut, false);
}
function logOut()
{
setTimeout(readLog, 50);
}
function readLog()
{
exerLog.file(function(file)
{
var reader = new FileReader();
reader.onloadend = function(e)
{
console.log(this.result);
exerRoutine = JSON.parse(this.result);
//console.log(exerRoutine);
//console.log(exerRoutine[0]);
var looper = exerRoutine.length + 1;
for (var i=0; i<looper; i++)
{
console.log("Loop count is: " + i);
console.log(looper);
exerRoutine[i] = new exercise(exerRoutine[i].type, exerRoutine[i].name, exerRoutine[i].sets, exerRoutine[i].reps, exerRoutine[i].pace);
console.log(exerRoutine[i]);
console.log(exerRoutine[i].description());
console.log(exerRoutine[i].type);
console.log(exerRoutine[i].name);
console.log(exerRoutine[i].sets);
console.log(exerRoutine[i].reps);
console.log(exerRoutine[i].pace);
addRow();
setTimeout(fillExer, 50);
console.log(exerRoutine);
}
console.log(looper);
};
reader.readAsText(file);
}, fail);
}
function fillExer()
{
var typeRow = "typeSel" + rowNum;
console.log(typeRow);
console.log(document.getElementById(typeRow));
var nameRow = "nameSel" + rowNum;
var setsRow = "setsSel" + rowNum;
var repsRow = "repsSel" + rowNum;
var paceRow = "paceSel" + rowNum;
fillType(typeRow);
setTimeout(fillName, 10, nameRow);
fillSets(setsRow);
fillReps(repsRow);
fillPace(paceRow);
}
function fillType(typek)
{
for (var k=0; k<document.getElementById(typek).options.length; k++)
{
//console.log(document.getElementById(typek).options.length);
console.log(exerRoutine[rowNum-1].type);
if(document.getElementById(typek).options[k].value == exerRoutine[rowNum-1].type)
{
document.getElementById(typek).selectedIndex = k;
//console.log(document.getElementById(typei).selectedIndex);
var quer = "#" + typek;
//console.log(query);
var typeInput = document.querySelector(quer);
//console.log(typeInput);
TypeSelect(typeInput);
return;
}
}
}
function fillName(namek)
{
for (var k=0; k<document.getElementById(namek).options.length; k++)
{
//console.log(document.getElementById(namek).options.length);
console.log(exerRoutine[rowNum-1].name);
if(document.getElementById(namek).options[k].value === exerRoutine[rowNum-1].name)
{
document.getElementById(namek).selectedIndex = k;
//console.log(document.getElementById(namei).selectedIndex);
return;
}
}
}
function fillSets(setsk)
{
for (var k=0; k<document.getElementById(setsk).options.length; k++)
{
console.log(exerRoutine[rowNum-1].sets);
if(document.getElementById(setsk).options[k].value === exerRoutine[rowNum-1].sets)
{
document.getElementById(setsk).selectedIndex = k;
//console.log(document.getElementById(setsk).selectedIndex);
return;
}
}
}
function fillReps(repsk)
{
for (var k=0; k<document.getElementById(repsk).options.length; k++)
{
console.log(exerRoutine[rowNum-1].reps);
if(document.getElementById(repsk).options[k].value === exerRoutine[rowNum-1].reps)
{
document.getElementById(repsk).selectedIndex = k;
//console.log(document.getElementById(repsi).selectedIndex);
return;
}
}
}
function fillPace(pacek)
{
for (var k=0; k<document.getElementById(pacek).options.length; k++)
{
console.log(exerRoutine[rowNum-1].pace);
if(document.getElementById(pacek).options[k].value === exerRoutine[rowNum-1].pace)
{
document.getElementById(pacek).selectedIndex = k;
//console.log(document.getElementById(pacek).selectedIndex);
return;
}
}
}
}

Display All Frames List for any WebPage

I want to create a recursive JavaScript loop which can display all the Frames (either using IFRAMES Tag or using FRAME Tag). The result should give FrameName, NestingLevel, FrameURL, etc.
For example, when we open 'Developer Tools' in Google Chrome, we can see in all the FRAMES(even nested Frames too) in the console tab.
Please give a recursive loop with error handling (iframe security issue) for the same.!
Sample Code:
function getFramesInfo(oWin) {
arrFrames = oWin.frames;
//alert(arrFrames.length);
if (arrFrames.length == 0) {
alert('document do not have frame(s)');
} else {
for (i = 0; i < arrFrames.length; i++) {
console.log(i + ' ' + arrFrames[i].location.href);
if (arrFrames[i].document) {
getFramesInfo(arrFrames[i]);
}
}
}
}
//Calling This Function
getFramesInfo(window);
Alternate / Improved Code:
function getAllFrames(frame, allFrameArray) {
allFrameArray.push(frame.frames);
for (var i = 0; i < frame.frames.length; i++) {
getAllFrames(frame.frames[i], allFrameArray);
}
return allFrameArray;
}
//Usage:
var allFramesArray = getAllFrames(window, new Array());
console.log(allFramesArray.length); // number of frames in array
for (var i = 0; i < allFramesArray.length; i++) {
console.log('\n\n No.'+ (i+1) + '.');
try {
console.log(allFramesArray[i].id);
console.log(allFramesArray[i].name);
console.log(allFramesArray[i].location.href); // location of first frame in array}
} catch (e) {
console.log('Error in Accessing Frame No.' + (i+1) ); // location of first frame in array}
}
}
After googling and exploring I have found a simple solution, but I think this answer should be improved to avoid any possible errors (i.e. precise error handling should be done to avoid cross-domain frames issue):
Sample Solution:
// first parameter is frame (window) in which to get sub frames
// second is the array to build on
function getAllFrames(frame, allFrameArray) {
allFrameArray.push(frame.frames);
for (var i = 0; i < frame.frames.length; i++) {
getAllFrames(frame.frames[i], allFrameArray);
}
return allFrameArray;
}
Usage:
var allFramesArray = getAllFrames(window, new Array());
console.log(allFramesArray.length); // number of frames in array
for (var i = 0; i < allFramesArray.length; i++) {
try {
console.log(allFramesArray[i].location.href); // location of first frame in array}
} catch (e) {
console.log('Error in Accessing Frame No.' + i); // location of first frame in array}
}
}
Here is another one (shortcut example)
var i = new Array;
function wow(a, n) {
t = '';
for (i[n] = 0; i[n] < a.length; i[n]++) {
try {
t += a[i[n]].name + ' \n';
} catch (e) {
t += 'Error \n';
}
if (a[i[n]].frames.length) t += wow(a[i[n]].frames, n + 1);
}
return t;
}
//Usage:
console.log(wow(top.frames, 0));
While browsing for a code, I recently have learnt a new code, using this code which takes a javascript window object as input and returns all its nested windows (including multiple levels of nesting). This function however is an efficient/elegant way that can be used to grab a reference to all the windows (objects) and all the child windows (objects). I have checked this function and found that it works well, it could be understood that making/writing this function completely recursively, as the DOM object tree is actually an n-ary, not binary.
function getAllNestedWindows(window) {
var windows = [window];
if (window.length === 0) {
return windows;
} else {
for (var i = 0; i < window.length; i++) {
windows = windows.concat(getAllNestedWindows(window[i]));
}
}
return windows;
}

Echoing the "then" state of variable in a mootools event even after var has changed

Let's take some pseudo-Mootools code:
for loop (i) from 0 to 5 {
create.element {
id: 'element_'+i
}
$('element_'+i).addevent.click {
alert(i);
}
}
The events get added to the elements properly. However, when clicked, they'll all alert the latest iterator... which would be 5.
Is there any way I can change "alert(i)" in the event to alert the iterator at that point in time?
.toString didn't do much.
http://www.jsfiddle.net/dimitar/sbytJ/1/
for (var ii = 0; ii < 5; ++ii) {
new Element("div#id_" + ii + ".monkey[text=click me]").store("id", ii).addEvent("click", function() {
alert(this.retrieve("id") + " id: " + this.get("id"));
}).inject(document.body);
}
using the element storage (element.store("id", ii) / this.retrieve("id")) is one way of ensuring correct value reference at runtime...
another way via an anonymous function: http://www.jsfiddle.net/dimitar/sbytJ/2/
for (var ii = 0; ii < 5; ++ii) {
(function(id) {
new Element("div#id_" + id + ".monkey[text=click me]").addEvent("click", function() {
alert(id);
}).inject(document.body);
})(ii);
}
etc etc. i am sure there are other ways to refactor this also

Categories

Resources