Skip to content
This repository was archived by the owner on May 29, 2019. It is now read-only.

feat(collapse): Bootstrap3 compatibility/bugfixes #1001

Closed
wants to merge 4 commits into from
Closed
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
211 changes: 186 additions & 25 deletions src/collapse/collapse.js
Original file line number Diff line number Diff line change
@@ -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,206 @@ 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).emulateTransitionEnd(350);
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');
element
.removeClass('collapse')
.removeClass('collapsed');
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);
element.removeClass('in');
events.afterHide(dimension);
} else {
fixUpHeight(scope, element, element[0].scrollHeight + 'px');
doTransition({'height':'0'});
doTransition(false, '0');
}
};
}
2 changes: 2 additions & 0 deletions src/collapse/test/collapse.spec.js
Original file line number Diff line number Diff line change
@@ -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();
17 changes: 17 additions & 0 deletions src/transition/test/transition.spec.js
Original file line number Diff line number Diff line change
@@ -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('<div></div>');
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() {
25 changes: 25 additions & 0 deletions src/transition/transition.js
Original file line number Diff line number Diff line change
@@ -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;
};