0

(This is an edited question, based on Adam H's help.)

I have a complex web-app in Javascript. Some actions are lengthy, and I want to show the busy cursor during these actions. These actions take on many forms, in many places in the code. They inspect, modify and rearrange the DOM, and often add event handlers.

I want to use a wrapper-function. The wrapper should show the busy cursor, execute the wrapped function, then revert the cursor to normal. Simplified, it looks like this:

function lengthy1(func) {
  document.body.classList.add('waiting');
  document.getElementById('notice').innerText = 'waiting';
  window.setTimeout(() => {
    func();
    document.body.classList.remove('waiting')
    document.getElementById('notice').innerText = 'done';
  }, 0); // Zero delay is unreliable, but works almost always with 100 msec
}

function lengthy2(func) {
  document.body.classList.add('waiting');
  document.getElementById('notice').innerText = 'waiting';
  new Promise((resolve, reject) => {
    func();
    resolve();
  }).then(() => {
    document.body.classList.remove('waiting')
    document.getElementById('notice').innerText = 'done';
  });
}

function LongAction() {
  // First add many paragraphs...
  for (let i = 0; i < 20000; i++) {
    const para = document.createElement('p');
    para.innerText = `This is paragraph ${i}`;
    para.classList.add('asdf');
    document.body.appendChild(para);
  }
  // ... then remove them again.
  let matches = document.getElementsByClassName('asdf');
  while (matches.length > 0) matches[0].remove();
}

function butt1() {
  lengthy1(LongAction);
}

function butt2() {
  lengthy2(LongAction);
}
body.waiting * {
  cursor: wait !important;
}
<body>
  <span id='notice'>xxx</span><br>
  <button onClick='butt1();'>Button 1</button>
  <button onClick='butt2();'>Button 2</button>
</body>

Function lengthy1() is my original attempt. This works often but not nearly always, and works more often when the delay is increased (!).

Function lengthy2() is rephrased from Adam H. This works fine in Adam H's version, but not in my rewritten version.

What would be the best way to reliably change the cursor during a lengthy operation?

EelcoV
  • 1
  • 3
  • What is happening inside of `func()`? I understand you might not be able to post the full code but inside there are there any other promises or anything like that? or is it just doing complex calculations or processing directly in the browser? – Adam H Mar 02 '23 at 18:06
  • never mind, i see it now. Let me play with this and get back to you. – Adam H Mar 02 '23 at 18:07
  • So after playing with this it seems like the way to address the reliability of setTimeout you are going to need to use both solutions or at least that's what worked for me. Ill update my answer with the second solution to illustrate it. – Adam H Mar 02 '23 at 20:23
  • That works, but still some of the time the cursor does not change. I can't easily reproduce that, but it seems unrelated to my code. For example, when I do a repetition of undo-redo-undo-redo-... the actions are always the same but some of the time the cursor stays plain. – EelcoV Mar 04 '23 at 08:02
  • And either solution should have worked, right? The setTimeout and the Promise both should have reliably changed the cursor? – EelcoV Mar 04 '23 at 08:03

1 Answers1

1

Use promises.

function DoSomeLengthyCalculation(){
  return new Promise((resolve, reject) => {
    // just use setTimeout to simulate a long process
    setTimeout(resolve, 3000);
  })
}

document.body.classList.add('waiting')
DoSomeLengthyCalculation().then(() => { document.body.classList.remove('waiting')});
body.waiting * { cursor: wait !important; }
<div>
  this is my content
</div>

Second Solution

Use both a promise and setTimeout.

function runLongAction(func) {
    // update the DOM now
  document.body.classList.add('waiting');
  document.getElementById('notice').innerText = 'waiting';
  
  // return a promise so the call can be chained
  // also wrapping this in a promise addresses the 
  // reliability of just using setTimeout
  return new Promise((resolve, reject) => {
    // use the setTimout work around to unblock the UI
    setTimeout(() => {
        // execute the long running function
      LongAction();
      // resolve the promise
      resolve();
    }, 0);
  })
    // wait for the promise to resolve then 
    // update the DOM to remove waiting indicators
    .then(() => {
        document.body.classList.remove('waiting')
      document.getElementById('notice').innerText = 'done';
    })
}

function LongAction() {
  // First add many paragraphs...
  for (let i = 0; i < 20000; i++) {
    const para = document.createElement('p');
    para.innerText = `This is paragraph ${i}`;
    para.classList.add('asdf');
    document.body.appendChild(para);
  }
  // ... then remove them again.
  let matches = document.getElementsByClassName('asdf');
  while (matches.length > 0) matches[0].remove();
}

function run() {
  runLongAction();
}
body.waiting * {
  cursor: wait !important;
}
<span id='notice'>xxx</span><br>
<button onClick='run();'>Run Long Action</button>
Adam H
  • 1,750
  • 1
  • 9
  • 24
  • I should have looked into Promises before. However, this solution works when the log process is a setTimeout(), but not with "real" code. I realize now that I should have asked a better question. I will prepare one separately. So this answer really put me on the right track. Thanks! – EelcoV Mar 02 '23 at 06:35
  • @EelcoV this should work with "real" code so please feel free to post a link to your new question here so I can take a look and help you out. Alternatively you could just edit this question to expand on the problem you are facing with promises. – Adam H Mar 02 '23 at 07:48