BOLD denotes updates.
I have an array, steps, whose contents are objects with an action and element associated with them. Like so:
steps = [{action: 'click', element: <jQuery element>},
{action: 'click', element: <jQuery element>}, ., ., N]
I would like to implement a runner, whose job is to run through each element in the array and perform the particular action upon the element. Each step must be performed in serial. So for example, if you have:
steps = [{action: 'click', element: <jQuery element representing a button>},
{action: 'click', element: <jQuery element representing an anchor tag>}]
Running, run(steps, timeout), would run through each step. step[0].action would be performed on step[0].element. Since it is possible for step[0] to create the dom element (by means of AJAX) to be interacted with in step[1], the runner needs to wait a particular period(hence, the timeout), polling the dom for the presence of step[1].element.
Here is a rough take at what I have so far:
var run = function() {
$.each(steps, function(i, v) {
var interval = 25,
start = 0,
timeout = 3000;
var i = setInterval(function(timeout) {
start = start + interval;
console.log(start);
if ($(v).is(':visible')) {
v.click();
console.log('clicked', v);
clearInterval(i);
}
}, interval);
});
};
Note that in the above example, steps is just an array of jquery objects. It isn't in the desired format just yet:
steps = [{action: 'click', element: <jQuery element>},
{action: 'click', element: <jQuery element>}, ., ., N]
What is the 'pattern' so to say, that I need to follow? Do I need to use deferred objects to take care of this? Is it implemented with a setTimeout, setInterval? Thanks!
Final implementation
var run = function(steps, interval, timeout) {
var timer,
time = 0,
i = 0;
runSingle(steps[0]);
function abort() {
console.log("Run aborted");
}
function runSingle(step) {
timer = setInterval(function() {
time += interval;
if ($(step.element).is(':visible') === true) {
clearInterval(timer);
time = 0;
$(step.element).trigger(step.action);
(i < (steps.length - 1)) && runSingle(steps[++i]);
} else if (time >= timeout) {
clearInterval(timer);
abort();
}
}, interval);
console.log("Performed: ", step.action, "on", step.element)
if (i === (steps.length - 1)) console.log("Run successful");
}
}
Here's something. I haven't tested it thoroughly:
var run = function(steps, interval)
{
var timer,
time = 0, timeout = 10000,
ciel = steps.length - 1,
i = 0;
run_single(steps[0]);
function run_single(item)
{
timer = setInterval(function()
{
var $el = $(item.selector);
time += interval;
if ( $el.length )
{
clearInterval( timer );
time = 0;
$el.trigger( item.action );
i < ciel && run_single( step[ ++i ] );
}
else
{
if ( time >= timeout ) clearInterval( timer );
}
}, interval);
}
};
var steps = [
{action: 'click', selector: '#first'},
{action: 'hover', selector: '#second'},
{action: 'change', selector: '#third'}
// and so on...
];
run(steps, 100);
See it here in action: http://jsfiddle.net/myaeh/
First up, note that in your example the v variable will represent an object from your array and thus it doesn't make sense to say v.click() or $(v).is(':visible') - you'd want to say v.element.click() or v.element.is(':visible').
If you mean that action will be a string that is the name of a jQuery method, and element is a jQuery object, then you can do something like this:
$.each(steps, function(i, obj) {
obj.element[obj.action]();
});
If element is a string representing a selector that should be used to create a jQuery object then:
$.each(steps, function(i, obj) {
$(obj.element)[obj.action]();
});
You don't need to introduce the polling concept unless the action might do something asynchronously, for example, if it does a fade-in, or adds elements via Ajax.
In your example the only criterion you seem to be applying for whether to proceed with the current step is whether the current element is visible. If that's the case you can do something like this:
var run = function(steps, delay, timeout) {
var i = 0,
nextStep = function() {
if (i < steps.length) {
var step = steps[i],
retryDelay = 25,
retryTotal = 0,
intervalId = setInterval(function() {
retryTotal += retryDelay;
var $el = $(step.element);
if ($el.is(':visible')) {
$el[step.action]();
clearInterval(intervalId);
i++;
setTimeout(nextStep, delay);
} else if (retryTotal >= timeout) {
clearInterval(intervalId);
}
}, retryDelay);
};
}
nextStep();
};
run(steps, 50, 3000);
The run() function defines a nextStep() function that uses setInterval to keep checking whether the current element is visible. Once it is, it performs the action, clears the interval, and moves on to the next element by calling itself via setTimeout.
I wasn't sure how to fit the timeout concept in with the polling, because if the current element isn't visible after the specified amount of time what would you do? You can't really continue on to the next element because it too might depend on previous steps. I guess you could just abort the whole thing by clearing the interval and not calling nextStep() again. EDIT: I've updated the code to work as per that last sentence.
Related
Cannot terminate the setInterval I created in launch. It works until the time is up. I want to use clearInterval (interval) operation in next() function and prev() function. How should I do this? When I click forward, I want clearInterval(interval) to run this, but I couldn't.
function launch() {
thisTimeline = document.getElementsByClassName('story-active-' + start)[0];
var maxtime = 5000;
var incremental = 100;
var actualtime = 0;
var interval = setInterval(function() {
actualtime += incremental;
var percentage = Math.ceil((100 / maxtime) * actualtime);
thisTimeline.style.width = percentage + '%';
if (percentage == 100) {
clearInterval(interval);
thisTimeline.style.width = "0%";
}
}, incremental);
}
function next() {
// Set previous video timeline to 100% complete
thisTimeline.style.width = '100%';
// Advance play count to next video
start++;
// If next video doesn't exist (i.e. the previous video was the last) then close the Social Story popup
if (start >= defaults.playlist.length) {
setTimeout(function() {
close();
return false;
}, 400);
} else {
// Otherwise run the next video
launch(start);
}
}
function prev() {
if (start != 0) {
thisTimeline.style.width = '0%';
}
// Subtract play count to previous video
start--;
// If next video doesn't exist (i.e. the previous video was the last) then close the Social Story popup
if (start < 0) {
start = 0;
return false;
} else {
// Otherwise run the previous video
launch(start);
}
}
This is an extension of #lagoCalazans comment.
What he is saying is that in your variable "interval" is created in your launch function. You need to make "interval" global in order to clear your setInterval.
Ex:
let interval = null; //global
function launch() {
let tempInterval = setInterval(function() {
//whatever code
},100);
interval = setInterval(function(){
console.log("Hello");
}, 100);
}
function clear() {
//Since interval is global I can clear it when I call clear();
clearInterval(interval);
}
As you can see in the launch function "tempInterval" is limited to the scope of launch, therefore cannot be accessed anywhere else, but now since "interval" is global it can be accessed in any function.
Your code seems a bit incomplete, so for illustrative purposes only I will assume you encapsulate those functions in a higher order function (like an IIFE) and will avoid writing that (also, some kind of global state or variable would do for an example).
First of all, setInterval will return an id which you would use later, so if you want to use it within next and prev, you need that value to be available to them.
So, in your example, you should declare interval outside launch, and assign a value to it inside:
let interval
function launch() {
// ...
interval = setInterval(function() { ... })
}
and then use interval wherever you want.
launch, next and prev are three separate functions. They do not reference the same interval because they don't share scope. Raise the scope of the interval variable.
let interval = ''; // declared here, interval can be accessed by all functions
function launch() {
// ...
// remove the var before interval
interval = setInterval( ... )
}
function next() {
// ...
// remove the var before interval
interval = setInterval( ... )
}
function prev() {
// ...
// remove the var before interval
interval = setInterval( ... )
}
I'm using setIntervals within an each() function like so
$(".elements").each(function() {
setInterval(function() {
}, 1000);
});
Obviously a setIntervals is created for each element.
My question is: How do I clear all the setIntervals when I no longer need them? I have tried storing the setInterval in a variable and call window.clearInterval(int), but that only clears the last setInterval since each variable is overridden.
When you set an interval, you get a pointer to it:
var myInterval = setInterval(function(){}, 4000);
If you want to cancel an interval, you do the following:
clearInterval(myInterval);
So, for what you want to do, you would do the following:
var intervals = [];
$(".elements").each(function() {
var i = setInterval(function() {
}, 1000);
intervals.push(i);
});
Then if you need to cancel them all you can do this:
intervals.forEach(clearInterval);
That should do it for you.
There's no "clear-all-intervals" function.
You'll need to store all of them, and clear all of them:
var ints = [];
$(".elements").each(function() {
ints.push( setInterval(function() {
}, 1000)
);
});
// later
for ( var i = 0; i < ints.length; ++i )
clearInterval( ints[i] );
ints = []; // and forget them
This worked for me:
// clear interval
var id = window.setInterval(function() {}, 0);
while (id--) {
window.clearInterval(id);
var clearAllIntervals = function ( ) {
var intervals = [];
$(".elements").each(function() {
intervals.push( setInterval(function() {
}, 1000) );
});
return function clearAll ( ) {
intervals.forEach( clearInterval );
}
}( );
// When you want to clear them:
clearAllIntervals( );
If you are wanting to be compatible with IE8 or under you should shim .forEach, or replace it with a library equivalent, or a plain loop.
Since each interval is associated with an element, you could store the interval ID in the element:
$(".elements").each(function() {
$(this).data('interval-id', setInterval(function() {
// ...
}, 1000));
});
Then, if you want to clear the intervals,
$(".elements").each(function() {
clearInterval($(this).data('interval-id'));
});
I don't recommend you use this solution, but it really do the trick. The idea it to override setInterval function to collect all links to setInterval:
(function(originalSetInterval){
var intervals = [];
window.setInterval = function(func, timeout) {
var newInterval = originalSetInterval(func, timeout);
intervals.push(newInterval);
return newInterval;
}
window.clearAllIntervals = function() {
intervals.forEach(clearInterval);
}
})(window.setInterval)
To do a better job you would also need to override clearInterval to remove all intervals being clear already:
(function(originalSetInterval, originalClearInterval){
var intervals = [];
window.setInterval = function(func, timeout) {
var newInterval = originalSetInterval(func, timeout);
intervals.push(newInterval);
return newInterval;
}
window.clearInterval = function(interval) {
originalClearInterval(interval);
intervals.splice(intervals.indexOf(interval), 1)
}
window.clearAllIntervals = function() {
intervals.forEach(clearInterval);
}
})(window.setInterval, window.clearInterval)
When you set an interval, you get a pointer to it.
To clear all intervals, you'll need to store all of them:
var arr = [];
arr.push(setInterval(function () {
console.log(1);
}, 1000));
arr.push(setInterval(function () {
console.log(2);
}, 1000));
arr.push(setInterval(function () {
console.log(3);
}, 1000));
Following loop will clear all intervals
// Clear multiple Intervals
arr.map((a) => {
console.log(a)
clearInterval(a);
arr = [];
})
Interestingly, an interval handle ID is an incremental whole number greater than 0. So, all you'd have to do is create a no-function interval, and then for-loop it to clear all intervals from your latest interval handle ID down to 1.
let hInterval1 = window.setInterval(function(){console.log('interval A');},3000);
let hInterval2 = window.setInterval(function(){console.log('interval B');},3000);
for(let i = hInterval2; i > 0; i--) window.clearInterval(i);
If you run that sample, you'll see that we see 2 intervals running in the console emitting "interval A" and "interval B", and then by the time you run the for-loop, it stops both.
I need to pause a for loop and not continue until I specify. For each item in the array that I'm looping through, I run some code that runs an operation on a separate device, and I need to wait until that operation is finished before looping to the next item in the array.
Fortunately, that code/operation is a cursor and features an after: section.
However, it's been running the entire for loop instantly, which I need to prevent. Is there any way to prevent the loop from continuing until specified? Or perhaps a different type of loop or something that I should use?
My first (poor) idea was to make a while-loop within the for-loop that ran continuously, until the after: portion of the cursor set a boolean to true. This just locked up the browser :( As I feared it would.
Anything I can do? I'm fairly new to javascript. I've been enjoying my current project though.
Here's the while-loop attempt. I know it's running the entire loop immediately because the dataCounter goes from 1 to 3 (two items in the array currently) instantly:
if(years.length>0){
var dataCounter = 1;
var continueLoop;
for(var i=0;i<years.length;i++){
continueLoop = false;
baja.Ord.make(historyName+"?period=timeRange;start="+years[i][1].encodeToString()+";end="+years[i][2].encodeToString()+"|bql:select timestamp, sum|bql:historyFunc:HistoryRollup.rollup(history:RollupInterval 'hourly')").get(
{
ok: function (result) {
// Iterate through all of the Columns
baja.iterate(result.getColumns(), function (c) {
baja.outln("Column display name: " + c.getDisplayName());
});
},
cursor: {
before: function () {
baja.outln("Called just before iterating through the Cursor");
counter=0;
data[dataCounter] = [];
baja.outln("just made data["+dataCounter+"]");
},
after: function () {
baja.outln("Called just after iterating through the Cursor");
continueLoop = true;
},
each: function () {
if(counter>=data[0].length) {
var dateA = data[dataCounter][counter-1][0];
dateA += 3600000;
}
else {
var dateA = data[0][counter][0];
}
var value=this.get("sum").encodeToString();
var valueNumber=Number(value);
data[dataCounter][counter] = [dateA,valueNumber];
counter++;
},
limit: 744, // Specify optional limit on the number of records (defaults to 10)2147483647
offset: 0 // Specify optional record offset (defaults to 0)
}
})
while(continueLoop = false){
var test = 1;
baja.outln("halp");
}
dataCounter++;
}
}
Do not use a for loop to loop on each element. You need, in the after: to remember which element of the array you've just done and then move to the next one.
Something like this :
var myArray = [1, 2, 3, 4]
function handleElem(index) {
module.sendCommand({
..., // whatever the options are for your module
after: function() {
if(index+1 == myArray.length) {
return false; // no more elem in the array
} else {
handleElem(index+1)} // the after section
}
});
}
handleElem(0);
I assumed that you call a function with some options (like you would for $.ajax()) and that the after() section is a function called at the end of your process (like success() for $.ajax())
If the "module" you call is not properly ended in the after() callback you could use setTimeout() to launch the process on the next element with a delay
EDIT: With your real code it would be something like this :
function handleElem(index) {
baja.Ord.make("...start="+years[index][1].encodeToString()+ "...").get(
{
ok: ...
after: function() {
if(index+1 == years.length) {
return false; // no more elem in the array
} else {
handleElem(index+1)} // the after section
}
}
});
}
Using Javascript/jQuery, how can I pause (or resume) the following loop when the "P" key is pressed?
(function() {
var arr = [...],
len = arr.length;
(function doProcess(i) {
if (i) {
console.log(len - i);
/* do something with arr[len - i] */
setTimeout(function() { doProcess(--i); }, 20000);
}
})(len);
})();
Pausing and resuming is fairly complex. What you really have to do is this:
Start a process and store its timeout ID.
Store the time when you ran that process.
On keypress, clear the timeout using its timeout ID.
Store the unfinished time in another variable.
On next keypress, set the timeout using that unfinished time.
Set the following timeouts to the original intended delay.
Here's a more generalized jsFiddle example I whipped up.
var counterOn = true;
var delay = 3000;
var lastRun;
var tempDelay;
var intervalId;
function decrementCounter() {
// do something
lastRun = new Date();
timeoutId = setTimeout(decrementCounter, delay);
}
function toggleCounter() {
var curTime = new Date();
counterOn = !counterOn;
if (counterOn) {
lastRun = curTime.valueOf() + tempDelay - delay;
timeoutId = setTimeout(decrementCounter, tempDelay);
} else {
clearTimeout(timeoutId);
tempDelay = delay - (curTime.valueOf() - lastRun);
}
}
$(document).keydown(function(e) {
if (e.which === 80) {
toggleCounter();
}
});
decrementCounter();
You'll want to keep track of how much time has passed with your timer (see here: javascript: pause setTimeout();) and call clearTimeout on whatever event you want to stop, then call setTimeout again with your remaining time left once whatever event unpauses is fired again.
What is the most recommended/best way to stop multiple instances of a setTimeout function from being created (in javascript)?
An example (psuedo code):
function mouseClick()
{
moveDiv("div_0001", mouseX, mouseY);
}
function moveDiv(objID, destX, destY)
{
//some code that moves the div closer to destination
...
...
...
setTimeout("moveDiv(objID, destX, destY)", 1000);
...
...
...
}
My issue is that if the user clicks the mouse multiple times, I have multiple instances of moveDiv() getting called.
The option I have seen is to create a flag, that only allows the timeout to be called if no other instance is available...is that the best way to go?
I hope that makes it clear....
when you call settimeout, it returns you a variable "handle" (a number, I think)
if you call settimeout a second time, you should first
clearTimeout( handle )
then:
handle = setTimeout( ... )
to help automate this, you might use a wrapper that associates timeout calls with a string (i.e. the div's id, or anything you want), so that if there's a previous settimeout with the same "string", it clears it for you automatically before setting it again,
You would use an array (i.e. dictionary/hashmap) to associate strings with handles.
var timeout_handles = []
function set_time_out( id, code, time ) /// wrapper
{
if( id in timeout_handles )
{
clearTimeout( timeout_handles[id] )
}
timeout_handles[id] = setTimeout( code, time )
}
There are of course other ways to do this ..
I would do it this way:
// declare an array for all the timeOuts
var timeOuts = new Array();
// then instead of a normal timeOut call do this
timeOuts["uniqueId"] = setTimeout('whateverYouDo("fooValue")', 1000);
// to clear them all, just call this
function clearTimeouts() {
for (key in timeOuts) {
clearTimeout(timeOuts[key]);
}
}
// clear just one of the timeOuts this way
clearTimeout(timeOuts["uniqueId"]);
var timeout1 = window.setTimeout('doSomething();', 1000);
var timeout2 = window.setTimeout('doSomething();', 1000);
var timeout3 = window.setTimeout('doSomething();', 1000);
// to cancel:
window.clearTimeout(timeout1);
window.clearTimeout(timeout2);
window.clearTimeout(timeout3);
I haven't tested any of this, and just cut this up in the editor here. Might work, might not, hopefully will be food for thought though.
var Timeout = {
_timeouts: {},
set: function(name, func, time){
this.clear(name);
this._timeouts[name] = {pending: true, func: func};
var tobj = this._timeouts[name];
tobj.timeout = setTimeout(function()
{
/* setTimeout normally passes an accuracy report on some browsers, this just forwards that. */
tobj.func.call(arguments);
tobj.pending = false;
}, time);
},
hasRun: function(name)
{
if( this._timeouts[name] )
{
return !this._timeouts[name].pending;
}
return -1; /* Whut? */
},
runNow: function(name)
{
if( this._timeouts[name] && this.hasRun(name)===false )
{
this._timeouts[name].func(-1); /* fake time. *shrug* */
this.clear(name);
}
}
clear: function(name)
{
if( this._timeouts[name] && this._timeouts[name].pending )
{
clearTimeout(this._timeouts[name].timeout);
this._timeouts[name].pending = false;
}
}
};
Timeout.set("doom1", function(){
if( Timeout.hasRun("doom2") === true )
{
alert("OMG, it has teh run");
}
}, 2000 );
Timeout.set("doom2", function(){
/* NooP! */
}, 1000 );
Successive calls with the same identifier will cancel the previous call.
You could store multiple flags in a lookup-table (hash) using objID as a key.
var moving = {};
function mouseClick()
{
var objID = "div_0001";
if (!moving[objID])
{
moving[objID] = true;
moveDiv("div_0001", mouseX, mouseY);
}
}
You can avoid a global or lesser variable by using a property within the function. This works well if the function is only used for this specific context.
function set_time_out( id, code, time ) /// wrapper
{
if(typeof this.timeout_handles == 'undefined') this.timeout_handles = [];
if( id in this.timeout_handles )
{
clearTimeout( this.timeout_handles[id] )
}
this.timeout_handles[id] = setTimeout( code, time )
}
you can always overwrite the buttons onclick to return false. example:
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="UTF-8">
<head>
<title>Javascript example</title>
<script type="text/javascript">
var count = 0;
function annoy() {
document.getElementById('testa').onclick = function() { return false; };
setTimeout(function() {
alert('isn\'t this annoying? ' + count++);
document.getElementById('testa').onclick = window.annoy;
}, 1000);
}
</script>
</head>
<body>
<h2>Javascript example</h2>
Should Only Fire Once<br />
</body>
</html>
You can set a global flag somewhere (like var mouseMoveActive = false;) that tells you whether you are already in a call and if so not start the next one. You set the flag just before you enter the setTimeout call, after checking whether it's already set. Then at the end of the routine called in setTimeout() you can reset the flag.
I'm using this to force a garbage collection on all obsolete timeout references which really un-lagged my script preformance:
var TopObjList = new Array();
function ColorCycle( theId, theIndex, RefPoint ) {
...
...
...
TopObjList.push(setTimeout( function() { ColorCycle( theId, theIndex ,CCr ); },CC_speed));
TO_l = TopObjList.length;
if (TO_l > 8888) {
for (CCl=4777; CCl<TO_l; CCl++) {
clearTimeout(TopObjList.shift());
}
}
}
My original sloppy code was generating a massive array 100,000+ deep over a very short time but this really did the trick!