1

I am so used to working with jQuery that I can't seem to figure out how I can do this in plain Javascript. I don't want to use jQuery because this is the only snippet that I use on the site and the library is too large for only that purpose.

This is the jQuery script (working): http://jsfiddle.net/1jt6dhzu/2/

$(document).ready(function () {
    var whatAmI = ["Fruit", "Not a vegetable", "A phone", "Yummie in tummy"];
    var j, i = 4,
        n = whatAmI.length;

    function changeSubTitle() {
        setTimeout(function () {
            j = Math.floor(Math.random() * n - 1);
            if (j >= i) j += 1;

            $(".page-header > h2").animate({
                "opacity": 0
            }, 700, function () {
                $(this).children("span").text(whatAmI[j]);
                $(this).animate({
                    "opacity": 1
                }, 700, changeSubTitle);
            });
        }, 1000);
        i = j;
    }
    changeSubTitle();
});

And I want to swap it for vanilla JS. A large part of the loop can stay, however, the timeout and callbacks have to be replaced. I figured because I don't need IE9 support I could do this with css3 transitions and add classes. This is what I have so far:

h2 {
    opacity: 1;
    transition: opacity 700ms;
}
h2.fade-out {
    opacity: 0;
}


document.addEventListener("DOMContentLoaded", function () {
    var whatAmI = ["Fruit", "Not a vegetable", "A phone", "Yummie in tummy"];
    var j, i = 4,
        n = whatAmI.length,
        heading = document.querySelector(".page-header > h2");

    function changeSubTitle() {
        setTimeout(function () {
            j = Math.floor(Math.random() * n - 1);
            if (j >= i) j += 1;

            heading.classList.add("fade-out");

            setTimeout(function () {
                heading.children("span")[0].innerHTML = whatAmI[j];
                heading.classList.remove("fade-out");
                setTimeout(changeSubTitle, 700);
            }, 700);
        }, 1000);
        i = j;
    }
    changeSubTitle();
});

Unfortunately this doesn't work. It would probably be better to swap out the timeOuts (except the most outer one) for events on transitionend. But I'm not sure how to implement this crossbrowser (IE 10 and higher, and other major browsers).

Bram Vanroy
  • 27,032
  • 24
  • 137
  • 239
  • `transitionend` is supported by IE10 and above, so if you're targeting eariler versions you're out of luck. https://developer.mozilla.org/en-US/docs/Web/Events/transitionend – Jan Sep 06 '15 at 12:48
  • @Jan I suppose I can leave out support for IE9 for this one. It isn't critical. – Bram Vanroy Sep 06 '15 at 12:54

1 Answers1

2

You had done almost everything right bar removing the children method which is implemented in a way specific to jQuery. Contrary to what I had posted earlier, JS does have a children function and it returns a HTMLCollection but it cannot filter based on element type by providing the type as param. It returns all child nodes and we must pick the correct one either by using the child element's index or by checking the type of element.

For this example (and for simplicity sake), replace

heading.children("span")[0].innerHTML = whatAmI[j];

with

heading.children[0].innerHTML = whatAmI[j]; // since span is the first and only child

or

heading.querySelector("span").innerHTML = whatAmI[j];

and it should work as expected.

document.addEventListener("DOMContentLoaded", function() {
  var whatAmI = ["Fruit", "Not a vegetable", "A phone", "Yummie in tummy"];
  var j, i = 2,
    n = whatAmI.length,
    heading = document.querySelector(".page-header > h2");

  function changeSubTitle() {
    setTimeout(function() {
      j = Math.floor(Math.random() * (n - 1));
      if (j >= i) j += 1;

      heading.classList.add("fade-out");

      setTimeout(function() {
        heading.querySelector("span").innerHTML = whatAmI[j];
        heading.classList.remove("fade-out");
        setTimeout(changeSubTitle, 700);
      }, 700);
      i = j;
    }, 1000);
  }
  changeSubTitle();
});
h2 {
  opacity: 1;
  transition: opacity 700ms;
}
h2.fade-out {
  opacity: 0;
}
<header class="page-header">
  <h1>Bananas</h1>

  <h2><span>A phone</span></h2>

</header>

Using transitionend:

transitionend is a single event listener which is attached and fired whenever the transition on an element's properties end. It will be fired both after the addition and removal of fade-out clas and so, we have to manually check what is the state of the element when the event is fired and then act based on it.

Here I have used the getComputedStyle property to check the opacity state of the element and if it is in faded-out state then change text and remove fade-out class (or) else, call the changeSubTitle function again.

if (window.getComputedStyle(heading).opacity == 0) {
  heading.querySelector("span").innerHTML = whatAmI[j];
  heading.classList.remove("fade-out");
} else
  changeSubTitle();

document.addEventListener("DOMContentLoaded", function() {
  var whatAmI = ["Fruit", "Not a vegetable", "A phone", "Yummie in tummy"];
  var j, i = 2,
    n = whatAmI.length,
    heading = document.querySelector(".page-header > h2");

  function changeSubTitle() {
    setTimeout(function() {
      j = Math.floor(Math.random() * (n - 1));
      if (j >= i) j += 1;

      heading.classList.add("fade-out");
      i = j;
    }, 1000);
  }
  heading.addEventListener('transitionend', function() {
    if (window.getComputedStyle(heading).opacity == 0) {
      heading.querySelector("span").innerHTML = whatAmI[j];
      heading.classList.remove("fade-out");
    } else
      changeSubTitle();
  });
  changeSubTitle();
});
h2 {
  opacity: 1;
  transition: opacity 700ms;
}
h2.fade-out {
  opacity: 0;
}
<header class="page-header">
  <h1>Bananas</h1>

  <h2><span>A phone</span></h2>

</header>

Alternately (like you commented), you could check the class present on the element also and then decide accordingly. This seems a simpler method compared to my earlier one.

if (heading.classList.contains("fade-out")) {
  heading.querySelector("span").innerHTML = whatAmI[j];
  heading.classList.remove("fade-out");
} else
  changeSubTitle();

document.addEventListener("DOMContentLoaded", function() {
  var whatAmI = ["Fruit", "Not a vegetable", "A phone", "Yummie in tummy"];
  var j, i = 2,
    n = whatAmI.length,
    heading = document.querySelector(".page-header > h2");

  function changeSubTitle() {
    setTimeout(function() {
      j = Math.floor(Math.random() * (n - 1));
      if (j >= i) j += 1;

      heading.classList.add("fade-out");
      i = j;
    }, 1000);
  }
  heading.addEventListener('transitionend', function() {
    if (heading.classList.contains("fade-out")) {
      heading.querySelector("span").innerHTML = whatAmI[j];
      heading.classList.remove("fade-out");
    } else
      changeSubTitle();
  });
  changeSubTitle();
});
h2 {
  opacity: 1;
  transition: opacity 700ms;
}
h2.fade-out {
  opacity: 0;
}
<header class="page-header">
  <h1>Bananas</h1>

  <h2><span>A phone</span></h2>

</header>

Both the above snippets have been tested in latest versions of Firefox, Chrome, Opera, IE11 and IE10 (using Emulation). It should work in latest version of Safari on Mac also. For Windows, I think Safari versions stopped with 5.1.x and still fire only webkitTransitionEnd event. To cater for these browsers, the method mentioned in this SO thread can be used.

Community
  • 1
  • 1
Harry
  • 87,580
  • 25
  • 202
  • 214
  • 1
    I deleted it because it contained its own logic error ;) . I got what it was that Harry was trying to do, and to fix it, you just need to add a parentheses around `(n-1)`. It makes the method less random and intuitive than having a loop `while (j===i) /* calculate random */` though. – Jan Sep 06 '15 at 13:17
  • Also, some thing odd is happening now. The point of defining `i` early on is to tell the loop that *this* is the item that we start with and that we don't want to show that again immediately after the first one. However, this doesn't work as expected. Even if you set `i = 2` (which it should be), this would still occur. Also, if you would look at your running code, you'll find that sometimes the text returned is *undefined*. So I'm guessing something goes wrong with assigning `j`. The strange thing is that this doesn't happen in jQuery (see my OP). – Bram Vanroy Sep 06 '15 at 13:19
  • @BramVanroy that most certainly happened in your jquery as well since it contains the same logic error. You just never noticed it. – Jan Sep 06 '15 at 13:21
  • @Jan You are right that it does show the same element multiple times consequently, however I never get to see an "undefined" with jQuery. Why is that? – Bram Vanroy Sep 06 '15 at 13:27
  • @BramVanroy: Yes, you are defining `i` but not `j` and so at the first pass (remember the Timeout wouldn't have yet happened), the i=j statement makes both *undefined*. – Harry Sep 06 '15 at 13:36
  • @Jan Also, when `i` is 2, and the randomiser sets `j` to 2, then for some reason the "corrector" (j += 1) doesn't lead to `j=3`. I don't understand why. [See here](http://jsfiddle.net/1jt6dhzu/4/) where I explicitly set j to 2 on the first run. – Bram Vanroy Sep 06 '15 at 13:39
  • @Harry Thank you for your efforts so far (+1). If you know how, can you make it cross-browser for IE10 and above and all other major browsers? If not I'll try something myself. I know it's a hussle but considering I asked for a cross-browser solution in my OP I figured I could simply ask. `^^` – Bram Vanroy Sep 06 '15 at 13:41
  • @BramVanroy: I didn't get the whole randomize part. The whole thing there seems quite counter intuitive to me. Can you explain in English what is that you are trying to achieve with the loop? – Harry Sep 06 '15 at 13:41
  • Well actually the loop is something that other peeps on SO came up with in [this question](http://jsfiddle.net/1jt6dhzu/4/) of mine. The goal is to have an array of elements and randomize/shuffle it and in an infinite loop display one after the other without ever having two (or more) times the same element following itself. So "A phone" - "A phone" - "Fruit" is not acceptable, but "A phone" - "Fruit" - "A phone" is. defining `i` at the beginning is necessary to make sure that the text that's already there in HTML is the same as one in the array. Let me know if it isn't clear! – Bram Vanroy Sep 06 '15 at 13:46
  • @Harry The (working) randomizer works like this: Get a value between 0 and 2: `Math.random() = 0->0.999...` * `length of array minus one = 3` and then `floor` it. So you get a value between 0 and 2, which is one less than the length of the array. THEN, if the randomized value is the same or bigger than the last, increase it by one. So the last value 3, for example, will be returned every time the randomizer returns 2, but 0 is only available if the randomizer returns 0 and the current value is larger than 0. etc. So the spread of the random values is affected by logic. – Jan Sep 06 '15 at 14:19
  • 1
    Now the NON-working randomizer didn't work, because what I'm guessing is the above behavior was the intended one, but instead it returned a value between 0 and 4 and then subtracted 1. So the returned range was between -1 and 3. And `whatAmI[-1]` didn't return anything because it wasn't set. And setting it would have created a property rather than an index but anyways, that's why it didn't work – Jan Sep 06 '15 at 14:24
  • @Jan: Hmm yeah got that but since the `i=j` was outside the timeout and it would get executed once before the randomizer kicks in, I could note than i and j were both becoming undefined by the time the first time out happens. I had moved the statement to be within the time out and it seems to be working fine now. I had modified i to 2 based on Bram's statement that it corresponds to the index of the value that is in HTML. – Harry Sep 06 '15 at 14:24
  • @Harry Oh sorry, I thought that was the part you were wondering about. Never mind – Jan Sep 06 '15 at 14:25
  • @Jan: No worries mate. You have been of a lot of help. I was wondering about it initially but understood post Bram's comment :) – Harry Sep 06 '15 at 14:26
  • @BramVanroy: I have checked the snippets (both) in all recent browser versions and they seem to be working fine (I am expecting Safari to work because Chrome and Opera does). The loop also seems to work properly post a small correction that I made. Let me know in case you are still having any troubles with it. – Harry Sep 06 '15 at 14:35
  • 1
    I'll test in a minute Harry (I'm sorry for the late reply, I just went for a run). One thing I note, wouldn't it be more straightforward to test if the class is already present instead of getComputedStyle? `if(heading.classList.contains("fade-out"))`? – Bram Vanroy Sep 06 '15 at 15:30
  • @Jan Thank you for the interesting discussion and explanation! – Bram Vanroy Sep 06 '15 at 15:31
  • 1
    I have tested in Edge, IE 11 and 10, FF and Chrome. Approved! Thanks again for the help! – Bram Vanroy Sep 06 '15 at 15:55