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