0

I'm using the typewriter effect from this answer on SO with the intention of having the first div type out, followed by the second div (when the first div has finished typing out).

<div class="text">
 Lorem ipsum dolor sit amet...
</div>
<div class="text">
 Cras eros augue, tempor...
</div>

JS:

// Create typewrite function as jQuery plugin:
// (From https://stackoverflow.com/a/22266737/3217306)
$.fn.typeText = function() {
    var keystrokeDelay = 10,

        self = this,
        str = self.html(),
        i = 0,
        isTag,
        text;

    self.html("");

    (function type() {
        text = str.slice(0, ++i);
        if (text == str) {
            console.log("completed typing");
            return;
        }
        self.html(text);

        var char = text.slice(-1);
        if (char == "<") {
            isTag = true;
        }
        if (char == ">") {
            isTag = false;
        }

        if (isTag) {
            return type();
        }
        setTimeout(type, keystrokeDelay);
    } ());
};

var textElements = $(".text");

// For each div.text, type them out
textElements.each(function() {
    $(this).typeText();
});

The trouble is, both type out at the same time. I've tried so many different things involving promises, callbacks etc. but I can't get them to work for this situation.

JSFiddle

Community
  • 1
  • 1
binaryfunt
  • 6,401
  • 5
  • 37
  • 59

4 Answers4

2

There are a lot of ways to do this, the way I will show is just one option :

... 
self.html("");

$.fn.typeText.queue = $.fn.typeText.queue || [] // make a shared variable between the elements that call this function. This will call them in the order that were added

$.fn.typeText.queue.push(function type() {
    text = str.slice(0, ++i);
    if (text == str) {
        console.log("completed typing");
        $.fn.typeText.queue.shift(); // remove first element which just ended
        if ($.fn.typeText.queue.length > 0) { // if there are more
            $.fn.typeText.queue[0]() // start next
        }
        return;
    }
    ...
   setTimeout(type, keystrokeDelay);
});

if ($.fn.typeText.queue.length == 1) { // if its the first element added or no elements are in queue, call it
    $.fn.typeText.queue[0]();
}

};

This will work for any amount of elements and will call them in order. If you would wish to have groups which all run at the same time but within each group they go in order, its easy to change the function to do it. One way would be to pass an array to the method and use that instead of $.fn.typeText.queue

Complete code :

$.fn.typeText = function() {
    var keystrokeDelay = 10,
    
      self = this,
        str = self.html(),
        i = 0,
        isTag,
        text;

    self.html("");

    $.fn.typeText.queue = $.fn.typeText.queue || []

$.fn.typeText.queue.push(function type() {
        text = str.slice(0, ++i);
        if (text == str) {
            console.log("completed typing");
      $.fn.typeText.queue.shift(); // remove first element which just ended
          if ($.fn.typeText.queue.length > 0) { // if there are more
              $.fn.typeText.queue[0]() // start next
          }
          return;
        }
        self.html(text);

        var char = text.slice(-1);
        if (char == "<") {
            isTag = true;
        }
        if (char == ">") {
            isTag = false;
        }

        if (isTag) {
            return type();
        }
        setTimeout(type, keystrokeDelay);
    })

  if ($.fn.typeText.queue.length == 1) { // if its the first element added or no elements are in queue, call it
      $.fn.typeText.queue[0]();
  }
    
};

var textElements = $(".text");

textElements.each(function() {
    $(this).typeText();
});
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<div class="text">
  Lorem ipsum dolor sit amet, consectetur adipiscing elit. Suspendisse condimentum erat non velit euismod, ac tristique orci hendrerit. Nullam varius in sem sed tempor. Maecenas tincidunt, nisi a accumsan maximus, lectus sem congue ligula, eget tempus neque lacus quis arcu. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; Quisque at purus eleifend, euismod nibh ut, auctor nulla. Phasellus convallis nulla urna, porta egestas justo sollicitudin sit amet.
</div>
<div class="text">
  Cras eros augue, tempor ut velit ornare, dignissim sollicitudin mi. Duis accumsan diam sed nulla sodales mattis. Pellentesque nec efficitur nunc, vitae rutrum massa. Pellentesque metus metus, consequat in auctor non, lobortis eget ligula. Sed volutpat, dui vulputate congue iaculis, justo massa consequat turpis, id facilisis ex sapien eget ligula. Praesent eu erat lorem. Mauris id dui orci. Mauris vitae urna tortor. Proin luctus id purus non maximus.
</div>
juvian
  • 15,875
  • 2
  • 37
  • 38
  • Does that go inside `$.fn.typeText = function() { }` ? – binaryfunt Nov 03 '16 at 02:11
  • 1
    @binaryfunt added snippet – juvian Nov 03 '16 at 02:15
  • I'm a little confused by `$.fn.typeText.queue = $.fn.typeText.queue || []`. What does `|| []` do? – binaryfunt Nov 03 '16 at 02:47
  • 1
    @binaryfunt first time funcition is called, $.fn.typeText.queue is undefined. Undefined is a falsy value on js, so with doing || you are evaluating the second choice, which gives [] which is a truthy value. So $.fn.typeText.queue basically gets assigned to []. On any other following call, $.fn.typeText.queue is defined so second choice is not evaluated and that way the array stays always the same for all elements – juvian Nov 03 '16 at 02:51
  • 1
    It would be the same to do if ($.fn.typeText.queue == undefined) {$.fn.typeText.queue = []} – juvian Nov 03 '16 at 02:52
0

Use setTimeout and a counter as you did in the jQuery plugin.

var textElements = $(".text");
var counter = 0;

function typeDiv(){ 
     if(counter < textElements.length -1){
           textElements[counter].typeText();
            counter += 1;
            setTimeout(typeDiv, 2000);
      }
 }
TheValyreanGroup
  • 3,554
  • 2
  • 12
  • 30
0

Note that if you put the divs you want to have the typewriter effect into one div, they will automatically progress one after the other.

<div class="text">
  <p>Lorem ipsum dolor sit amet…</p>
  <p>Cras eros augue, tempor…</p>
</div>
$(".text").typeText();
/* Instead of
textElements.each(function() {
    $(this).typeText();
}); */

See this JSFiddle.

binaryfunt
  • 6,401
  • 5
  • 37
  • 59
0

If you can use ES6, I'd recommend using Promises and async functions, with a for loop instead of .each(). Once you understand these, it makes the solution quite straightforward (some unaltered code omitted for brevity):

$.fn.typeText = function() {
    var keystrokeDelay = 10,
        self = this,
        ...
    return new Promise(function(resolve) { // <--- new
        self.html("");
        self.addClass("visible"); // <--- new, explained below

        (function type() {
            text = str.slice(0, ++i);
            if (text == str) {
                console.log("completed typing");
                resolve(); // <--- new
                return;
            }
            self.html(text);
            ...
    });
}

var textElements = $(".text");

(async function run() { // new
    for (var j = 0; j < textElements.length; j++) {
        await $(textElements[j]).typeText();
        // Waits for the promise in the .typeText() func to be fulfilled
        // before progressing to the next iteration of the for loop
    }
})();

You'll need this bit of CSS too, because if the text is not hidden by default, then the bottom div will be completely visible while the top div is still typing out (note the self.addClass("visible") above):

.text {
  display: none;
}
.visible {
  display: block;
}

See this JSFiddle. (Code warnings show for async because JSFiddle doesn't have full ES6 hinting)

binaryfunt
  • 6,401
  • 5
  • 37
  • 59