vsync
vsync

Reputation: 130215

transitionEnd event with multiple transitions, detect last transition

transitionEnd event is fired on the transition which ends first and not last, which is not the desired behavior. Any workarounds?

document.querySelector('a').addEventListener('transitionend', function(){
  var time = (new Date().getMinutes()) + ':' + (new Date().getSeconds());
  console.log('transitionEnd - ', time);
});
a{
  display:block;
  opacity:.5;
  width:100px;
  height:50px;
  background:lightblue;
}
a:hover{
  width:200px;
  height:100px;
  background:red;
  transition: 4s width,     /* <-- "transitionEnd" should fire after this */
              2s height,
              .5s background;  
}
<a>Hover me</a>

Now that I check on Chrome (I do not use that browser), I see that the event gets called 3 times, one per transition. Anyway, I need it to fire for the last one, and in Firefox. (I can't know how many transitions were on the element anyway, to know which was the last)

Upvotes: 13

Views: 5816

Answers (4)

Robert
Robert

Reputation: 2753

I solve this problem in a little simpler way(in my opinion). In my case i was animating path of svg (clipping path). So i listening on cilpPath element transitionEnd event. Each path was animated with different time but(i'n my case only one property). 

Solution:

Counting all transitionStart and store elements in set. (if is one element with many transitions i believe that can be stored propertyName or some id: element-property). And then on transtionEnd event just remove element/property from current running transitions set. If set is empty then all transition was ended. 

(function() {
    window.addEventListener("load", () => {
        const animated = document.querySelectorAll("[data-animation]") || [];
        [...animated].forEach(target => {
            animate(target as HTMLElement);
        })
    })


    function animate(target: HTMLElement){
        let currentFrame = 0;
        let frameLimit = parseInt(target.getAttribute("data-animation-frames") || "0");

        const setFrame = (frame: number) => {
            currentFrame = frame;
            target.setAttribute("data-animation-frame", frame.toString());
        }

        const runningTransitions = new Set<HTMLElement>();

        target.addEventListener("transitionend", function transitionEnd(e){
            runningTransitions.delete(e.target as HTMLElement);
            if(runningTransitions.size)
                return;
            const nextFrame = currentFrame + 1;
            if(nextFrame > frameLimit){
                target.removeEventListener("transitionend", transitionEnd);
                return;
            }
            setFrame(nextFrame);
        })

        target.addEventListener("transitionstart", function transitionStart(e){
            runningTransitions.add(e.target as HTMLElement)
        })

        setFrame(currentFrame);
    }
})()

Upvotes: 0

2oppin
2oppin

Reputation: 1991

I now it's maybe old question, but encounter same trouble. Solved in that way:

document.querySelector('a').addEventListener('click', function(e){
  this.classList.toggle('animate');
  let style = window.getComputedStyle(this, null);
  Promise.all(style.transition.split(',').map((prop) => {
     prop = prop.trim().split(/\s+/);
     return Promise.race([
         new Promise((resolve) => {
              let h = (e) => {
                   if (e.propertyName == prop[0])
                        resolve('transitionEnd ' + prop[0]);
              };
              this.addEventListener('transitionend', h, {once:false});
         }),
         new Promise((resolve) => 
              setTimeout(
                  () => resolve('TIMEOUT ' + prop[0]),
                  prop[1].replace(/s/,'')*1000 + 100
              )
         )
     ])
  }))
  .then((res) => {
     console.log(res + 'DONE!!!!');
     /*  do your stuff */
  });
});

Explanation:

  • first we determine what properties actually should be changed, then split/map/convert it to perform manipulation. usually it have format like: "width 4s ease 0s, height 2s ease 0s,.."
  • next we use Promise.race, since not always transitionEnd will be triggerred (either prop is the same for the both states, or prop name will not match in rules and event (background may be background-color), and other) we need tp fallback to simple timeout
  • finally we wait for all timeout/eventListeners for each property

Disclaimer:

  • it's example I admit it use a lot of magic, and missed lot of checks
  • "callback" will be fired even in case if animation was cancelled (timeout will fire anyway)
  • requires ES6 :)

you can play around with it on jsfiddle

Upvotes: 2

sidonaldson
sidonaldson

Reputation: 25284

transitionEnd returns a property called propertyName in the event object. Source

Therefore you can test for the property you want and use simple logic to filter the callbacks:

document.querySelector('a').addEventListener('transitionend', function(event){
    if(event.propertyName !== 'width') return;
    console.log('transitionEnd - width!');
});

Upvotes: 15

user3297291
user3297291

Reputation: 23372

A bit of a hacky solution might be to try to find out which css property has the longest total duration. You can do so by using window.getComputedStyle on your <a> element and adding up all duration and delay properties.

You could do this in the regular event handler that is fired three times (it's pretty quick), or make a function that pre-computes the property name you're looking for.

Main problems with this approach:

  • Css allows you to use ms and s in one statement, you might need to do some more computing.
  • It can be kind of difficult to predict what the computed style is when you're changing the transition style on hover, or when adding new classes just before/after a transition.

var getValues = function(str) {
  return str
    .replace(/[A-Z]/gi, "")
    .split(", ")
    .map(parseFloat);
};

var getMaxTransitionProp = function(el) {
  var style = window.getComputedStyle(el);
  var props = style.transitionProperty.split(", ");

  var delays = getValues(style.transitionDelay);
  var durations = getValues(style.transitionDuration);
  var totals = durations.map(function(v, i) {
    return v + delays[i];
  });

  var maxIndex = totals.reduce(function(res, cur, i) {
    if (res.val > cur) {
      res.val = cur;
      res.i = i;
    }
    return res;
  }, {
    val: -Infinity,
    i: 0
  }).i;

  return props[maxIndex];
}

var lastEventListenerFor = function(el, cb) {
  var lastProp = getMaxTransitionProp(el);
  return function(e) {
    if (e.propertyName == lastProp) {
      cb(e);
    }
  };
}

var a = document.querySelector("a");
var cb = function(e) {
  console.log("End");
};

a.addEventListener("transitionend", lastEventListenerFor(a, cb));
a {
  display: block;
  opacity: .5;
  width: 100px;
  height: 50px;
  background: lightblue;
  transition: 3s width,
  /* <-- "transitionEnd" should fire after this */
  2s height, .5s background;
}
a:hover {
  width: 200px;
  height: 100px;
  background: red;
}
<a>Hover me</a>

Upvotes: 2

Related Questions