0

I am working on a WordPress plugin. One of its features involves hiding and revealing segments of text by class using <span>.

This functionality works, but I have been hoping to enhance it by having the segments of text reveal one letter at a time (quickly of course) as though they were being typed out very quickly, rather than all at once in large chunks.

I know there are animations out there for this kind of thing ... and perhaps that would be a better solution, but I've been trying to keep it. But the functionality is not really graphic or "animation" oriented; my intent is more just to make a text-based feature look prettier.

I've gotten the portion of the code that builds each segment of text character by character, but I'm trying to insert a very short (5-10ms) delay between each character so that the effect is actually visible. I simply cannot get the setTimeout function to work; can anyone please give me some suggestions?

For simplicity I'm just including the segment of the text that does this; let me know if more context is needed. The following is the FOR loop that goes through every element of an array called cols[] and reveals each element in the array by character. This code works but the delay is never observed.

numberofSnippets = the size of the array cols[]

  for (c = 0; c < numberofSnippets; c++)                
      {
          h=0;
         currentshown = '';                 
         snippet = cols[c].textContent;         
         sniplength = snippet.length;           

         (function addNextCharacter()  
        {   
            onecharacter = snippet.charAt(h);
                currentshown = currentshown.concat(onecharacter);
            cols[c].textContent = currentshown;
             h=h+1;
            if (h < sniplength) {window.setTimeout(addNextCharacter, 200); }

          })();*/


          }

    }    
}        
NFB
  • 642
  • 8
  • 26
  • `setTimeout` accepts milliseconds as the second argument, and 5-10ms is not visible, you probably want to pass in something like `200` and then go from there. – adeneo Dec 14 '15 at 22:54
  • Thanks - just updated the text to reflect that. I've tried numbers up to 1000 and it doesn't delay. – NFB Dec 14 '15 at 22:55
  • Here is a little more information after the updates suggested below and above: When I step through this code, the first time it gets to the line 'if (h < sniplength) { window.setTimeout ...' and jumps out back to the top of the FOR loop. The value of h at this point is 1, and the value of sniplength is 7; so I am not understanding what the issue is. – NFB Dec 14 '15 at 23:11
  • I don't think this qualifies as an exact duplicate, but it's very much related to http://stackoverflow.com/questions/111102/how-do-javascript-closures-work?rq=1 and http://stackoverflow.com/questions/750486/javascript-closure-inside-loops-simple-practical-example – Kevin B Dec 14 '15 at 23:19
  • actually yeah it might be an exact dupe. the whole problem is closure usage within a loop. – Kevin B Dec 14 '15 at 23:26

2 Answers2

0

Well, one issue is that you're setting your timeout to 0, which means, effectively 'next tick'. If you want a 5 second delay, for example, you need to put 5000 in there as the second param.

Paul
  • 35,689
  • 11
  • 93
  • 122
  • Yes, thanks and sorry - I have tried that but finally set it to zero for another reason. I'll update the example back to reflect 10ms. – NFB Dec 14 '15 at 22:55
0

There were a few oddities in your code that was preventing the setTimeout from performing as expected, mostly due to the closure reusing variables within the loop due to the fact that the loop isn't going to wait for the IIFE to finish recursively executing with a setTimeout. I solved that by moving those variables to parameters passed to addNextCharacter.

var cols = document.getElementsByClassName('foo');
var numberofSnippets = cols.length;

for (var c = 0; c < numberofSnippets; c++) {
  (function addNextCharacter(h, c, snippet, sniplength, currentshown) {
    var onecharacter = snippet.charAt(h);
    currentshown = currentshown.concat(onecharacter);
    cols[c].textContent = currentshown;
    h = h + 1;
    if (h < sniplength) {
      setTimeout(function () {
        addNextCharacter(h, c, snippet, sniplength, currentshown);
      }, 10);
    }
  })(0, c, cols[c].textContent, cols[c].textContent.length, '');
}
<div class="foo">Apple</div>
<div class="foo">Banana</div>
<div class="foo">Orange</div>
<p class="foo">There were a few oddities in your code that was preventing the setTimeout from performing as expected, mostly due to the closure reusing variables within the loop due to the fact that the loop isn't going to wait for the IIFE to finish recursively executing with a setTimeout. I solved that by moving those variables to parameters passed to addNextCharacter.</p>

And here's the obligatory .forEach version which avoids needing to pass the variables around as parameters.

var cols = document.getElementsByClassName('foo');
var numberofSnippets = cols.length;

[].forEach.call(cols, function(el) {
  var snippet = el.textContent;
  var sniplength = snippet.length;
  var currentshown = '';
  (function addNextCharacter(h) {
    var onecharacter = snippet.charAt(h);
    currentshown = currentshown.concat(onecharacter);
    el.textContent = currentshown;
    h = h + 1;
    if (h < sniplength) {
      setTimeout(function() {
        addNextCharacter(h);
      }, 1000);
    }
  })(0);
});
Kevin B
  • 94,570
  • 16
  • 163
  • 180
  • Wow. Thank you. Trying this code now in context and will post again shortly on the results. – NFB Dec 14 '15 at 23:15
  • what do you mean it jumps out of the loop? it has to leave the loop before being finished with the delayed iteration. you can't get around that, if it doesn't leave the loop the browser won't be able to render the text incrementally. – Kevin B Dec 14 '15 at 23:32
  • OK - I see that this works in the code window, however there is another complicating factor I didn't include in the original question. Before this section of the code runs, the classes concerned were hidden altogether. In order to be able to able to reveal them, the FOR loop stores each cols[c] value in var snippet, then sets col[c] = '', and sets it to display: inline before running the reveal loop. This means that col[c].textContent can't be passed directly to the addNextCharacter loop. And when I try to pass it instead via snippet, I'm getting the same problem where it jumps out. – NFB Dec 14 '15 at 23:38
  • I'm not sure i'm following. Does the 2nd snippet change anything? It should perform identically, but it is written closer to what you had originally, and not passing col[c].textContent directly to addNextCharacter. – Kevin B Dec 14 '15 at 23:41
  • why will the browser not be able to do that? – NFB Dec 14 '15 at 23:41
  • because the setTimeout pushes the callback to the callback queue, and the callback queue can't be processed until after the for loop (and everything after it) is complete. the forEach snippet will have the same problem. – Kevin B Dec 14 '15 at 23:41