I need a special transitionend
-like event that fires once after all transitions are plete, or fires immediately if there are no transitions defined in the CSS.
This what I've e up so far:
(function($){
$.event.special.transitionsComplete = {
setup: function(data, namespaces, eventHandle){
var queue = [],
style = window.getComputedStyle(this, null),
putedProps = style.getPropertyValue('transition-property').split(', '),
putedDurations = style.getPropertyValue('transition-duration').split(', '),
$node = $(this);
// only count properties with duration higher than 0s
for(var i = 0; i < putedDurations.length; i++)
if(putedDurations[i] !== '0s')
queue.push(putedProps[i]);
// there are transitions
if(queue.length > 0){
$node.on('webkitTransitionEnd.x transitionend.x', function(e){
queue.splice(queue.indexOf(e.originalEvent.propertyName));
if(queue.length < 1)
$node.trigger('transitionsComplete');
});
// no transitions, fire (almost) immediately
}else{
setTimeout(function(){
$node.trigger('transitionsComplete');
}, 5);
}
},
teardown: function(namespaces){
$(this).off('.x');
}
};
})(jQuery);
I've made a live example here.
The only issue is that it only works if the element itself has transition properties, ignoring transitions from children elements. If I switch transitionsComplete
to transitionend
both the parent and child event handlers are run after the child transition finishes. Is there some way, or perhaps a better approach to determine if an element has transitions happening to it or its children? I'd like to avoid going through the children manually and checking their transitions properties, if possible. (That wouldn't be reliable anyway, because even if some children have transitions, it doesn't mean they would be active at that point)
I need a special transitionend
-like event that fires once after all transitions are plete, or fires immediately if there are no transitions defined in the CSS.
This what I've e up so far:
(function($){
$.event.special.transitionsComplete = {
setup: function(data, namespaces, eventHandle){
var queue = [],
style = window.getComputedStyle(this, null),
putedProps = style.getPropertyValue('transition-property').split(', '),
putedDurations = style.getPropertyValue('transition-duration').split(', '),
$node = $(this);
// only count properties with duration higher than 0s
for(var i = 0; i < putedDurations.length; i++)
if(putedDurations[i] !== '0s')
queue.push(putedProps[i]);
// there are transitions
if(queue.length > 0){
$node.on('webkitTransitionEnd.x transitionend.x', function(e){
queue.splice(queue.indexOf(e.originalEvent.propertyName));
if(queue.length < 1)
$node.trigger('transitionsComplete');
});
// no transitions, fire (almost) immediately
}else{
setTimeout(function(){
$node.trigger('transitionsComplete');
}, 5);
}
},
teardown: function(namespaces){
$(this).off('.x');
}
};
})(jQuery);
I've made a live example here.
The only issue is that it only works if the element itself has transition properties, ignoring transitions from children elements. If I switch transitionsComplete
to transitionend
both the parent and child event handlers are run after the child transition finishes. Is there some way, or perhaps a better approach to determine if an element has transitions happening to it or its children? I'd like to avoid going through the children manually and checking their transitions properties, if possible. (That wouldn't be reliable anyway, because even if some children have transitions, it doesn't mean they would be active at that point)
- Can you edit the CSS for the child elements that transition? – David Murdoch Commented Sep 15, 2015 at 13:35
- But the transition is already on the child in the example – nice ass Commented Sep 16, 2015 at 9:06
- If you are able to edit the CSS itself I think I can help you e up with a cleaner way. Let me know – David Murdoch Commented Sep 16, 2015 at 10:39
- From your question, I wonder if css transitions are best for what you have in mind. you will have better control and likely better performance with JavaScript transitions instead. look at greensock.js. – CodeToad Commented Sep 16, 2015 at 14:06
- @niceass, can you edit the CSS for the child elements that transition? – David Murdoch Commented Sep 17, 2015 at 15:54
4 Answers
Reset to default 4 +400I've used the treeWalker api to traverse the original node (root), and all child nodes (elements only), filter out elements without transitions, and collect transitions properties to queue
(fiddle). As you can see, I've solved the time difference between plete-div
and plete-p
, and they fire now (almost - couple of ms) at the same time.
There are two caveat, for which I have no workarounds:
- If there are transitions that are triggered by different means, for
example one is triggered by adding
.visible
to thediv
, and the other by adding.invisible
, they will all be added to thequeue
. The event will never fire, as thequeue
will never empty - I don't have any idea how to solve this. - If there are transitions of shortcut properties (
padding
for example), multipletransitionend
events may be fired, withtransition-property
, such aspadding-top
,padding-right
, etc... This will cause the array to empty very quickly assplice(-1, 1)
removes item from the end of an array. I had a workaround, but that may cause problems, as it might remove other properties in thequeue
. The best workaround is not to transition on shortcut properties.
The code for the treeWalker is based on Ban Nadel's - Finding HTML Comment Nodes In The DOM Using TreeWalker.
And the code at last:
(function ($) {
$.event.special.transitionsComplete = {
setup: function (data, namespaces, eventHandle) {
var TRANSITION_PROPERTY = 'transition-property';
var TRANSITION_DURATION = 'transition-duration';
var root = this;
var queue = [];
var $node = $(this);
function filter(node) { // filter for treeWalker
/*** filters transitions which are a string with one '0s'. If more then '0s' is defined it will be catched when creating the queue ***/
var putedDuration = window.getComputedStyle(node, null)
.getPropertyValue(TRANSITION_DURATION);
return putedDuration === '0s' ? NodeFilter.FILTER_SKIP : NodeFilter.FILTER_ACCEPT;
}
filter.acceptNode = filter; // for webkit and firefox
/** create the treeWalker to traverse only elements **/
var treeWalker = document.createTreeWalker(root, NodeFilter.SHOW_ELEMENT, filter, false);
/** traverse all elements using treeWalker.nextNode(). First node is the root **/
do {
var style = window.getComputedStyle(treeWalker.currentNode, null);
var putedProps = style.getPropertyValue(TRANSITION_PROPERTY).split(', ');
var putedDurations = style.getPropertyValue(TRANSITION_DURATION).split(', ');
/** push all props with duration which is not 0s **/
putedDurations.forEach(function (duration, index) {
duration !== '0s' && queue.push(putedProps[index]);
});
} while (treeWalker.nextNode()); // iterate until no next node
// no transitions, fire (almost) immediately
if (queue.length === 0) {
setTimeout(function () {
$node.trigger('transitionsComplete');
}, 5);
return; // return out of the function to skip the transitions block
}
// there are transitions
$node.on('webkitTransitionEnd.x transitionend.x', function (e) {
var propertyName = e.originalEvent.propertyName;
var indexOfProp = queue.indexOf(propertyName);
queue.splice(indexOfProp, 1);
if (queue.length < 1) {
console.log('Transitions Complete');
$node.trigger('transitionsComplete');
}
});
},
teardown: function (namespaces) {
$(this).off('.x');
}
};
})(jQuery);
So here you go, indeed i inspect the children: http://jsfiddle/cegejk59/2/
(function($){
$.event.special.transitionsComplete = {
setup: function( data, namespaces, eventHandle ) {
var allTransitions = [];
w = window,
TRANSITION_PROPERTY_KEY = 'transition-property',
TRANSITION_DURATION_KEY = 'transition-duration',
$node = $( this );
function collectTransitionsRecursively( node ) {
var style = w.getComputedStyle( node ),
nodeComputedProperties = style.getPropertyValue( TRANSITION_PROPERTY_KEY ).split( ', ' ),
nodeComputedDurations = style.getPropertyValue( TRANSITION_DURATION_KEY ).split( ', ' );
for( var i = 0; i < nodeComputedDurations.length; i++ )
if( nodeComputedDurations[ i ] !== '0s' )
allTransitions.push( nodeComputedProperties[ i ] );
for( var childIndex = 0; childIndex < node.children.length; childIndex++ )
collectTransitionsRecursively( node.children[ childIndex ] );
}
function triggerTransitionsComplete( $onNode ) {
console.log( "No transitions (left)." );
$onNode.trigger('transitionsComplete');
}
function onNoTransitionsFound() {
setTimeout( function() {
triggerTransitionsComplete( $node );
});
}
collectTransitionsRecursively( this );
if( allTransitions.length == 0 )
return onNoTransitionsFound();
else
console.log( 'remaining', allTransitions );
$node.on('webkitTransitionEnd.x transitionend.x', function( e ){
allTransitions.splice(allTransitions.indexOf(e.originalEvent.propertyName));
if( allTransitions.length == 0 )
triggerTransitionsComplete( $node );
else
console.log('remaining', allTransitions);
});
},
teardown: function( namespaces ) {
$( this ).off( '.x' );
}
};
})(jQuery);
var div = $('div'), p = $('p'), start = new Date().getTime();
console.log('-- start --');
div.addClass('visible');
div.one('transitionsComplete', function(e){
console.log('plete-div', (new Date().getTime() - start) / 1000);
});
//p.one('transitionsComplete', function(e){
// console.log('plete-p', (new Date().getTime() - start) / 1000);
//});
if($('div').one('webkitTransitionEnd otransitionend oTransitionEnd msTransitionEnd transitionend')) {
$('div').one('webkitTransitionEnd otransitionend oTransitionEnd msTransitionEnd transitionend', function(e) {
console.log("Fire after transitions");
});
} else {
console.log("Fire immediately if there are no transitions");
}
I'm sure someone will explain why an implementation like this won't work, but maybe it will provide some inspiration / discussion.
https://jsfiddle/nf8gvbuo/16/
$(function() {
var div = $("div"),
p = $("p"),
start = new Date().getTime();
console.log("-- start --");
div.addClass("visible");
var n = 0
, transitions = [];
div.on({
"transitionend": function(e) {
++n;
transitions.push({
"element": e.originalEvent.srcElement,
"property": e.originalEvent.propertyName,
"duration": e.originalEvent.elapsedTime
});
var container = $(this).css("transition").split(","),
elems = $(p, this).css("transition").split(",");
if (container.length === 1 && n === elems.length) {
$(this).trigger("transitionComplete", [transitions])
}
},
"transitionComplete": function(e, tx) {
console.log(e.type, this, (new Date().getTime() - start) / 1000, tx);
alert(e.type);
}
});
});
p {
opacity: 0;
transition: opacity 10s, transform 5s;
background: red;
width: 50px;
height: 50px;
margin: 100px;
}
div.visible p {
opacity: 1;
transform: scale(1.5);
}
<script src="https://ajax.googleapis./ajax/libs/jquery/1.11.1/jquery.min.js"></script>
<div>
<p></p>
</div>
jsfiddle http://jsfiddle/nf8gvbuo/1/