Google Script: Play Sound when a specific cell change the Value - javascript

Situation:
Example Spreadsheet
Sheet: Support
Column: H has the following function "=IF(D:D>0;IF($B$1>=$G:G;"Call";"In Time");" ")" that changes the value depending on the result.
Problem:
I need to:
Play a sound when a cell in column H changes to "Call" on the sheet "Support".
This function will need to run every 5min.
Does the sound need to be uploaded to Drive or can I use a sound from a URL?
I will appreciate to anyone can help on it... I see a lot of code but I didn't understand very well.

This is a pretty tough problem, but it can be done with a sidebar that periodically polls the H column for changes.
Code.gs
// creates a custom menu when the spreadsheet is opened
function onOpen() {
var ui = SpreadsheetApp.getUi()
.createMenu('Call App')
.addItem('Open Call Notifier', 'openCallNotifier')
.addToUi();
// you could also open the call notifier sidebar when the spreadsheet opens
// if you find that more convenient
// openCallNotifier();
}
// opens the sidebar app
function openCallNotifier() {
// get the html from the file called "Page.html"
var html = HtmlService.createHtmlOutputFromFile('Page')
.setTitle("Call Notifier");
// open the sidebar
SpreadsheetApp.getUi()
.showSidebar(html);
}
// returns a list of values in column H
function getColumnH() {
var sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("Support");
// get the values in column H and turn the rows into a single values
return sheet.getRange(1, 8, sheet.getLastRow(), 1).getValues().map(function (row) { return row[0]; });
}
Page.html
<!DOCTYPE html>
<html>
<head>
<base target="_top">
</head>
<body>
<p id="message">Checking for calls...</p>
<audio id="call">
<source src="||a URL is best here||" type="audio/mp3">
Your browser does not support the audio element.
</audio>
<script>
var lastTime = []; // store the last result to track changes
function checkCalls() {
// This calls the "getColumnH" function on the server
// Then it waits for the results
// When it gets the results back from the server,
// it calls the callback function passed into withSuccessHandler
google.script.run.withSuccessHandler(function (columnH) {
for (var i = 0; i < columnH.length; i++) {
// if there's a difference and it's a call, notify the user
if (lastTime[i] !== columnH[i] && columnH[i] === "Call") {
notify();
}
}
// store results for next time
lastTime = columnH;
console.log(lastTime);
// poll again in x miliseconds
var x = 1000; // 1 second
window.setTimeout(checkCalls, x);
}).getColumnH();
}
function notify() {
document.getElementById("call").play();
}
window.onload = function () {
checkCalls();
}
</script>
</body>
</html>
Some sources to help:
Sidebars and Dialogs
Custom Menus
Simple Trigger - onOpen
`google.script.run.withSuccessHandler(callback).customFunction()
Array.prototype.map

Recursively calling checkCalls() eventually led to errors, when I implemented the main answer given (which is mostly correct and really useful, so thank you!).
// Note: But the original implementation would work fine for a while - say 90 minutes - then crash. The call that would normally take 1 second would take 300 seconds, and Execution would Halt. It looks like it blew the stack by keeping on recursively calling itself. When moved to a single call of check() with proper exiting of the function, it then worked.
The console log in Chrome on running the JavaScript, said this:
ERR_QUIC_PROTOCOL_ERROR.QUIC_TOO_MANY_RTOS 200
After much investigation, I worked out a better way of doing it... Which doesn't require recursion (and therefore won't blow the stack).
Remove this line:
// window.setTimeout(checkCalls, 500);
And use something like this - at the end of your script:
// This function returns a Promise that resolves after "ms" Milliseconds
// The current best practice is to create a Promise...
function timer(ms) {
return new Promise(res => setTimeout(res, ms));
}
async function loopthis () { // We need to wrap the loop into an async function for the await call (to the Promise) to work. [From web: "An async function is a function declared with the async keyword. Async functions are instances of the AsyncFunction constructor, and the await keyword is permitted within them. The async and await keywords enable asynchronous, promise-based behavior to be written in a cleaner style, avoiding the need to explicitly configure promise chains."]
for (var i = 0; i >= 0; i++) {
console.log('Number of times function has been run: ' + i);
checkCalls();
await timer(3000);
}
}
window.onload = function () {
loopthis();
}
</script>

Related

GS function keeps running after RETURN

My Google Scripts function keeps running even after returning the next function.
To summarise, I have a function (nightTargetSelection ) that calls another function (dayStart). And as soon as the second function gets called, I want the first one to stop running.
I have stripped down the function and removed everything that should not be relevant for this post. But please let me know if you need more information.
Firstly, I call a user-input form via the following code in GS:
function roleWakeUp (roleName, j){
var roleStrAppend = "<div id='id_role_name' style='display:none;'>" + roleName + "</div>";
var jAppend = "<div id='id_j' style='display:none;'>" + j + "</div>";
var actionInputDlg = HtmlService.createHtmlOutputFromFile('night')
.setSandboxMode(HtmlService.SandboxMode.IFRAME)
.setWidth(600)
.setHeight(425);
var wakeUpText = roleName + " wakes up."
actionInputDlg.append(roleStrAppend).append(jAppend);
SpreadsheetApp.getUi().showModalDialog(actionInputDlg, wakeUpText);
}
Then I have the form submitted by a user via HTML/JS:
<script>
selected = select.options[select.selectedIndex].value
google.script.run.withSuccessHandler(selected).nightTargetSelection(selected);
google.script.host.close();
</script>
Back in GS, I have the nightTargetSelection (the problematic function that I need helped getting fixed)
function nightTargetSelection (selected){
return dayStart();
}
Finally I have the dayStart function that is called within nightTargetSelection.
function dayStart() {
Browser.msgBox("DAY START");
//If I do not close the msgBox within 6 minutes (GS timeout period), then nightTargetSelection times out.
roleWakeUp (roleName, j);
}
I am expecting that the nightTargetSelection function stops running as soon as dayStart is called.
Unfortunately that is not happening and the nightTargetSelection function only completes when the next nightTargetSelection gets called.
As you can see, dayStart function calls roleWakeUp , making it a loop between functions. I have omitted including the loop details into this question because I dont believe it is relevant. Let me know if it is relevant and I will provide more details.
I don't know if this is your problem but in this:
<script>
var selected=select.options[select.selectedIndex].value;
google.script.run
.withSuccessHandler(selected)//select is supposed to be a function name
.nightTargetSelection(selected);
google.script.host.close();
</script>
.withSuccessHandler(functionname or anonymous function)
Yes, because nightTargetSelection() will not terminate until all of the processes it called have finished or it times out.
Looks like the end of the cycle is Browser.msgBox("DAY START");. So allow your server code to finish execution and then use .withSuccessHandler() to call roleWakeUp() via the client-side script.
(I don't really know what your code is doing. nightTargetSelection() seems to be unnecessary given the information you provided. So the example below is an abstraction to show you the flow.)
Client-Side Script
<script>
function yourOriginal() {
selected = select.options[select.selectedIndex].value;
google.script.run
.withSuccessHandler(restartCycle) // Calls restartCycle() after nightTargetSelection() finishes
.nightTargetSelection(selected);
google.script.host.close();
}
function restartCycle(values) {
google.script.run.roleWakeUp(values.roleName, values.j);
}
</script>
Server-Side Scripts
function roleWakeUp(roleName, j) {
// opens the dialog
}
function nightTargetSelection(selected) {
return dayStart();
}
function dayStart() {
Browser.msgBox("DAY START");
return {"roleName": roleName, "j": j}; // Pass these values to client-side script
}

Get Header's OOXML

I am unable to get the OOXML of a Header. According to the documentation getHeader" method will return Body type. The Body has a method to get OOXML. But it looks like it is not returning the OOXML. Maybe I am missing something?
Here's my code:
Word.run(function (context) {
// Create a proxy sectionsCollection object.
var mySections = context.document.sections;
// Queue a commmand to load the sections.
context.load(mySections, 'body/style');
// Synchronize the document state by executing the queued commands,
// and return a promise to indicate task completion.
return context.sync().then(function () {
// header
var headerBody = mySections.items[0].getHeader("primary");
// header OOXML
//// NOT GETTING OOXML HERE
var headerOOXML = headerBody.getOoxml();
// Synchronize the document state by executing the queued commands,
// and return a promise to indicate task completion.
return context.sync().then(function () {
// modify header
var headerOOXMLValue = ModifyHeaderMethod(headerOOXML.value);
headerBody.clear();
headerBody.insertOoxml(headerOOXMLValue, 'Start');
// Synchronize the document state by executing the queued commands,
// and return a promise to indicate task completion.
return context.sync().then(function () {
callBackFunc({
isError: false
});
});
});
});
})
The "art" of Office.js is to minimize the number of "syncs" you do. I know that is kind of an unnecessary burden, but that's how it is.
With that in mind, In this case you only need ONE sync.
this code works (assuming that you have only one section in the doc).
btw you can try it in script lab with this yaml.
if this does not work, please indicate if this is Word for Windows (and what build) or Online, or Mac... thanks!
async function run() {
await Word.run(async (context) => {
let myOOXML = context.document.sections.getFirst()
.getHeader("primary").getOoxml();
await context.sync();
console.log(myOOXML.value);
});
}
You have a lot of extra code here but the gist of your problem is that headerOOXML won't be populated until you sync():
Word.run(function (context) {
var header = context.document.sections // Grabv
.getFirst() // Get the first section
.getHeader("primary"); // Get the header
var ooxml = header.getOoxml();
return context.sync().then(function () {
console.log(ooxml.value);
});
});

setTimeout doesn't work as expected

In my application I am loading user posts using the ajax scroll down feature.
The for loop iteration takes too much time, browser freezes until the results are displayed. So I implemented a setTimeout method to fix that, but for some reason the flow doesn't go inside the setTimeout method on debugging.
Also the page is blank, data is not rendered.
success : function(responseJson) {
$("#loadingdata").toggle();
enableScrolling();
if($.isEmptyObject(responseJson)){
$("#noMorePosts").toggle();
disableScrolling();
paginationComplete=true;
}
$.each(responseJson, function (index) {
(function(index) {
setTimeout(function(index) { //the flow doesn't move inside this
var resp_JSON=responseJson[index];
var dateObj=resp_JSON.postCreationTime;
resp_JSON.postCreationTime = moment(dateObj).format("h:mm a, ddd, MMM Do YY");
var timeago = moment(dateObj).fromNow();
resp_JSON.timeago = timeago;
resp_JSON.username=userName;
var post_template = $('#homepostcontainertemplate').html();
Mustache.parse(post_template);
var post_info = Mustache.to_html(post_template, resp_JSON);
$('#homepublisherpostsdiv').append(post_info);
$('div').linkify();
});
})(index);
});
When the flow reaches setTimeout the next code it hits is the jquery lib
Am I doing it right or missing something?
Note: I get the responseJson data from the server fine. Without the setTimeout the data is loaded on the page.
setTimeout takes an argument-less function (https://developer.mozilla.org/en-US/docs/Web/API/WindowTimers/setTimeout), so having index as an argument is a little odd. I suspect that index is undefined so responseJson[index] is throwing out of bound exception (as evidenced by console.log(1) showing up as per Niloct's comment). If you change your code to:
$.each(responseJson, function (index) {
setTimeout(function() { // no index in the argument list
var resp_JSON=responseJson[index];
var dateObj=resp_JSON.postCreationTime;
resp_JSON.postCreationTime = moment(dateObj).format("h:mm a, ddd, MMM Do YY");
var timeago = moment(dateObj).fromNow();
resp_JSON.timeago = timeago;
resp_JSON.username=userName;
var post_template = $('#homepostcontainertemplate').html();
Mustache.parse(post_template);
var post_info = Mustache.to_html(post_template, resp_JSON);
$('#homepublisherpostsdiv').append(post_info);
$('div').linkify();
});
});
I suspect it will work.
(edited to take into account jjaulimsing's comment about not needing the encapsulating function.)

Console output one time every 40 seconds with interval doesn't work

I'm trying to use Twitter API in order to retweet.And because Twitter has limitation to 2400 actions per day I decided to limit the retweet to one time every 40 seconds.
I'm using https://www.npmjs.com/package/twit using Streaming API.
My problem is that it continuously streams console.log instead of using setInterval.
By stream I mean it outputs console.log multiple times instead of one time.
function hastagRetweet() {
var stream = T.stream('statuses/filter', { track: ['#hastag']})
stream.on('tweet', function (tweet,error) {
var retweetId = tweet.id; // id
var retweetId_str = tweet.id_str;
var tweetTextRetweet = tweet.text; // tweet text
function twInterval() {
console.log('INFO ------- ',tweet.id);
console.log('INFO ------- ',tweet.text);
};
setInterval(twInterval, 1000 * 40);
});
}
Any way of getting the variales data retweetId,retweetId_str,tweetTextRetweet outside stream.on beside inserting them into a mysql table so that every 40 seconds it checks the variables fixed data and executes console.log?
There are multiple problems here:
You're trying to start an interval timer in an event handler. That means that every time the event fires you would be attempting to start a new interval timer and they would pile up.
Even if you did sucessfully start up an interval timer, each one would never change it's output since the variables in scope for it never change as it is started up within a given function and the arguments to that function are what they were when it was first called. Subsequent calls of the function will start a new function not change the arguments on the prior call of the function.
You aren't even starting your interval correctly. As it is, all you're doing is calling the function and passing it's return value to setInterval() which does nothing.
If the goal is just to output to the console each stream tweet event, then probably what you want is just this:
function hastagRetweet() {
var stream = T.stream('statuses/filter', { track: ['#hastag']})
stream.on('tweet', function (tweet,error) {
var retweetId = tweet.id; // id
var retweetId_str = tweet.id_str;
var tweetTextRetweet = tweet.text; // tweet text
console.log('INFO ------- ',tweet.id);
console.log('INFO ------- ',tweet.text);
});
}
You cannot get these variables outside the .on() handler. It's an async callback and the only place they reliably exist is within that handler.
If you can describe in more detail what the end result you're trying to achieve is, we can likely help more specifically. If you want to do something every 40 seconds, then maybe you need to collect data in some collection (probably an array) and then every 40 seconds evaluate what you have just recently collected.
Here's a method of collecting the events in an array and then evaluating them every 40 seconds:
function hastagRetweet() {
var stream = T.stream('statuses/filter', { track: ['#hastag']});
var tweets = [];
stream.on('tweet', function (tweet,error) {
tweets.push({id: tweet.id, str: tweet.id_str, text: tweet.text});
});
setInterval(function() {
// evaluate the tweets in the tweets array every 40 seconds
// do something with them
for (var i = 0; i < tweets.length; i++) {
// tweets[i].id
// tweets[i].str
// tweets[i].text
}
// reset the tweets array
tweets.length = 0;
}, 40 * 1000);
}
Please note that once you call hastagRetweet(), it will run forever.

DOM refresh on long running function

I have a button which runs a long running function when it's clicked. Now, while the function is running, I want to change the button text, but I'm having problems in some browsers like Firefox, IE.
html:
<button id="mybutt" class="buttonEnabled" onclick="longrunningfunction();"><span id="myspan">do some work</span></button>
javascript:
function longrunningfunction() {
document.getElementById("myspan").innerHTML = "doing some work";
document.getElementById("mybutt").disabled = true;
document.getElementById("mybutt").className = "buttonDisabled";
//long running task here
document.getElementById("myspan").innerHTML = "done";
}
Now this has problems in firefox and IE, ( in chrome it works ok )
So I thought to put it into a settimeout:
function longrunningfunction() {
document.getElementById("myspan").innerHTML = "doing some work";
document.getElementById("mybutt").disabled = true;
document.getElementById("mybutt").className = "buttonDisabled";
setTimeout(function() {
//long running task here
document.getElementById("myspan").innerHTML = "done";
}, 0);
}
but this doesn't work either for firefox! the button gets disabled, changes colour ( due to the application of the new css ) but the text does not change.
I have to set the time to 50ms instead of just 0ms, in order to make it work ( change the button text ). Now I find this stupid at least. I can understand if it would work with just a 0ms delay, but what would happen in a slower computer? maybe firefox would need 100ms there in the settimeout? it sounds rather stupid. I tried many times, 1ms, 10ms, 20ms...no it won't refresh it. only with 50ms.
So I followed the advice in this topic:
Forcing a DOM refresh in Internet explorer after javascript dom manipulation
so I tried:
function longrunningfunction() {
document.getElementById("myspan").innerHTML = "doing some work";
var a = document.getElementById("mybutt").offsetTop; //force refresh
//long running task here
document.getElementById("myspan").innerHTML = "done";
}
but it doesn't work ( FIREFOX 21). Then i tried:
function longrunningfunction() {
document.getElementById("myspan").innerHTML = "doing some work";
document.getElementById("mybutt").disabled = true;
document.getElementById("mybutt").className = "buttonDisabled";
var a = document.getElementById("mybutt").offsetTop; //force refresh
var b = document.getElementById("myspan").offsetTop; //force refresh
var c = document.getElementById("mybutt").clientHeight; //force refresh
var d = document.getElementById("myspan").clientHeight; //force refresh
setTimeout(function() {
//long running task here
document.getElementById("myspan").innerHTML = "done";
}, 0);
}
I even tried clientHeight instead of offsetTop but nothing. the DOM does not get refreshed.
Can someone offer a reliable solution preferrably non-hacky ?
thanks in advance!
as suggested here i also tried
$('#parentOfElementToBeRedrawn').hide().show();
to no avail
Force DOM redraw/refresh on Chrome/Mac
TL;DR:
looking for a RELIABLE cross-browser method to have a forced DOM refresh WITHOUT the use of setTimeout (preferred solution due to different time intervals needed depending on the type of long running code, browser, computer speed and setTimeout requires anywhere from 50 to 100ms depending on situation)
jsfiddle: http://jsfiddle.net/WsmUh/5/
Webpages are updated based on a single thread controller, and half the browsers don't update the DOM or styling until your JS execution halts, giving computational control back to the browser. That means if you set some element.style.[...] = ... it won't kick in until your code finishes running (either completely, or because the browser sees you're doing something that lets it intercept processing for a few ms).
You have two problems: 1) your button has a <span> in it. Remove that, just set .innerHTML on the button itself. But this isn't the real problem of course. 2) you're running very long operations, and you should think very hard about why, and after answering the why, how:
If you're running a long computational job, cut it up into timeout callbacks (or, in 2019, await/async - see note at the end of this anser). Your examples don't show what your "long job" actually is (a spin loop doesn't count) but you have several options depending on the browsers you take, with one GIANT booknote: don't run long jobs in JavaScript, period. JavaScript is a single threaded environment by specification, so any operation you want to do should be able to complete in milliseconds. If it can't, you're literally doing something wrong.
If you need to calculate difficult things, offload it to the server with an AJAX operation (universal across browsers, often giving you a) faster processing for that operation and b) a good 30 seconds of time that you can asynchronously not-wait for the result to be returned) or use a webworker background thread (very much NOT universal).
If your calculation takes long but not absurdly so, refactor your code so that you perform parts, with timeout breathing space:
function doLongCalculation(callbackFunction) {
var partialResult = {};
// part of the work, filling partialResult
setTimeout(function(){ doSecondBit(partialResult, callbackFunction); }, 10);
}
function doSecondBit(partialResult, callbackFunction) {
// more 'part of the work', filling partialResult
setTimeout(function(){ finishUp(partialResult, callbackFunction); }, 10);
}
function finishUp(partialResult, callbackFunction) {
var result;
// do last bits, forming final result
callbackFunction(result);
}
A long calculation can almost always be refactored into several steps, either because you're performing several steps, or because you're running the same computation a million times, and can cut it up into batches. If you have (exaggerated) this:
var resuls = [];
for(var i=0; i<1000000; i++) {
// computation is performed here
if(...) results.push(...);
}
then you can trivially cut this up into a timeout-relaxed function with a callback
function runBatch(start, end, terminal, results, callback) {
var i;
for(var i=start; i<end; i++) {
// computation is performed here
if(...) results.push(...); }
if(i>=terminal) {
callback(results);
} else {
var inc = end-start;
setTimeout(function() {
runBatch(start+inc, end+inc, terminal, results, callback);
},10);
}
}
function dealWithResults(results) {
...
}
function doLongComputation() {
runBatch(0,1000,1000000,[],dealWithResults);
}
TL;DR: don't run long computations, but if you have to, make the server do the work for you and just use an asynchronous AJAX call. The server can do the work faster, and your page won't block.
The JS examples of how to deal with long computations in JS at the client are only here to explain how you might deal with this problem if you don't have the option to do AJAX calls, which 99.99% of the time will not be the case.
edit
also note that your bounty description is a classic case of The XY problem
2019 edit
In modern JS the await/async concept vastly improves upon timeout callbacks, so use those instead. Any await lets the browser know that it can safely run scheduled updates, so you write your code in a "structured as if it's synchronous" way, but you mark your functions as async, and then you await their output them whenever you call them:
async doLongCalculation() {
let firstbit = await doFirstBit();
let secondbit = await doSecondBit(firstbit);
let result = await finishUp(secondbit);
return result;
}
async doFirstBit() {
//...
}
async doSecondBit...
...
SOLVED IT!! No setTimeout()!!!
Tested in Chrome 27.0.1453, Firefox 21.0, Internet 9.0.8112
$("#btn").on("mousedown",function(){
$('#btn').html('working');}).on('mouseup', longFunc);
function longFunc(){
//Do your long running work here...
for (i = 1; i<1003332300; i++) {}
//And on finish....
$('#btn').html('done');
}
DEMO HERE!
As of 2019 one uses double requesAnimationFrame to skip a frame instead of creating a race condition using setTimeout.
function doRun() {
document.getElementById('app').innerHTML = 'Processing JS...';
requestAnimationFrame(() =>
requestAnimationFrame(function(){
//blocks render
confirm('Heavy load done')
document.getElementById('app').innerHTML = 'Processing JS... done';
}))
}
doRun()
<div id="app"></div>
As an usage example think of calculating pi using Monte Carlo in an endless loop:
using for loop to mock while(true) - as this breaks the page
function* piMonteCarlo(r = 5, yield_cycle = 10000){
let total = 0, hits = 0, x=0, y=0, rsqrd = Math.pow(r, 2);
while(true){
total++;
if(total === Number.MAX_SAFE_INTEGER){
break;
}
x = Math.random() * r * 2 - r;
y = Math.random() * r * 2 - r;
(Math.pow(x,2) + Math.pow(y,2) < rsqrd) && hits++;
if(total % yield_cycle === 0){
yield 4 * hits / total
}
}
}
let pi_gen = piMonteCarlo(5, 1000), pi = 3;
for(let i = 0; i < 1000; i++){
// mocks
// while(true){
// basically last value will be rendered only
pi = pi_gen.next().value
console.log(pi)
document.getElementById('app').innerHTML = "PI: " + pi
}
<div id="app"></div>
And now think about using requestAnimationFrame for updates in between ;)
function* piMonteCarlo(r = 5, yield_cycle = 10000){
let total = 0, hits = 0, x=0, y=0, rsqrd = Math.pow(r, 2);
while(true){
total++;
if(total === Number.MAX_SAFE_INTEGER){
break;
}
x = Math.random() * r * 2 - r;
y = Math.random() * r * 2 - r;
(Math.pow(x,2) + Math.pow(y,2) < rsqrd) && hits++;
if(total % yield_cycle === 0){
yield 4 * hits / total
}
}
}
let pi_gen = piMonteCarlo(5, 1000), pi = 3;
function rAFLoop(calculate){
return new Promise(resolve => {
requestAnimationFrame( () => {
requestAnimationFrame(() => {
typeof calculate === "function" && calculate()
resolve()
})
})
})
}
let stopped = false
async function piDOM(){
while(stopped==false){
await rAFLoop(() => {
pi = pi_gen.next().value
console.log(pi)
document.getElementById('app').innerHTML = "PI: " + pi
})
}
}
function stop(){
stopped = true;
}
function start(){
if(stopped){
stopped = false
piDOM()
}
}
piDOM()
<div id="app"></div>
<button onclick="stop()">Stop</button>
<button onclick="start()">start</button>
As described in the "Script taking too long and heavy jobs" section of Events and timing in-depth (an interesting reading, by the way):
[...] split the job into parts which get scheduled after each other. [...] Then there is a “free time” for the browser to respond between parts. It is can render and react on other events. Both the visitor and the browser are happy.
I am sure that there are many times in which a task cannot be splitted into smaller tasks, or fragments. But I am sure that there will be many other times in which this is possible too! :-)
Some refactoring is needed in the example provided. You could create a function to do a piece of the work you have to do. It could begin like this:
function doHeavyWork(start) {
var total = 1000000000;
var fragment = 1000000;
var end = start + fragment;
// Do heavy work
for (var i = start; i < end; i++) {
//
}
Once the work is finished, function should determine if next work piece must be done, or if execution has finished:
if (end == total) {
// If we reached the end, stop and change status
document.getElementById("btn").innerHTML = "done!";
} else {
// Otherwise, process next fragment
setTimeout(function() {
doHeavyWork(end);
}, 0);
}
}
Your main dowork() function would be like this:
function dowork() {
// Set "working" status
document.getElementById("btn").innerHTML = "working";
// Start heavy process
doHeavyWork(0);
}
Full working code at http://jsfiddle.net/WsmUh/19/ (seems to behave gently on Firefox).
If you don't want to use setTimeout then you are left with WebWorker - this will require HTML5 enabled browsers however.
This is one way you can use them -
Define your HTML and an inline script (you don't have to use inline script, you can just as well give an url to an existing separate JS file):
<input id="start" type="button" value="Start" />
<div id="status">Preparing worker...</div>
<script type="javascript/worker">
postMessage('Worker is ready...');
onmessage = function(e) {
if (e.data === 'start') {
//simulate heavy work..
var max = 500000000;
for (var i = 0; i < max; i++) {
if ((i % 100000) === 0) postMessage('Progress: ' + (i / max * 100).toFixed(0) + '%');
}
postMessage('Done!');
}
};
</script>
For the inline script we mark it with type javascript/worker.
In the regular Javascript file -
The function that converts the inline script to a Blob-url that can be passed to a WebWorker. Note that this might note work in IE and you will have to use a regular file:
function getInlineJS() {
var js = document.querySelector('[type="javascript/worker"]').textContent;
var blob = new Blob([js], {
"type": "text\/plain"
});
return URL.createObjectURL(blob);
}
Setup worker:
var ww = new Worker(getInlineJS());
Receive messages (or commands) from the WebWorker:
ww.onmessage = function (e) {
var msg = e.data;
document.getElementById('status').innerHTML = msg;
if (msg === 'Done!') {
alert('Next');
}
};
We kick off with a button-click in this demo:
document.getElementById('start').addEventListener('click', start, false);
function start() {
ww.postMessage('start');
}
Working example here:
http://jsfiddle.net/AbdiasSoftware/Ls4XJ/
As you can see the user-interface is updated (with progress in this example) even if we're using a busy-loop on the worker. This was tested with an Atom based (slow) computer.
If you don't want or can't use WebWorker you have to use setTimeout.
This is because this is the only way (beside from setInterval) that allow you to queue up an event. As you noticed you will need to give it a few milliseconds as this will give the UI engine "room to breeth" so-to-speak. As JS is single-threaded you cannot queue up events other ways (requestAnimationFrame will not work well in this context).
Hope this helps.
Update: I don't think in the long term that you can be sure of avoiding Firefox's aggressive avoidance of DOM updates without using a timeout. If you want to force a redraw / DOM update, there are tricks available, like adjusting the offset of elements, or doing hide() then show(), etc., but there is nothing very pretty available, and after a while when those tricks get abused and slow down user experience, then browsers get updated to ignore those tricks. See this article and the linked articles beside it for some examples: Force DOM redraw/refresh on Chrome/Mac
The other answers look like they have the basic elements needed, but I thought it would be worthwhile to mention that my practice is to wrap all interactive DOM-changing functions in a "dispatch" function which handles the necessary pauses needed to get around the fact that Firefox is extremely aggressive in avoiding DOM updates in order to score well on benchmarks (and to be responsive to users while browsing the internet).
I looked at your JSFiddle and customized a dispatch function the one that many of my programs rely on. I think it is self-explanatory, and you can just paste it into your existing JS Fiddle to see how it works:
$("#btn").on("click", function() { dispatch(this, dowork, 'working', 'done!'); });
function dispatch(me, work, working, done) {
/* work function, working message HTML string, done message HTML string */
/* only designed for a <button></button> element */
var pause = 50, old;
if (!me || me.tagName.toLowerCase() != 'button' || me.innerHTML == working) return;
old = me.innerHTML;
me.innerHTML = working;
setTimeout(function() {
work();
me.innerHTML = done;
setTimeout(function() { me.innerHTML = old; }, 1500);
}, pause);
}
function dowork() {
for (var i = 1; i<1000000000; i++) {
//
}
}
Note: the dispatching function also blocks calls from happening at the same time, because it can seriously confuse users if status updates from multiple clicks are happening together.
Fake an ajax request
function longrunningfunction() {
document.getElementById("myspan").innerHTML = "doing some work";
document.getElementById("mybutt").disabled = true;
document.getElementById("mybutt").className = "buttonDisabled";
$.ajax({
url: "/",
complete: function () {
//long running task here
document.getElementById("myspan").innerHTML = "done";
}
});}
Try this
function longRunningTask(){
// Do the task here
document.getElementById("mybutt").value = "done";
}
function longrunningfunction() {
document.getElementById("mybutt").value = "doing some work";
setTimeout(function() {
longRunningTask();
}, 1);
}
Some browsers don't handle onclick html attribute good. It's better to use that event on js object. Like this:
<button id="mybutt" class="buttonEnabled">
<span id="myspan">do some work</span>
</button>
<script type="text/javascript">
window.onload = function(){
butt = document.getElementById("mybutt");
span = document.getElementById("myspan");
butt.onclick = function () {
span.innerHTML = "doing some work";
butt.disabled = true;
butt.className = "buttonDisabled";
//long running task here
span.innerHTML = "done";
};
};
</script>
I made a fiddle with working example http://jsfiddle.net/BZWbH/2/
Have you tried adding listener to "onmousedown" to change the button text and click event for longrunning function.
Slightly modified your code at jsfiddle and:
$("#btn").on("click", dowork);
function dowork() {
document.getElementById("btn").innerHTML = "working";
setTimeout(function() {
for (var i = 1; i<1000000000; i++) {
//
}
document.getElementById("btn").innerHTML = "done!";
}, 100);
}
Timeout set to more reasonable value 100ms did the trick for me. Try it.
Try adjusting the latency to find the best value.
DOM buffer also exists in default browser on android,
long running javascript only flush DOM buffer once,
use setTimeout(..., 50) to solve it.
I have adapted Estradiaz's double animation frame method for async/await:
async function waitForDisplayUpdate() {
await waitForNextAnimationFrame();
await waitForNextAnimationFrame();
}
function waitForNextAnimationFrame() {
return new Promise((resolve) => {
window.requestAnimationFrame(() => resolve());
});
}
async function main() {
const startTime = performance.now();
for (let i = 1; i <= 5; i++) {
setStatus("Step " + i);
await waitForDisplayUpdate();
wasteCpuTime(1000);
}
const elapsedTime = Math.round(performance.now() - startTime);
setStatus(`Completed in ${elapsedTime} ms`);
}
function wasteCpuTime(ms) {
const startTime = performance.now();
while (performance.now() < startTime + ms) {
if (Math.random() == 0) {
console.log("A very rare event has happened.");
}
}
}
function setStatus(s) {
document.getElementById("status").textContent = s;
}
document.addEventListener("DOMContentLoaded", main);
Status: <span id="status">Start</span>

Categories