diff --git a/src/progressbar/docs/demo.html b/src/progressbar/docs/demo.html index ca00567d85..69e9733736 100644 --- a/src/progressbar/docs/demo.html +++ b/src/progressbar/docs/demo.html @@ -1,27 +1,23 @@
-

Static

+ +

Static

-
-
-
+
+
22%
+
166 / 200
- -

Dynamic

-
Value: {{dynamic}}
- + +

Dynamic

+ {{dynamic}} / {{max}} No animation - + {{dynamic}}% Object (changes type based on value) - + {{type}} !!! Watch out !!! -

Stacked

- Array values with automatic types -
Value: {{stackedArray}}
- - - Objects -
Value: {{stacked}}
- + +

Stacked

+ {{bar.value}}% +
\ No newline at end of file diff --git a/src/progressbar/docs/demo.js b/src/progressbar/docs/demo.js index 57676e8af6..f7358510a9 100644 --- a/src/progressbar/docs/demo.js +++ b/src/progressbar/docs/demo.js @@ -1,7 +1,9 @@ var ProgressDemoCtrl = function ($scope) { - + + $scope.max = 200; + $scope.random = function() { - var value = Math.floor((Math.random()*100)+1); + var value = Math.floor((Math.random() * 100) + 1); var type; if (value < 25) { @@ -14,28 +16,21 @@ var ProgressDemoCtrl = function ($scope) { type = 'danger'; } + $scope.showWarning = (type === 'danger' || type === 'warning'); + $scope.dynamic = value; - $scope.dynamicObject = { - value: value, - type: type - }; + $scope.type = type; }; $scope.random(); - var types = ['success', 'info', 'warning', 'danger']; $scope.randomStacked = function() { - $scope.stackedArray = []; $scope.stacked = []; + var types = ['success', 'info', 'warning', 'danger']; - 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)); + for (var i = 0, n = Math.floor((Math.random() * 4) + 1); i < n; i++) { + var index = Math.floor((Math.random() * 4)); $scope.stacked.push({ - value: value, + value: Math.floor((Math.random() * 30) + 1), type: types[index] }); } diff --git a/src/progressbar/docs/readme.md b/src/progressbar/docs/readme.md index ebc9cb9580..a43f36b3e4 100644 --- a/src/progressbar/docs/readme.md +++ b/src/progressbar/docs/readme.md @@ -1,3 +1,29 @@ -A lightweight progress bar directive that is focused on providing progress visualization! +A progress bar directive that is focused on providing feedback on the progress of a workflow or action. -The progress bar directive supports multiple (stacked) bars into the same element, optional transition animation, event handler for full & empty state and many more. +It supports multiple (stacked) bars into the same `` element or a single `` elemtnt with optional `max` attribute and transition animations. + +### Settings ### + +#### `` #### + + * `value` + : + The current value of progress completed. + + * `type` + _(Default: null)_ : + Style type. Possible values are 'success', 'warning' etc. + + * `max` + _(Default: 100)_ : + A number that specifies the total value of bars that is required. + + * `animate` + _(Default: true)_ : + Whether bars use transitions to achieve the width change. + + +### Stacked ### + +Place multiple `` into the same `` element to stack them. +`` supports `max` and `animate` & `` supports `value` and `type` attributes. diff --git a/src/progressbar/progressbar.js b/src/progressbar/progressbar.js index 2a5eb28d2d..9dcee7dfd2 100644 --- a/src/progressbar/progressbar.js +++ b/src/progressbar/progressbar.js @@ -2,105 +2,99 @@ angular.module('ui.bootstrap.progressbar', ['ui.bootstrap.transition']) .constant('progressConfig', { animate: true, - autoType: false, - stackedTypes: ['success', 'info', 'warning', 'danger'] + max: 100 }) -.controller('ProgressBarController', ['$scope', '$attrs', 'progressConfig', function($scope, $attrs, progressConfig) { +.controller('ProgressController', ['$scope', '$attrs', 'progressConfig', '$transition', function($scope, $attrs, progressConfig, $transition) { + var self = this, + bars = [], + max = angular.isDefined($attrs.max) ? $scope.$parent.$eval($attrs.max) : progressConfig.max, + animate = angular.isDefined($attrs.animate) ? $scope.$parent.$eval($attrs.animate) : progressConfig.animate; - // 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; + this.addBar = function(bar, element) { + var oldValue = 0, index = bar.$parent.$index; + if ( angular.isDefined(index) && bars[index] ) { + oldValue = bars[index].value; + } + bars.push(bar); + + this.update(element, bar.value, oldValue); - // 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; + bar.$watch('value', function(value, oldValue) { + if (value !== oldValue) { + self.update(element, value, oldValue); + } + }); - return { - from: oldValue, - to: newValue, - type: type, - animate: animate - }; + bar.$on('$destroy', function() { + self.removeBar(bar); + }); }; - function getStackedType(index) { - return stackedTypes[index]; - } + // Update bar element width + this.update = function(element, newValue, oldValue) { + var percent = this.getPercentage(newValue); + + if (animate) { + element.css('width', this.getPercentage(oldValue) + '%'); + $transition(element, {width: percent + '%'}); + } else { + element.css({'transition': 'none', 'width': percent + '%'}); + } + }; - this.addBar = function(bar) { - $scope.bars.push(bar); - $scope.totalPercent += bar.to; + this.removeBar = function(bar) { + bars.splice(bars.indexOf(bar), 1); }; - this.clearBars = function() { - $scope.bars = []; - $scope.totalPercent = 0; + this.getPercentage = function(value) { + return Math.round(100 * value / max); }; - this.clearBars(); }]) .directive('progress', function() { return { restrict: 'EA', replace: true, - controller: 'ProgressBarController', + transclude: true, + controller: 'ProgressController', + require: 'progress', + scope: {}, + template: '
' + //templateUrl: 'template/progressbar/progress.html' // Works in AngularJS 1.2 + }; +}) + +.directive('bar', function() { + return { + restrict: 'EA', + replace: true, + transclude: true, + require: '^progress', scope: { - value: '=percent', - onFull: '&', - onEmpty: '&' + value: '=', + type: '@' }, - 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); + templateUrl: 'template/progressbar/bar.html', + link: function(scope, element, attrs, progressCtrl) { + progressCtrl.addBar(scope, element); } }; }) -.directive('progressbar', ['$transition', function($transition) { +.directive('progressbar', function() { return { restrict: 'EA', replace: true, + transclude: true, + controller: 'ProgressController', scope: { - width: '=', - old: '=', - type: '=', - animate: '=' + value: '=', + type: '@' }, - 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 + '%'); - } - }); + templateUrl: 'template/progressbar/progressbar.html', + link: function(scope, element, attrs, progressCtrl) { + progressCtrl.addBar(scope, angular.element(element.children()[0])); } }; -}]); \ No newline at end of file +}); \ No newline at end of file diff --git a/src/progressbar/test/progressbar.spec.js b/src/progressbar/test/progressbar.spec.js index 70e20f0f03..e2da304e93 100644 --- a/src/progressbar/test/progressbar.spec.js +++ b/src/progressbar/test/progressbar.spec.js @@ -1,326 +1,167 @@ -describe('progressbar directive with no binding', function () { +describe('progressbar directive', function () { var $rootScope, element; beforeEach(module('ui.bootstrap.progressbar')); - beforeEach(module('template/progressbar/progress.html', 'template/progressbar/bar.html')); + beforeEach(module('template/progressbar/progressbar.html', 'template/progressbar/progress.html', 'template/progressbar/bar.html')); beforeEach(inject(function(_$compile_, _$rootScope_) { $compile = _$compile_; $rootScope = _$rootScope_; - element = $compile('')($rootScope); + $rootScope.value = 22; + element = $compile('{{value}} %')($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%'); - }); -}); + var BAR_CLASS = 'bar'; -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(); - })); + function getBar(i) { + return element.children().eq(i); + } it('has a "progress" css class', function() { - expect(element.hasClass('progress')).toBe(true); + expect(element).toHaveClass('progress'); }); 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); + expect(getBar(0)).toHaveClass(BAR_CLASS); }); it('has a "bar" element with expected width', function() { - expect(element.children().eq(0).css('width')).toBe('33%'); + expect(getBar(0).css('width')).toBe('22%'); }); - 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('transcludes "bar" text', function() { + expect(getBar(0).text()).toBe('22 %'); }); - it('can handle correctly objects value && class', function() { - $rootScope.percent = { - value: 45, - type: 'warning' - }; + it('it should be possible to add additional classes', function () { + element = $compile('')($rootScope); $rootScope.$digest(); - expect(element.children().length).toBe(1); - expect(element.hasClass('progress')).toBe(true); + expect(element).toHaveClass('progress-striped'); + expect(element).toHaveClass('active'); - 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); - }); + expect(getBar(0)).toHaveClass('pizza'); + }); -}); + describe('"max" attribute', function () { + beforeEach(inject(function() { + $rootScope.max = 200; + element = $compile('{{value}}/{{max}}')($rootScope); + $rootScope.$digest(); + })); + + it('adjusts the "bar" width', function() { + expect(element.children().eq(0).css('width')).toBe('11%'); + }); + + it('adjusts the "bar" width when value changes', function() { + $rootScope.value = 60; + $rootScope.$digest(); + expect(getBar(0).css('width')).toBe('30%'); -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); - }); + $rootScope.value += 12; + $rootScope.$digest(); + expect(getBar(0).css('width')).toBe('36%'); + + $rootScope.value = 0; + $rootScope.$digest(); + expect(getBar(0).css('width')).toBe('0%'); + }); - 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('transcludes "bar" text', function() { + expect(getBar(0).text()).toBe('22/200'); + }); }); - it('changes width when bind value changes', function() { - $rootScope.stacked[1] = 35; - $rootScope.$digest(); + describe('"type" attribute', function () { + beforeEach(inject(function() { + $rootScope.type = 'success'; + element = $compile('')($rootScope); + $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('should use correct classes', function() { + expect(getBar(0)).toHaveClass(BAR_CLASS); + expect(getBar(0)).toHaveClass(BAR_CLASS + '-success'); + }); - it('can remove bars', function() { - $rootScope.stacked.pop(); - $rootScope.$digest(); + it('should change classes if type changed', function() { + $rootScope.type = 'warning'; + $rootScope.value += 1; + $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%'); + var barEl = getBar(0); + expect(barEl).toHaveClass(BAR_CLASS); + expect(barEl).not.toHaveClass(BAR_CLASS + '-success'); + expect(barEl).toHaveClass(BAR_CLASS + '-warning'); + }); }); - it('can handle correctly object changes', function() { - $rootScope.stacked[1] = { - value: 29, - type: 'danger' - }; - $rootScope.$digest(); + describe('stacked', function () { + beforeEach(inject(function() { + $rootScope.objects = [ + { value: 10, type: 'success' }, + { value: 50, type: 'warning' }, + { value: 20 } + ]; + element = $compile('{{o.value}}')($rootScope); + $rootScope.$digest(); + })); - expect(element.children().length).toBe(3); + it('contains the right number of bars', function() { + expect(element.children().length).toBe(3); + for (var i = 0; i < 3; i++) { + expect(getBar(i)).toHaveClass(BAR_CLASS); + } + }); - var barElement; + it('renders each bar with the appropriate width', function() { + expect(getBar(0).css('width')).toBe('10%'); + expect(getBar(1).css('width')).toBe('50%'); + expect(getBar(2).css('width')).toBe('20%'); + }); - barElement = element.children().eq(0); - expect(barElement.css('width')).toBe('12%'); - expect(barElement.hasClass('bar')).toBe(true); - expect(barElement.hasClass('bar-danger')).toBe(false); + it('uses correct classes', function() { + expect(getBar(0)).toHaveClass(BAR_CLASS + '-success'); + expect(getBar(0)).not.toHaveClass(BAR_CLASS + '-warning'); - barElement = element.children().eq(1); - expect(barElement.css('width')).toBe('29%'); - expect(barElement.hasClass('bar')).toBe(true); - expect(barElement.hasClass('bar-danger')).toBe(true); + expect(getBar(1)).not.toHaveClass(BAR_CLASS + '-success'); + expect(getBar(1)).toHaveClass(BAR_CLASS + '-warning'); - barElement = element.children().eq(2); - expect(barElement.css('width')).toBe('33%'); - expect(barElement.hasClass('bar')).toBe(true); - expect(barElement.hasClass('bar-danger')).toBe(false); + expect(getBar(2)).not.toHaveClass(BAR_CLASS + '-success'); + expect(getBar(2)).not.toHaveClass(BAR_CLASS + '-warning'); + }); + + it('should change classes if type changed', function() { + $rootScope.objects = [ + { value: 20, type: 'warning' }, + { value: 50 }, + { value: 30, type: 'info' } + ]; + $rootScope.$digest(); + + expect(getBar(0)).not.toHaveClass(BAR_CLASS + '-success'); + expect(getBar(0)).toHaveClass(BAR_CLASS + '-warning'); + + expect(getBar(1)).not.toHaveClass(BAR_CLASS + '-success'); + expect(getBar(1)).not.toHaveClass(BAR_CLASS + '-warning'); + + expect(getBar(2)).toHaveClass(BAR_CLASS + '-info'); + expect(getBar(2)).not.toHaveClass(BAR_CLASS + '-success'); + expect(getBar(2)).not.toHaveClass(BAR_CLASS + '-warning'); + }); + + it('should change classes if type changed', function() { + $rootScope.objects = [ + { value: 70, type: 'info' } + ]; + $rootScope.$digest(); + + expect(element.children().length).toBe(1); + + expect(getBar(0)).toHaveClass(BAR_CLASS + '-info'); + expect(getBar(0)).not.toHaveClass(BAR_CLASS + '-success'); + expect(getBar(0)).not.toHaveClass(BAR_CLASS + '-warning'); + }); }); - - 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 index 09a5a6b010..f5895768a2 100644 --- a/template/progressbar/bar.html +++ b/template/progressbar/bar.html @@ -1 +1 @@ -
\ No newline at end of file +
\ No newline at end of file diff --git a/template/progressbar/progress.html b/template/progressbar/progress.html index d390e79f7d..1968537006 100644 --- a/template/progressbar/progress.html +++ b/template/progressbar/progress.html @@ -1 +1 @@ -
\ No newline at end of file +
\ No newline at end of file diff --git a/template/progressbar/progressbar.html b/template/progressbar/progressbar.html new file mode 100644 index 0000000000..22f57c55b7 --- /dev/null +++ b/template/progressbar/progressbar.html @@ -0,0 +1 @@ +
\ No newline at end of file