4

I have some jQuery inside javascript functions that changes text on a page and fades it in and out at specific time intervals. I want the functions to run in order one after the other, after each function finishes doing its effects.

dialogueExchange1();
dialogueExchange2();
dialogueExchange3();

function dialogueExchange1() {
  $('.text-area1').text("hey");
  $('.text-area1').delay(1000).showDialogue(800, 3000).prepareDialogue(800, "hey, are you awake?");
}

function dialogueExchange2() {
  $('.text-area1').delay(900).showDialogue(800, 4000).prepareDialogue(800, "wake up").delay(900);

  $('.text-area2').text("...");
  $('.text-area2').delay(1200).showDialogue(800, 1500).fadeOut(800);
}

function dialogueExchange3() {
  $('.text-area1').delay(900).showDialogue(800, 4000).prepareDialogue(800, "let's go").delay(900);

  $('.text-area2').text("not yet");
  $('.text-area2').delay(1200).showDialogue(800, 1500).fadeOut(800);
}

The showDialogue and prepareDialogue are methods I created that delay and fade text in and out. That is working fine. Basically, I'm just trying to get the text to change in the text area selectors after a specific time. What is currently happening is that all the functions are being run at the same time, thus firing the text changing effects all at the same time. I want dialogueExchange1 to do its effects then when it is done, for dialogueExchange2 to do its effects and then when its done, etc etc.

I have tried messing around with queues, timeouts and callbacks via the solutions below, but I haven't gotten it to do exactly what I want:

how to avoid callback chains?

How do I chain or queue custom functions using JQuery?

I had this working in the past and doing what I wanted by just having all the text changing methods chained together in one line of code but it just looks bad. Having it broken up in functions and running them in order would make it more organized and helpful to keep track of the text changes and delay times. Thanks!

EDIT: showDialogue and prepareDialogue functions as requested

$.fn.showDialogue = function(fadeInTime, showTextTime) {
    this.fadeIn(fadeInTime).delay(showTextTime);
    return this;
};

$.fn.prepareDialogue = function(fadeOutTime, dialogue) {
    this.fadeOut(fadeOutTime, function() {
        $(this).html(dialogue);
    });
    return this;
};

SOLUTION EDIT2: Thanks for the responses everyone and to whoughton for first suggesting the use of promise(). This is my solution at the moment, but I'm sure I'm going to refactor it down the road and change it now that I have seen Shaunak's answer.

dialogueExchange1();

function dialogueExchange1() {
    $('.text-area1').text("hey");
    $('.text-area1').delay(1000).showDialogue(800, 3000).prepareDialogue(800, "hey, are you awake?");

    $('.text-area1, .text-area2, .text-area3').promise().done(function() {
        dialogueExchange2();     
    });
}

function dialogueExchange2() {
    $('.text-area1').delay(900).showDialogue(800, 4000).prepareDialogue(800, "wake up");

    $('.text-area3').text("...");
    $('.text-area3').delay(1800).showDialogue(800, 1500).fadeOut(800);

    $('.text-area1, .text-area2, .text-area3').promise().done(function() {
        dialogueExchange3();     
    });
}

function dialogueExchange3() {
    $('.text-area1').delay(900).showDialogue(800, 4000).prepareDialogue(800, "come on let's go");

    $('.text-area2').text("hold on");
    $('.text-area2').delay(1200).showDialogue(800, 1500).fadeOut(800);
}

This way it gives me a lot of flexibility in refining delay times to reflect and mimic a conversation. The next function only runs when the effects within a function are finished, as made possible by promise(). Here is a jsFiddle link if you want to see it in action.

Community
  • 1
  • 1
Alex E
  • 466
  • 1
  • 6
  • 17
  • can you prepare a fiddle with the actual code? because a solution to your problem depends on what happens in showDialogue and prepareDialogue functions. – Shaunak Aug 13 '13 at 16:05
  • 1
    added the showDialogue and prepareDialogue functions – Alex E Aug 13 '13 at 16:40
  • Okay I have added a answer below that should work for you. I have created a generic example which is very simple so you get it. Then how to apply it to your case. You can use it to chain any number of animation function in the order you want. – Shaunak Aug 13 '13 at 18:47

5 Answers5

6

Here is a a way to do this with promises as @whoughton pointed out:

Solution using .promise() ( jsfiddle )

Html:

<div class="hasEffects effect1"> Div 1</div>
<div class="hasEffects effect2"> Div 2</div>
<div class="hasEffects effect3"> Div 3</div>

<button id="animate">animate</button>

Jquery:

effects1 = function(){
    $(".effect1").effect("shake", {}, 1000);
    return $(".effect1");
// or you can return $(".hasEffects"); if you are running effects on multiple elements
};

effects2 = function(){
    $(".effect2").effect("shake", {}, 1000);
    return $(".effect2");
};

effects3 = function(){
    $(".effect3").effect("shake", {}, 1000);
    return $(".effect3");
};

$("#animate").click(function(){
    runAnimations([effects1,effects2,effects3]);
});

 runAnimations = function(functionArray) {
    //extract the first function        
    var func = functionArray.splice(0, 1);

    //run it. and wait till its finished 
    func[0]().promise().done(function() {

        //then call run animations again on remaining array
        if (functionArray.length > 0) runAnimations(functionArray);
    });

}

Here is the jsfiddle

How this works

You pass an array of all the functions you need to chain in the runAnimation function which is run recursively until all functions are completed. They way it halts the execution of next function before animation in previous one is completed is using .promise() feature of jquery.

Everytime the runAnimation() runs, it extracts the first function in the array and runs it, and after .proimise().done() is executed for that function it call runAnimation() again with the remaining array.

The real trick is in understanding how .promise() works. It waits for all animations running on all the selectors passed to it. So in the individual effects function you can run effects on as many elements as you want. As long as you return correct selector you should be good.

For example, suppose in all thee effects you wanted to run 3 different effects on all 3 divs. Thats fine, just return $("hasEffects") from the function and it will work. :)

In your case

In your particular case here is how you can use this example. Add a grouping class on all your elements for example class="hasEffects" . And return a jquery selector for them from your animation functions. Then chain them using runAnimation. Here is how it should look:

function dialogueExchange1() {
   $('.text-area1').text("hey");
   $('.text-area1').delay(1000).showDialogue(800, 3000).prepareDialogue(800, "hey, are you awake?");
   return $(".hasEffects");
}

function dialogueExchange2() {
   $('.text-area1').delay(900).showDialogue(800, 4000).prepareDialogue(800, "wake up").delay(900);

   $('.text-area2').text("...");
   $('.text-area2').delay(1200).showDialogue(800, 1500).fadeOut(800);
   return $(".hasEffects");
}

function dialogueExchange3() {
   $('.text-area1').delay(900).showDialogue(800, 4000).prepareDialogue(800, "let's go").delay(900);

   $('.text-area2').text("not yet");
   $('.text-area2').delay(1200).showDialogue(800, 1500).fadeOut(800);
   return $(".hasEffects");
}

runAnimations([dialogueExchange1,dialogueExchange2,dialogueExchange3]);

runAnimations = function(functionArray){
        var func = functionArray.splice(0,1);
          func[0]().promise().done(function(){
            if(functionArray.length > 0 ) runAnimations(functionArray);
          });

    }
Shaunak
  • 17,377
  • 5
  • 53
  • 84
  • Thanks Shaunak, I was looking into promises before seeing your solution and it definitely looked like the way to go. Thank you for the your solution and in-depth explanation. – Alex E Aug 13 '13 at 19:33
  • I appreciate you doing this as well, I didn't unfortunately have the time then to go into this much detail, I'm glad you did! Good work. – whoughton Aug 14 '13 at 14:28
2

I would recommend using a system of promises, jQuery has its own implementation:

http://api.jquery.com/promise/

whoughton
  • 1,395
  • 11
  • 21
1

In general, the way you do this in jQuery happens in two ways. First a word about asynchronous stuff in Javascript.

When you call fadeOut(), it acts like there are two threads of execution. One of these threads starts making a series of changes to the element to make it less and less visible. The other thread continues with the commands following the fadeOut(). Perhaps the fading in of the other screen element. You will need to read up on how this works because I'm using the word "thread" really loosely and the details are different. But you can think of it as two things happening at once. That's why you see all your elements animating at the same time. (Sometimes that's the desired behavior.)

One way to make animations happen in sequence is this. You string the animations calls on the same element:

$(whatever).delay(400).fadeIn(300).

So the animations on the same element wait for previously queued animations to finish before starting.

The other way allows multiple elements to change in sequence:

$(whatever).delay(400).fadeOut(300, function() {
    $(whateverelse).fadeIn(400);
}

The function isn't called until the fadeOut() is finished. So the fadeIn() doesn't begin until the other is finished.

Your situation is more complex, partly because of the two methods, but can be done using these two methods. Your methods may need to accept an additional parameter that is a function to call when those animations are complete.

NOTE: Since your two methods just work on 'this' and return 'this' they add the animations to the same element you were working on. Those animations should remain in sequence.

Its only when you need animations in one element to wait on the other element's animations that you need the 2nd form.

NOTE2: I noticed that the functions hang animations off the same elements so you could do something like this:

function dialogueExchange2() {
  $('.text-area1').delay(900).showDialogue(800, 4000).prepareDialogue(800, "wake up").delay(900,
    function() {

      $('.text-area2').text("...");
      $('.text-area2').delay(1200).showDialogue(800, 1500).fadeOut(800);
    });
}

function dialogueExchange3() {
  $('.text-area2').delay(1, function() { // this just makes #3 wait for #2 to finish
    $('.text-area1').delay(900).showDialogue(800, 4000).prepareDialogue(800, "let's go").delay(900,
      function() {

        $('.text-area2').text("not yet");
        $('.text-area2').delay(1200).showDialogue(800, 1500).fadeOut(800);
      });
  });
}

When you call them you do it like this:

dialogueExchange1();
dialogueExchange2();
dialogueExchange3();
Lee Meador
  • 12,829
  • 2
  • 36
  • 42
  • I had it set up this way before with the text area selectors changing via the function callback you have above. The issue is that it gets messy when I have multiple text areas changing text at the same time and it gets tough keeping track of it all and the delay times. It also isn't very dynamic since the animations have to finish before continuing, so I can't easily change different text areas at the same time. – Alex E Aug 13 '13 at 16:09
  • When you say "different text areas" do you mean areas 1 and 2 need to be able to change in order while areas 3 and 4 are changing in their own order? – Lee Meador Aug 13 '13 at 16:12
  • Yes, I have multiple text areas, just empty divs that contain the text when it appears. I want the text in different areas to change, appear and disappear after a certain amount of time as to reflect a conversation. This is for a game I'm making. – Alex E Aug 13 '13 at 16:35
0

Here is a fiddle to show a very simple version of what you are trying to do.. I think. http://jsfiddle.net/wembz/

Each item should be ran after a set delay since they are all async. For example if the first animation is 1500ms long and you want the text to change 300ms after that one finished it would have a delay of 1800ms... Hopefully that makes sense.

var timeoutQueue = [],
    $ta1 = $('.text-area1'),
    $ta2 = $('.text-area2');

//Each of the DELAY_IN_MS in the functions should be the exact point in ms where the animation should occur.
timeoutQueue.push(
    setTimeout(function(){
        $ta1.text("hey");
    }, DELAY_IN_MS)
);
timeoutQueue.push(
    setTimeout(function(){
        $ta1.showDialogue(800, 3000);
    }, DELAY_IN_MS)
);
timeoutQueue.push(
    setTimeout(function(){
        $ta1.prepareDialogue(800, "hey, are you awake?");
    }, DELAY_IN_MS)
);
timeoutQueue.push(
    setTimeout(function(){
        $ta1.showDialogue(800, 4000);
    }, DELAY_IN_MS)
);
timeoutQueue.push(
    setTimeout(function(){
        $ta1.prepareDialogue(800, "wake up");
    }, DELAY_IN_MS)
);
timeoutQueue.push(
    setTimeout(function(){
        $ta2.text("...");
    }, DELAY_IN_MS)
);
timeoutQueue.push(
    setTimeout(function(){
        $ta2.showDialogue(800, 1500)
    }, DELAY_IN_MS)
);
timeoutQueue.push(
    setTimeout(function(){
        $ta2.fadeOut(800)
    }, DELAY_IN_MS)
);
timeoutQueue.push(
    setTimeout(function(){
        $ta1.showDialogue(800, 4000);
    }, DELAY_IN_MS)
);
timeoutQueue.push(
    setTimeout(function(){
        $ta1.prepareDialogue(800, "let's go");
    }, DELAY_IN_MS)
);
timeoutQueue.push(
    setTimeout(function(){
        $ta2.text("not yet");
    }, DELAY_IN_MS)
);
timeoutQueue.push(
    setTimeout(function(){
        $ta2.showDialogue(800, 1500);
    }, DELAY_IN_MS)
);
timeoutQueue.push(
    setTimeout(function(){
        $ta2.fadeOut(800);
    }, DELAY_IN_MS)
);

EDIT: http://jsfiddle.net/AmqHB/

var timeoutQueue = [],
    currentDelay = 500;
    $ta1 = $('#mytext');

function newSequence(fn, expectedlength, delayafter){
    timeoutQueue.push( setTimeout(fn, currentDelay) );
    currentDelay += (expectedlength + delayafter);
}

newSequence(function(){$ta1.fadeOut(500);},500,1000);
newSequence(function(){$ta1.text("hey").fadeIn(500);},500,1000);
newSequence(function(){$ta1.fadeOut(500);},500,1000);
newSequence(function(){$ta1.text("It Can't possibly be this simple.").fadeIn(500);},500,1000);

Edit 2: http://jsfiddle.net/AmqHB/1/ I don't think there is any more efficient, but here you go with loopy goodness.

Daniel
  • 579
  • 2
  • 7
  • I have also tried it this way as well, the main issue is that I have to keep track of the total timeout delay which is a huge pain. I would rather not have to worry about that. – Alex E Aug 13 '13 at 16:39
  • 1
    I don't think this the above is Ideal solution because you are assuming that each function has just one async call , and that too on one element. Both these conditions are false so this solution wont work in a generic case. IMHO the ideal way is to do it using $.promise, This is exactly why they added promise function in jquery! So no point in reinventing the wheel. Please check my answer. Works on any number of animations in individual functions and you don't need to time them explicitly. All you do is pass in the array of functions to chain in the sequence you want. – Shaunak Aug 13 '13 at 19:10
  • Promise is a good solution. The above solution works with any number of elements. Jquery elements are not required. This is just a simplified method of building a sequence of events. – Daniel Aug 13 '13 at 20:06
-1

what about this :

dialogueExchange1();


function dialogueExchange1() {
  $('.text-area1').text("hey");
  $('.text-area1').delay(1000).showDialogue(800, 3000).prepareDialogue(800, "hey, are you awake?");
dialogueExchange2();
}

function dialogueExchange2() {
  $('.text-area1').delay(900).showDialogue(800, 4000).prepareDialogue(800, "wake up").delay(900);

  $('.text-area2').text("...");
  $('.text-area2').delay(1200).showDialogue(800, 1500).fadeOut(800, function(){
  dialogueExchange3();
  });
}

function dialogueExchange3() {
  $('.text-area1').delay(900).showDialogue(800, 4000).prepareDialogue(800, "let's go").delay(900);

  $('.text-area2').text("not yet");
  $('.text-area2').delay(1200).showDialogue(800, 1500).fadeOut(800);
}
Mohammad Masoudian
  • 3,483
  • 7
  • 27
  • 45