From 95f6f53f4d3d0775ca4fffd02e1871d95c7160d7 Mon Sep 17 00:00:00 2001 From: Caitlin Potter Date: Wed, 11 Sep 2013 14:52:07 -0400 Subject: [PATCH 1/4] feat(transition): emulateTransitionEnd implementation This is used by TWBS to provide support for browsers which do not have transition support, despite theoretically providing it. --- src/transition/test/transition.spec.js | 17 +++++++++++++++++ src/transition/transition.js | 25 +++++++++++++++++++++++++ 2 files changed, 42 insertions(+) diff --git a/src/transition/test/transition.spec.js b/src/transition/test/transition.spec.js index 34f744b037..9dfff8797d 100644 --- a/src/transition/test/transition.spec.js +++ b/src/transition/test/transition.spec.js @@ -54,6 +54,23 @@ describe('$transition', function() { expect(triggerFunction).toHaveBeenCalledWith(element); }); + // transitionend emulation + describe('emulateTransitionEnd', function() { + it('should emit transition end-event after the specified duration', function() { + var element = angular.element('
'); + var transitionEndHandler = jasmine.createSpy('transitionEndHandler'); + + // There is no transition property, so transitionend could not be fired + // on its own. + var promise = $transition(element, {height: '100px'}); + promise.then(transitionEndHandler); + promise.emulateTransitionEnd(1); + + $timeout.flush(); + expect(transitionEndHandler).toHaveBeenCalledWith(element); + }); + }); + // Versions of Internet Explorer before version 10 do not have CSS transitions if ( !ie || ie > 9 ) { describe('transitionEnd event', function() { diff --git a/src/transition/transition.js b/src/transition/transition.js index c23d3f76f7..4636ee1832 100644 --- a/src/transition/transition.js +++ b/src/transition/transition.js @@ -52,6 +52,31 @@ angular.module('ui.bootstrap.transition', []) deferred.reject('Transition cancelled'); }; + // Emulate transitionend event, useful when support is assumed to be + // available, but may not actually be used due to a transition property + // not being used in CSS (for example, in versions of firefox prior to 16, + // only -moz-transition is supported -- and is not used in Bootstrap3's CSS + // -- As such, no transitionend event would be fired due to no transition + // ever taking place. This method allows a fallback for such browsers.) + deferred.promise.emulateTransitionEnd = function(duration) { + var called = false; + deferred.promise.then( + function() { called = true; }, + function() { called = true; } + ); + + var callback = function() { + if ( !called ) { + // If we got here, we probably aren't going to get a real + // transitionend event. Emit a dummy to the handler. + element.triggerHandler(endEventName); + } + }; + + $timeout(callback, duration); + return deferred.promise; + }; + return deferred.promise; }; From 62ce87fb6e5f966ff8a43f54488f1c041939caeb Mon Sep 17 00:00:00 2001 From: Caitlin Potter Date: Fri, 6 Sep 2013 17:15:31 -0400 Subject: [PATCH 2/4] feat(collapse): Remove transition-canceling for TWBS3.0 compat Nice looking transition animations with the following browsers, on OSX 10.8.4: Chrome29.0.1547.65 FireFox19.02 FireFox23.01 FireFox26.0a1 (2013-09-04) No 'bogus state' issues with unfinished animations (that I've seen, as of yet). Unfortunately, there are some failing tests (on each browser), because some functionality had to be dropped to work well. These tests have not been rectified yet. There are some helpers for providing more robust information about the dimensions of the collapsing object, which I've included in a 'helpers' object. These should probably be either discarded, or else moved to a more central location where they can be shared by other components. Just a thought. (The implementations of these functions has been adapted from Prototype, and may not be quite as robust as jQuery). --- src/collapse/collapse.js | 207 ++++++++++++++++++++++++++++++++++----- 1 file changed, 182 insertions(+), 25 deletions(-) diff --git a/src/collapse/collapse.js b/src/collapse/collapse.js index d0a33b454f..3b837a679e 100644 --- a/src/collapse/collapse.js +++ b/src/collapse/collapse.js @@ -9,12 +9,15 @@ angular.module('ui.bootstrap.collapse',['ui.bootstrap.transition']) // The fix is to remove the "collapse" CSS class while changing the height back to auto - phew! var fixUpHeight = function(scope, element, height) { // We remove the collapse CSS class to prevent a transition when we change to height: auto + var collapse = element.hasClass('collapse'); element.removeClass('collapse'); element.css({ height: height }); // It appears that reading offsetWidth makes the browser realise that we have changed the // height already :-/ var x = element[0].offsetWidth; - element.addClass('collapse'); + if(collapse) { + element.addClass('collapse'); + } }; return { @@ -46,48 +49,202 @@ angular.module('ui.bootstrap.collapse',['ui.bootstrap.transition']) expand(); } }); + + // Some jQuery-like functionality, based on implementation in Prototype. + // + // There is a problem with these: We're instantiating them for every + // instance of the directive, and that's not very good. + // + // But we do need a more robust way to calculate dimensions of an item, + // scrollWidth/scrollHeight is not super reliable, and we can't rely on + // jQuery or Prototype or any other framework being used. + var helpers = { + style: function(element, prop) { + var elem = element; + if(typeof elem.length === 'number') { + elem = elem[0]; + } + function camelcase(name) { + return name.replace(/-+(.)?/g, function(match, chr) { + return chr ? chr.toUpperCase() : ''; + }); + } + prop = prop === 'float' ? 'cssFloat' : camelcase(prop); + var value = elem.style[prop]; + if (!value || value === 'auto') { + var css = window.getComputedStyle(elem, null); + value = css ? css[prop] : null; + } + if (prop === 'opacity') { + return value ? parseFloat(value) : 1.0; + } + return value === 'auto' ? null : value; + }, + + size: function(element) { + var dom = element[0]; + var display = helpers.style(element, 'display'); + + if (display && display !== 'none') { + // Fast case: rely on offset dimensions + return { width: dom.offsetWidth, height: dom.offsetHeight }; + } + + // Slow case -- Save original CSS properties, update the CSS, and then + // use offset dimensions, and restore the original CSS + var currentStyle = dom.style; + var originalStyles = { + visibility: currentStyle.visibility, + position: currentStyle.position, + display: currentStyle.display + }; + + var newStyles = { + visibility: 'hidden', + display: 'block' + }; + + // Switching `fixed` to `absolute` causes issues in Safari. + if (originalStyles.position !== 'fixed') { + newStyles.position = 'absolute'; + } + + // Quickly swap-in styles which would allow us to utilize offset + // dimensions + element.css(newStyles); + + var dimensions = { + width: dom.offsetWidth, + height: dom.offsetHeight + }; + + // And restore the original styles + element.css(originalStyles); + + return dimensions; + }, + + width: function(element, value) { + if(typeof value === 'number' || typeof value === 'string') { + if(typeof value === 'number') { + value = value + 'px'; + } + element.css({ 'width': value }); + return; + } + return helpers.size(element).width; + }, + height: function(element, value) { + if(typeof value === 'number' || typeof value === 'string') { + if(typeof value === 'number') { + value = value + 'px'; + } + element.css({ 'height': value }); + return; + } + return helpers.size(element).height; + }, + + dimension: function() { + var hasWidth = element.hasClass('width'); + return hasWidth ? 'width' : 'height'; + } + }; + + var events = { + beforeShow: function(dimension, dimensions) { + element + .removeClass('collapse') + .removeClass('collapsed') + .addClass('collapsing'); + helpers[dimension](element, 0); + }, + + beforeHide: function(dimension, dimensions) { + // Read offsetHeight and reset height: + helpers[dimension](element, dimensions[dimension] + "px"); + var unused = element[0].offsetWidth, + unused2 = element[0].offsetHeight; + element + .addClass('collapsing') + .removeClass('collapse') + .removeClass('in'); + }, + + afterShow: function(dimension) { + element + .removeClass('collapsing') + .addClass('in'); + helpers[dimension](element, 'auto'); + isCollapsed = false; + }, + + afterHide: function(dimension) { + element + .removeClass('collapsing') + .addClass('collapsed') + .addClass('collapse'); + isCollapsed = true; + } + }; var currentTransition; - var doTransition = function(change) { - if ( currentTransition ) { - currentTransition.cancel(); + var doTransition = function(showing, pixels) { + if (currentTransition || showing === element.hasClass('in')) { + return; } - currentTransition = $transition(element,change); + + var dimension = helpers.dimension(); + var dimensions = helpers.size(element); + var name = showing ? 'Show' : 'Hide'; + + events['before' + name](dimension, dimensions); + + var query = {}; + var makeUpper = function(name) { + return name.charAt(0).toUpperCase() + name.slice(1); + }; + if(pixels==='scroll') { + pixels = element[0][pixels + makeUpper(dimension)]; + } + if(typeof pixels === 'number') { + pixels = pixels + "px"; + } + query[dimension] = pixels; + currentTransition = $transition(element,query); currentTransition.then( - function() { currentTransition = undefined; }, - function() { currentTransition = undefined; } + function() { + events['after' + name](dimension); + currentTransition = undefined; + }, + function(reason) { + var descr = showing ? 'expansion' : 'collapse'; + currentTransition = undefined; + } ); return currentTransition; }; var expand = function() { - if (initialAnimSkip) { + if (initialAnimSkip || !$transition.transitionEndEventName) { initialAnimSkip = false; - if ( !isCollapsed ) { - fixUpHeight(scope, element, 'auto'); - } + var dimension = helpers.dimension(); + helpers[dimension](element, 'auto'); + events.afterShow(dimension); } else { - doTransition({ height : element[0].scrollHeight + 'px' }) - .then(function() { - // This check ensures that we don't accidentally update the height if the user has closed - // the group while the animation was still running - if ( !isCollapsed ) { - fixUpHeight(scope, element, 'auto'); - } - }); + doTransition(true, 'scroll'); } - isCollapsed = false; }; var collapse = function() { - isCollapsed = true; - if (initialAnimSkip) { + if (initialAnimSkip || !$transition.transitionEndEventName) { initialAnimSkip = false; - fixUpHeight(scope, element, 0); + var dimension = helpers.dimension(); + helpers[dimension](element, 0); + events.afterHide(dimension); } else { - fixUpHeight(scope, element, element[0].scrollHeight + 'px'); - doTransition({'height':'0'}); + doTransition(false, '0'); } }; } From 281c00cb41d95955830947a2a41bd4a8d7bf2692 Mon Sep 17 00:00:00 2001 From: Caitlin Potter Date: Thu, 12 Sep 2013 22:37:28 -0400 Subject: [PATCH 3/4] feat(collapse): rebased ontop of #991 --- src/collapse/collapse.js | 2 +- src/collapse/test/collapse.spec.js | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/collapse/collapse.js b/src/collapse/collapse.js index 3b837a679e..3c0574c231 100644 --- a/src/collapse/collapse.js +++ b/src/collapse/collapse.js @@ -212,7 +212,7 @@ angular.module('ui.bootstrap.collapse',['ui.bootstrap.transition']) pixels = pixels + "px"; } query[dimension] = pixels; - currentTransition = $transition(element,query); + currentTransition = $transition(element,query).emulateTransitionEnd(350); currentTransition.then( function() { events['after' + name](dimension); diff --git a/src/collapse/test/collapse.spec.js b/src/collapse/test/collapse.spec.js index d04a8b3c90..18298be62d 100644 --- a/src/collapse/test/collapse.spec.js +++ b/src/collapse/test/collapse.spec.js @@ -49,6 +49,7 @@ describe('collapse directive', function () { scope.$digest(); scope.isCollapsed = true; scope.$digest(); + $timeout.flush(); scope.isCollapsed = false; scope.$digest(); $timeout.flush(); @@ -92,6 +93,7 @@ describe('collapse directive', function () { scope.$digest(); scope.isCollapsed = true; scope.$digest(); + $timeout.flush(); scope.isCollapsed = false; scope.$digest(); $timeout.flush(); From 9224582b593482a6012f776f90e90402a5f33a6f Mon Sep 17 00:00:00 2001 From: Caitlin Potter Date: Fri, 13 Sep 2013 12:31:52 -0400 Subject: [PATCH 4/4] feat(collapse): Fix collapse on browsers without transition support Correct classes now added/removed in the no-transition path. --- src/collapse/collapse.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/collapse/collapse.js b/src/collapse/collapse.js index 3c0574c231..a0e7434814 100644 --- a/src/collapse/collapse.js +++ b/src/collapse/collapse.js @@ -231,6 +231,9 @@ angular.module('ui.bootstrap.collapse',['ui.bootstrap.transition']) initialAnimSkip = false; var dimension = helpers.dimension(); helpers[dimension](element, 'auto'); + element + .removeClass('collapse') + .removeClass('collapsed'); events.afterShow(dimension); } else { doTransition(true, 'scroll'); @@ -242,6 +245,7 @@ angular.module('ui.bootstrap.collapse',['ui.bootstrap.transition']) initialAnimSkip = false; var dimension = helpers.dimension(); helpers[dimension](element, 0); + element.removeClass('in'); events.afterHide(dimension); } else { doTransition(false, '0');