diff --git a/src/progressbar/docs/demo.html b/src/progressbar/docs/demo.html
new file mode 100644
index 0000000000..8fbfb082f8
--- /dev/null
+++ b/src/progressbar/docs/demo.html
@@ -0,0 +1,27 @@
+
+
Static
+
+
+
Dynamic
+
Value: {{dynamic}}
+
+
+
No animation
+
+
+
Object (changes type based on value)
+
+
+
Stacked
+
Array values with automatic types
+
Value: {{stackedArray}}
+
+
+
Objects
+
Value: {{stacked}}
+
+
\ No newline at end of file
diff --git a/src/progressbar/docs/demo.js b/src/progressbar/docs/demo.js
new file mode 100644
index 0000000000..57676e8af6
--- /dev/null
+++ b/src/progressbar/docs/demo.js
@@ -0,0 +1,44 @@
+var ProgressDemoCtrl = function ($scope) {
+
+ $scope.random = function() {
+ var value = Math.floor((Math.random()*100)+1);
+ var type;
+
+ if (value < 25) {
+ type = 'success';
+ } else if (value < 50) {
+ type = 'info';
+ } else if (value < 75) {
+ type = 'warning';
+ } else {
+ type = 'danger';
+ }
+
+ $scope.dynamic = value;
+ $scope.dynamicObject = {
+ value: value,
+ type: type
+ };
+ };
+ $scope.random();
+
+ var types = ['success', 'info', 'warning', 'danger'];
+ $scope.randomStacked = function() {
+ $scope.stackedArray = [];
+ $scope.stacked = [];
+
+ var n = Math.floor((Math.random()*4)+1);
+
+ for (var i=0; i < n; i++) {
+ var value = Math.floor((Math.random()*30)+1);
+ $scope.stackedArray.push(value);
+
+ var index = Math.floor((Math.random()*4));
+ $scope.stacked.push({
+ value: value,
+ type: types[index]
+ });
+ }
+ };
+ $scope.randomStacked();
+};
diff --git a/src/progressbar/docs/readme.md b/src/progressbar/docs/readme.md
new file mode 100644
index 0000000000..ebc9cb9580
--- /dev/null
+++ b/src/progressbar/docs/readme.md
@@ -0,0 +1,3 @@
+A lightweight progress bar directive that is focused on providing progress visualization!
+
+The progress bar directive supports multiple (stacked) bars into the same element, optional transition animation, event handler for full & empty state and many more.
diff --git a/src/progressbar/progressbar.js b/src/progressbar/progressbar.js
new file mode 100644
index 0000000000..ced3b88833
--- /dev/null
+++ b/src/progressbar/progressbar.js
@@ -0,0 +1,106 @@
+angular.module('ui.bootstrap.progressbar', ['ui.bootstrap.transition'])
+
+.constant('progressConfig', {
+ animate: true,
+ autoType: false,
+ stackedTypes: ['success', 'info', 'warning', 'danger']
+})
+
+.controller('ProgressBarController', ['$scope', '$attrs', 'progressConfig', function($scope, $attrs, progressConfig) {
+
+ // Whether bar transitions should be animated
+ var animate = angular.isDefined($attrs.animate) ? $scope.$eval($attrs.animate) : progressConfig.animate;
+ var autoType = angular.isDefined($attrs.autoType) ? $scope.$eval($attrs.autoType) : progressConfig.autoType;
+ var stackedTypes = angular.isDefined($attrs.stackedTypes) ? $scope.$eval('[' + $attrs.stackedTypes + ']') : progressConfig.stackedTypes;
+
+ // Create bar object
+ this.makeBar = function(newBar, oldBar, index) {
+ var newValue = (angular.isObject(newBar)) ? newBar.value : (newBar || 0);
+ var oldValue = (angular.isObject(oldBar)) ? oldBar.value : (oldBar || 0);
+ var type = (angular.isObject(newBar) && angular.isDefined(newBar.type)) ? newBar.type : (autoType) ? getStackedType(index || 0) : null;
+
+ return {
+ from: oldValue,
+ to: newValue,
+ type: type,
+ animate: animate
+ };
+ };
+
+ function getStackedType(index) {
+ return stackedTypes[index];
+ }
+
+ this.addBar = function(bar) {
+ $scope.bars.push(bar);
+ $scope.totalPercent += bar.to;
+ };
+
+ this.clearBars = function() {
+ $scope.bars = [];
+ $scope.totalPercent = 0;
+ };
+ this.clearBars();
+}])
+
+.directive('progress', function() {
+ return {
+ restrict: 'EA',
+ replace: true,
+ controller: 'ProgressBarController',
+ scope: {
+ value: '=',
+ onFull: '&',
+ onEmpty: '&'
+ },
+ templateUrl: 'template/progressbar/progress.html',
+ link: function(scope, element, attrs, controller) {
+ scope.$watch('value', function(newValue, oldValue) {
+ controller.clearBars();
+
+ if (angular.isArray(newValue)) {
+ // Stacked progress bar
+ for (var i=0, n=newValue.length; i < n; i++) {
+ controller.addBar(controller.makeBar(newValue[i], oldValue[i], i));
+ }
+ } else {
+ // Simple bar
+ controller.addBar(controller.makeBar(newValue, oldValue));
+ }
+ }, true);
+
+ // Total percent listeners
+ scope.$watch('totalPercent', function(value) {
+ if (value >= 100) {
+ scope.onFull();
+ } else if (value <= 0) {
+ scope.onEmpty();
+ }
+ }, true);
+ }
+ };
+})
+
+.directive('progressbar', ['$transition', function($transition) {
+ return {
+ restrict: 'EA',
+ replace: true,
+ scope: {
+ width: '=',
+ old: '=',
+ type: '=',
+ animate: '='
+ },
+ templateUrl: 'template/progressbar/bar.html',
+ link: function(scope, element) {
+ scope.$watch('width', function(value) {
+ if (scope.animate) {
+ element.css('width', scope.old + '%');
+ $transition(element, {width: value + '%'});
+ } else {
+ element.css('width', value + '%');
+ }
+ });
+ }
+ };
+}]);
\ No newline at end of file
diff --git a/src/progressbar/test/progressbar.spec.js b/src/progressbar/test/progressbar.spec.js
new file mode 100644
index 0000000000..24889e26e7
--- /dev/null
+++ b/src/progressbar/test/progressbar.spec.js
@@ -0,0 +1,326 @@
+describe('progressbar directive with no binding', function () {
+ var $rootScope, element;
+ beforeEach(module('ui.bootstrap.progressbar'));
+ beforeEach(module('template/progressbar/progress.html', 'template/progressbar/bar.html'));
+ beforeEach(inject(function(_$compile_, _$rootScope_) {
+ $compile = _$compile_;
+ $rootScope = _$rootScope_;
+ element = $compile('')($rootScope);
+ $rootScope.$digest();
+ }));
+
+ it('has a "progress" css class', function() {
+ expect(element.hasClass('progress')).toBe(true);
+ });
+
+ it('contains one child element with "bar" css class', function() {
+ expect(element.children().length).toBe(1);
+ expect(element.children().eq(0).hasClass('bar')).toBe(true);
+ });
+
+ it('has a "bar" element with expected width', function() {
+ expect(element.children().eq(0).css('width')).toBe('22%');
+ });
+});
+
+describe('progressbar directive with data-binding', function () {
+ var $rootScope, element;
+ beforeEach(module('ui.bootstrap.progressbar'));
+ beforeEach(module('template/progressbar/progress.html', 'template/progressbar/bar.html'));
+ beforeEach(inject(function(_$compile_, _$rootScope_) {
+ $compile = _$compile_;
+ $rootScope = _$rootScope_;
+ $rootScope.percent = 33;
+ element = $compile('')($rootScope);
+ $rootScope.$digest();
+ }));
+
+ it('has a "progress" css class', function() {
+ expect(element.hasClass('progress')).toBe(true);
+ });
+
+ it('contains one child element with "bar" css class', function() {
+ expect(element.children().length).toBe(1);
+ expect(element.children().eq(0).hasClass('bar')).toBe(true);
+ });
+
+ it('has a "bar" element with expected width', function() {
+ expect(element.children().eq(0).css('width')).toBe('33%');
+ });
+
+ it('changes width when bind value changes', function() {
+ $rootScope.percent = 55;
+ $rootScope.$digest();
+ expect(element.children().length).toBe(1);
+ expect(element.children().eq(0).css('width')).toBe('55%');
+ expect(element.children().eq(0).hasClass('bar')).toBe(true);
+
+ $rootScope.percent += 11;
+ $rootScope.$digest();
+ expect(element.children().eq(0).css('width')).toBe('66%');
+
+ $rootScope.percent = 0;
+ $rootScope.$digest();
+ expect(element.children().eq(0).css('width')).toBe('0%');
+ });
+
+ it('can handle correctly objects value && class', function() {
+ $rootScope.percent = {
+ value: 45,
+ type: 'warning'
+ };
+ $rootScope.$digest();
+
+ expect(element.children().length).toBe(1);
+ expect(element.hasClass('progress')).toBe(true);
+
+ var barElement = element.children().eq(0);
+ expect(barElement.css('width')).toBe('45%');
+ expect(barElement.hasClass('bar')).toBe(true);
+ expect(barElement.hasClass('bar-warning')).toBe(true);
+ });
+
+});
+
+describe('stacked progressbar directive', function () {
+ var $rootScope, element;
+ beforeEach(module('ui.bootstrap.progressbar'));
+ beforeEach(module('template/progressbar/progress.html', 'template/progressbar/bar.html'));
+ beforeEach(inject(function(_$compile_, _$rootScope_) {
+ $compile = _$compile_;
+ $rootScope = _$rootScope_;
+ $rootScope.stacked = [12, 22, 33];
+ element = $compile('')($rootScope);
+ $rootScope.$digest();
+ }));
+
+ it('has a "progress" css class', function() {
+ expect(element.hasClass('progress')).toBe(true);
+ });
+
+ it('contains tree child elements with "bar" css class each', function() {
+ expect(element.children().length).toBe(3);
+ expect(element.children().eq(0).hasClass('bar')).toBe(true);
+ expect(element.children().eq(1).hasClass('bar')).toBe(true);
+ expect(element.children().eq(2).hasClass('bar')).toBe(true);
+ });
+
+ it('has a elements with expected width', function() {
+ expect(element.children().eq(0).css('width')).toBe('12%');
+ expect(element.children().eq(1).css('width')).toBe('22%');
+ expect(element.children().eq(2).css('width')).toBe('33%');
+ });
+
+ it('changes width when bind value changes', function() {
+ $rootScope.stacked[1] = 35;
+ $rootScope.$digest();
+
+ expect(element.children().length).toBe(3);
+ expect(element.children().eq(0).css('width')).toBe('12%');
+ expect(element.children().eq(1).css('width')).toBe('35%');
+ expect(element.children().eq(2).css('width')).toBe('33%');
+ });
+
+ it('can remove bars', function() {
+ $rootScope.stacked.pop();
+ $rootScope.$digest();
+
+ expect(element.children().length).toBe(2);
+
+ expect(element.children().eq(0).css('width')).toBe('12%');
+ expect(element.children().eq(1).css('width')).toBe('22%');
+ });
+
+ it('can handle correctly object changes', function() {
+ $rootScope.stacked[1] = {
+ value: 29,
+ type: 'danger'
+ };
+ $rootScope.$digest();
+
+ expect(element.children().length).toBe(3);
+
+ var barElement;
+
+ barElement = element.children().eq(0);
+ expect(barElement.css('width')).toBe('12%');
+ expect(barElement.hasClass('bar')).toBe(true);
+ expect(barElement.hasClass('bar-danger')).toBe(false);
+
+ barElement = element.children().eq(1);
+ expect(barElement.css('width')).toBe('29%');
+ expect(barElement.hasClass('bar')).toBe(true);
+ expect(barElement.hasClass('bar-danger')).toBe(true);
+
+ barElement = element.children().eq(2);
+ expect(barElement.css('width')).toBe('33%');
+ expect(barElement.hasClass('bar')).toBe(true);
+ expect(barElement.hasClass('bar-danger')).toBe(false);
+ });
+
+ it('can handle mixed objects with custom classes', function() {
+ $rootScope.stacked = [
+ { value: 15, type: 'info' },
+ 11,
+ { value: 9, type: 'danger' },
+ { value: 22, type: 'warning' },
+ 5
+ ];
+ $rootScope.$digest();
+
+ expect(element.children().length).toBe(5);
+
+ var barElement;
+
+ barElement = element.children().eq(0);
+ expect(barElement.css('width')).toBe('15%');
+ expect(barElement.hasClass('bar-info')).toBe(true);
+
+ barElement = element.children().eq(1);
+ expect(barElement.css('width')).toBe('11%');
+ expect(barElement.hasClass('bar-info')).toBe(false);
+
+ barElement = element.children().eq(2);
+ expect(barElement.css('width')).toBe('9%');
+ expect(barElement.hasClass('bar-danger')).toBe(true);
+
+ barElement = element.children().eq(3);
+ expect(barElement.css('width')).toBe('22%');
+ expect(barElement.hasClass('bar-warning')).toBe(true);
+
+ barElement = element.children().eq(4);
+ expect(barElement.css('width')).toBe('5%');
+ expect(barElement.hasClass('bar-warning')).toBe(false);
+ });
+
+});
+
+describe('stacked progressbar directive handlers', function () {
+ var $rootScope, element;
+ beforeEach(module('ui.bootstrap.progressbar'));
+ beforeEach(module('template/progressbar/progress.html', 'template/progressbar/bar.html'));
+ beforeEach(inject(function(_$compile_, _$rootScope_) {
+ $compile = _$compile_;
+ $rootScope = _$rootScope_;
+ $rootScope.stacked = [20, 30, 40]; // total: 90
+ $rootScope.fullHandler = jasmine.createSpy('fullHandler');
+ $rootScope.emptyHandler = jasmine.createSpy('emptyHandler');
+ element = $compile('')($rootScope);
+ $rootScope.$digest();
+ }));
+
+
+ it("should not fire at start", function () {
+ expect($rootScope.fullHandler).not.toHaveBeenCalled();
+ expect($rootScope.emptyHandler).not.toHaveBeenCalled();
+ });
+
+ it("should fire callback when full", function () {
+ $rootScope.stacked.push(10); // total: 100
+ $rootScope.$digest();
+
+ expect($rootScope.fullHandler).toHaveBeenCalled();
+ expect($rootScope.emptyHandler).not.toHaveBeenCalled();
+ });
+
+ it("should fire callback when empties", function () {
+ $rootScope.stacked = 0;
+ $rootScope.$digest();
+
+ expect($rootScope.fullHandler).not.toHaveBeenCalled();
+ expect($rootScope.emptyHandler).toHaveBeenCalled();
+ });
+
+});
+
+describe('stacked progressbar directive with auto-types', function () {
+ var $rootScope, element;
+ var config = {};
+ beforeEach(module('ui.bootstrap.progressbar'));
+ beforeEach(module('template/progressbar/progress.html', 'template/progressbar/bar.html'));
+ beforeEach(inject(function(_$compile_, _$rootScope_, progressConfig) {
+ $compile = _$compile_;
+ $rootScope = _$rootScope_;
+ $rootScope.stacked = [12, 22, {value: 33}, {value: 5}, 11];
+ element = $compile('')($rootScope);
+ $rootScope.$digest();
+
+ angular.extend(config, progressConfig);
+ }));
+ afterEach(inject(function(progressConfig) {
+ // return it to the original state
+ angular.extend(progressConfig, config);
+ }));
+
+ it('has a "progress" css class', function() {
+ expect(element.hasClass('progress')).toBe(true);
+ });
+
+ it('contains tree child elements with "bar" css class each', function() {
+ expect(element.children().length).toBe(5);
+ for (var i = 0; i < 5; i++) {
+ expect(element.children().eq(i).hasClass('bar')).toBe(true);
+ }
+ });
+
+ it('has elements with expected width', function() {
+ expect(element.children().eq(0).css('width')).toBe('12%');
+ expect(element.children().eq(1).css('width')).toBe('22%');
+ expect(element.children().eq(2).css('width')).toBe('33%');
+ expect(element.children().eq(3).css('width')).toBe('5%');
+ expect(element.children().eq(4).css('width')).toBe('11%');
+ });
+
+ it('has elements with automatic types', function() {
+ var stackedTypes = config.stackedTypes;
+
+ for (var i = 0; i < stackedTypes.length; i++) {
+ expect(element.children().eq(i).hasClass('bar-' + stackedTypes[i])).toBe(true);
+ }
+ });
+
+ it('ignore automatic type if one is specified', function() {
+ $rootScope.stacked[1] = {
+ value: 18,
+ type: 'something'
+ };
+ $rootScope.$digest();
+
+ var stackedTypes = config.stackedTypes;
+
+ var bar = element.children().eq(1);
+ expect(bar.css('width')).toBe('18%');
+ expect(bar.hasClass('bar-' + stackedTypes[1])).toBe(false);
+ expect(bar.hasClass('bar-something')).toBe(true);
+ });
+
+
+ it('can provide automatic classes to be applied', function() {
+ $rootScope.stacked[1] = {
+ value: 18,
+ type: 'something'
+ };
+ $rootScope.$digest();
+
+ var stackedTypes = config.stackedTypes;
+
+ var bar = element.children().eq(1);
+ expect(bar.css('width')).toBe('18%');
+ expect(bar.hasClass('bar-' + stackedTypes[1])).toBe(false);
+ expect(bar.hasClass('bar-something')).toBe(true);
+ });
+
+ it('can bypass default configuration for stacked classes from attribute', function() {
+ element = $compile('')($rootScope);
+ $rootScope.$digest();
+
+ var stackedTypes = config.stackedTypes;
+
+ expect(element.children().eq(0).hasClass('bar-danger')).toBe(true);
+ expect(element.children().eq(0).hasClass('bar-' + stackedTypes[0])).toBe(false);
+
+ expect(element.children().eq(1).hasClass('bar-warning')).toBe(true);
+ expect(element.children().eq(2).hasClass('bar-success')).toBe(true);
+ });
+
+});
\ No newline at end of file
diff --git a/template/progressbar/bar.html b/template/progressbar/bar.html
new file mode 100644
index 0000000000..09a5a6b010
--- /dev/null
+++ b/template/progressbar/bar.html
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/template/progressbar/progress.html b/template/progressbar/progress.html
new file mode 100644
index 0000000000..d390e79f7d
--- /dev/null
+++ b/template/progressbar/progress.html
@@ -0,0 +1 @@
+
\ No newline at end of file