From c4d62ec0b53fd9620ed10da7fc96de8f2cb96fa8 Mon Sep 17 00:00:00 2001 From: Max Schulze Date: Thu, 27 Aug 2015 22:43:50 +0200 Subject: [PATCH 1/2] feat(rating): backport from angular-ui/bootstrap, now using `ngModel` --- Gruntfile.js | 2 +- src/rating/docs/demo.html | 16 +--- src/rating/docs/readme.md | 8 +- src/rating/rating.js | 88 ++++++++++++------ src/rating/test/rating.spec.js | 164 ++++++++++++++++++++++++++++++--- template/rating/rating.html | 6 +- 6 files changed, 222 insertions(+), 62 deletions(-) diff --git a/Gruntfile.js b/Gruntfile.js index 368b524..6dfe8b7 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -9,7 +9,7 @@ module.exports = function(grunt) { grunt.initConfig({ ngversion: '1.3.3', - fdversion: '5.2.0', + fdversion: '5.5.2', faversion: '4.2.0', modules: [],//to be filled in by build task pkg: grunt.file.readJSON('package.json'), diff --git a/src/rating/docs/demo.html b/src/rating/docs/demo.html index a203e9e..3e16292 100644 --- a/src/rating/docs/demo.html +++ b/src/rating/docs/demo.html @@ -1,21 +1,15 @@

Default

- + {{percent}}%
Rate: {{rate}} - Readonly is: {{isReadonly}} - Hovering over: {{overStar || "none"}}
- - + +

Custom icons

-
- - (Rate: {{x}}) -
-
- - (Rate: {{y}}) -
+
(Rate: {{x}})
+
(Rate: {{y}})
diff --git a/src/rating/docs/readme.md b/src/rating/docs/readme.md index bf20c18..ba8bf6b 100644 --- a/src/rating/docs/readme.md +++ b/src/rating/docs/readme.md @@ -6,7 +6,7 @@ It uses Font Awesome icons (http://fontawesome.io/) by default. #### `` #### - * `value` + * `ng-model` : The current rate. @@ -14,10 +14,14 @@ It uses Font Awesome icons (http://fontawesome.io/) by default. _(Defaults: 5)_ : Changes the number of icons. - * `readonly` + * `readonly` _(Defaults: false)_ : Prevent user's interaction. + * `titles` + _(Defaults: ["one", "two", "three", "four", "five"])_ : + An array of Strings defining titles for all icons + * `on-hover(value)` : An optional expression called when user's mouse is over a particular icon. diff --git a/src/rating/rating.js b/src/rating/rating.js index 6367d0e..b791075 100644 --- a/src/rating/rating.js +++ b/src/rating/rating.js @@ -3,70 +3,98 @@ angular.module('mm.foundation.rating', []) .constant('ratingConfig', { max: 5, stateOn: null, - stateOff: null + stateOff: null, + titles : ['one', 'two', 'three', 'four', 'five'] }) -.controller('RatingController', ['$scope', '$attrs', '$parse', 'ratingConfig', function($scope, $attrs, $parse, ratingConfig) { +.controller('RatingController', ['$scope', '$attrs', 'ratingConfig', function($scope, $attrs, ratingConfig) { + var ngModelCtrl = { $setViewValue: angular.noop }; + + this.init = function(ngModelCtrl_) { + ngModelCtrl = ngModelCtrl_; + ngModelCtrl.$render = this.render; + + ngModelCtrl.$formatters.push(function(value) { + if (angular.isNumber(value) && value << 0 !== value) { + value = Math.round(value); + } + return value; + }); - this.maxRange = angular.isDefined($attrs.max) ? $scope.$parent.$eval($attrs.max) : ratingConfig.max; this.stateOn = angular.isDefined($attrs.stateOn) ? $scope.$parent.$eval($attrs.stateOn) : ratingConfig.stateOn; this.stateOff = angular.isDefined($attrs.stateOff) ? $scope.$parent.$eval($attrs.stateOff) : ratingConfig.stateOff; + var tmpTitles = angular.isDefined($attrs.titles) ? $scope.$parent.$eval($attrs.titles) : ratingConfig.titles ; + this.titles = angular.isArray(tmpTitles) && tmpTitles.length > 0 ? + tmpTitles : ratingConfig.titles; - this.createRateObjects = function(states) { - var defaultOptions = { - stateOn: this.stateOn, - stateOff: this.stateOff - }; + var ratingStates = angular.isDefined($attrs.ratingStates) ? + $scope.$parent.$eval($attrs.ratingStates) : + new Array(angular.isDefined($attrs.max) ? $scope.$parent.$eval($attrs.max) : ratingConfig.max); + $scope.range = this.buildTemplateObjects(ratingStates); + }; - for (var i = 0, n = states.length; i < n; i++) { - states[i] = angular.extend({ index: i }, defaultOptions, states[i]); - } - return states; + this.buildTemplateObjects = function(states) { + for (var i = 0, n = states.length; i < n; i++) { + states[i] = angular.extend({ index: i }, { stateOn: this.stateOn, stateOff: this.stateOff, title: this.getTitle(i) }, states[i]); + } + return states; }; - // Get objects used in template - $scope.range = angular.isDefined($attrs.ratingStates) ? this.createRateObjects(angular.copy($scope.$parent.$eval($attrs.ratingStates))): this.createRateObjects(new Array(this.maxRange)); + this.getTitle = function(index) { + if (index >= this.titles.length) { + return index + 1; + } else { + return this.titles[index]; + } + }; $scope.rate = function(value) { - if ( $scope.value !== value && !$scope.readonly ) { - $scope.value = value; + if (!$scope.readonly && value >= 0 && value <= $scope.range.length) { + ngModelCtrl.$setViewValue(ngModelCtrl.$viewValue === value ? 0 : value); + ngModelCtrl.$render(); } }; $scope.enter = function(value) { - if ( ! $scope.readonly ) { - $scope.val = value; + if (!$scope.readonly) { + $scope.value = value; } $scope.onHover({value: value}); }; $scope.reset = function() { - $scope.val = angular.copy($scope.value); + $scope.value = ngModelCtrl.$viewValue; $scope.onLeave(); }; - $scope.$watch('value', function(value) { - $scope.val = value; - }); + $scope.onKeydown = function(evt) { + if (/(37|38|39|40)/.test(evt.which)) { + evt.preventDefault(); + evt.stopPropagation(); + $scope.rate($scope.value + (evt.which === 38 || evt.which === 39 ? 1 : -1)); + } + }; - $scope.readonly = false; - if ($attrs.readonly) { - $scope.$parent.$watch($parse($attrs.readonly), function(value) { - $scope.readonly = !!value; - }); - } + this.render = function() { + $scope.value = ngModelCtrl.$viewValue; + }; }]) .directive('rating', function() { return { restrict: 'EA', + require: ['rating', 'ngModel'], scope: { - value: '=', + readonly: '=?', onHover: '&', onLeave: '&' }, controller: 'RatingController', templateUrl: 'template/rating/rating.html', - replace: true + replace: true, + link: function(scope, element, attrs, ctrls) { + var ratingCtrl = ctrls[0], ngModelCtrl = ctrls[1]; + ratingCtrl.init( ngModelCtrl ); + } }; }); diff --git a/src/rating/test/rating.spec.js b/src/rating/test/rating.spec.js index 54afe0a..fb8641d 100644 --- a/src/rating/test/rating.spec.js +++ b/src/rating/test/rating.spec.js @@ -1,12 +1,12 @@ -describe('rating directive', function () { - var $rootScope, element; +describe('rating directive', function() { + var $rootScope, $compile, element; beforeEach(module('mm.foundation.rating')); beforeEach(module('template/rating/rating.html')); beforeEach(inject(function(_$compile_, _$rootScope_) { $compile = _$compile_; $rootScope = _$rootScope_; $rootScope.rate = 3; - element = $compile('')($rootScope); + element = $compile('')($rootScope); $rootScope.$digest(); })); @@ -27,12 +27,27 @@ describe('rating directive', function () { return state; } + function getTitles() { + var stars = getStars(); + return stars.toArray().map(function(star) { + return angular.element(star).attr('title'); + }); + } + + function triggerKeyDown(keyCode) { + var e = $.Event('keydown'); + e.which = keyCode; + element.trigger(e); + } + it('contains the default number of icons', function() { expect(getStars().length).toBe(5); + expect(element.attr('aria-valuemax')).toBe('5'); }); it('initializes the default star icons as selected', function() { expect(getState()).toEqual([true, true, true, false, false]); + expect(element.attr('aria-valuenow')).toBe('3'); }); it('handles correctly the click event', function() { @@ -40,11 +55,19 @@ describe('rating directive', function () { $rootScope.$digest(); expect(getState()).toEqual([true, true, false, false, false]); expect($rootScope.rate).toBe(2); + expect(element.attr('aria-valuenow')).toBe('2'); getStar(5).click(); $rootScope.$digest(); expect(getState()).toEqual([true, true, true, true, true]); expect($rootScope.rate).toBe(5); + expect(element.attr('aria-valuenow')).toBe('5'); + + getStar(5).click(); + $rootScope.$digest(); + expect(getState()).toEqual([false, false, false, false, false]); + expect($rootScope.rate).toBe(0); + expect(element.attr('aria-valuenow')).toBe('0'); }); it('handles correctly the hover event', function() { @@ -63,30 +86,47 @@ describe('rating directive', function () { expect($rootScope.rate).toBe(3); }); + it('rounds off the number of stars shown with decimal values', function() { + $rootScope.rate = 2.1; + $rootScope.$digest(); + + expect(getState()).toEqual([true, true, false, false, false]); + expect(element.attr('aria-valuenow')).toBe('2'); + + $rootScope.rate = 2.5; + $rootScope.$digest(); + + expect(getState()).toEqual([true, true, true, false, false]); + expect(element.attr('aria-valuenow')).toBe('3'); + }); + it('changes the number of selected icons when value changes', function() { $rootScope.rate = 2; $rootScope.$digest(); expect(getState()).toEqual([true, true, false, false, false]); + expect(element.attr('aria-valuenow')).toBe('2'); }); it('shows different number of icons when `max` attribute is set', function() { - element = $compile('')($rootScope); + element = $compile('')($rootScope); $rootScope.$digest(); expect(getStars().length).toBe(7); + expect(element.attr('aria-valuemax')).toBe('7'); }); it('shows different number of icons when `max` attribute is from scope variable', function() { $rootScope.max = 15; - element = $compile('')($rootScope); + element = $compile('')($rootScope); $rootScope.$digest(); expect(getStars().length).toBe(15); + expect(element.attr('aria-valuemax')).toBe('15'); }); it('handles readonly attribute', function() { $rootScope.isReadonly = true; - element = $compile('')($rootScope); + element = $compile('')($rootScope); $rootScope.$digest(); expect(getState()).toEqual([true, true, true, false, false]); @@ -106,7 +146,7 @@ describe('rating directive', function () { it('should fire onHover', function() { $rootScope.hoveringOver = jasmine.createSpy('hoveringOver'); - element = $compile('')($rootScope); + element = $compile('')($rootScope); $rootScope.$digest(); getStar(3).trigger('mouseover'); @@ -116,7 +156,7 @@ describe('rating directive', function () { it('should fire onLeave', function() { $rootScope.leaving = jasmine.createSpy('leaving'); - element = $compile('')($rootScope); + element = $compile('')($rootScope); $rootScope.$digest(); element.trigger('mouseleave'); @@ -124,11 +164,59 @@ describe('rating directive', function () { expect($rootScope.leaving).toHaveBeenCalled(); }); + describe('keyboard navigation', function() { + it('supports arrow keys', function() { + triggerKeyDown(38); + expect($rootScope.rate).toBe(4); + + triggerKeyDown(37); + expect($rootScope.rate).toBe(3); + triggerKeyDown(40); + expect($rootScope.rate).toBe(2); + + triggerKeyDown(39); + expect($rootScope.rate).toBe(3); + }); + + it('supports only arrow keys', function() { + $rootScope.rate = undefined; + $rootScope.$digest(); + + triggerKeyDown(36); + expect($rootScope.rate).toBe(undefined); + + triggerKeyDown(41); + expect($rootScope.rate).toBe(undefined); + }); + + it('can get zero value but not negative', function() { + $rootScope.rate = 1; + $rootScope.$digest(); + + triggerKeyDown(37); + expect($rootScope.rate).toBe(0); + + triggerKeyDown(37); + expect($rootScope.rate).toBe(0); + }); + + it('cannot get value above max', function() { + $rootScope.rate = 4; + $rootScope.$digest(); + + triggerKeyDown(38); + expect($rootScope.rate).toBe(5); + + triggerKeyDown(38); + expect($rootScope.rate).toBe(5); + }); + }); + describe('custom states', function() { beforeEach(inject(function() { $rootScope.classOn = 'icon-ok-sign'; $rootScope.classOff = 'icon-ok-circle'; - element = $compile('')($rootScope); + element = $compile('')($rootScope); $rootScope.$digest(); })); @@ -145,12 +233,13 @@ describe('rating directive', function () { {stateOn: 'heart'}, {stateOff: 'off'} ]; - element = $compile('')($rootScope); + element = $compile('')($rootScope); $rootScope.$digest(); })); - it('should define number of icon elements', function () { - expect(getStars().length).toBe($rootScope.states.length); + it('should define number of icon elements', function() { + expect(getStars().length).toBe(4); + expect(element.attr('aria-valuemax')).toBe('4'); }); it('handles each icon', function() { @@ -175,7 +264,7 @@ describe('rating directive', function () { ratingConfig.max = 10; ratingConfig.stateOn = 'on'; ratingConfig.stateOff = 'off'; - element = $compile('')($rootScope); + element = $compile('')($rootScope); $rootScope.$digest(); })); afterEach(inject(function(ratingConfig) { @@ -183,12 +272,57 @@ describe('rating directive', function () { angular.extend(ratingConfig, originalConfig); })); - it('should change number of icon elements', function () { + it('should change number of icon elements', function() { expect(getStars().length).toBe(10); }); - it('should change icon states', function () { + it('should change icon states', function() { expect(getState('on', 'off')).toEqual([true, true, true, true, true, false, false, false, false, false]); }); }); + + describe('Default title', function() { + it('should return the default title for each star', function() { + expect(getTitles()).toEqual(['one', 'two', 'three', 'four', 'five']); + }); + }); + + describe('shows different title when `max` attribute is greater than the titles array ', function() { + var originalConfig = {}; + beforeEach(inject(function(ratingConfig) { + $rootScope.rate = 5; + angular.extend(originalConfig, ratingConfig); + ratingConfig.max = 10; + element = $compile('')($rootScope); + $rootScope.$digest(); + })); + afterEach(inject(function(ratingConfig) { + // return it to the original state + angular.extend(ratingConfig, originalConfig); + })); + + it('should return the default title for each star', function() { + expect(getTitles()).toEqual(['one', 'two', 'three', 'four', 'five', '6', '7', '8', '9', '10']); + }); + }); + + describe('shows custom titles ', function() { + it('should return the custom title for each star', function() { + $rootScope.titles = [44,45,46]; + element = $compile('')($rootScope); + $rootScope.$digest(); + expect(getTitles()).toEqual(['44', '45', '46', '4', '5']); + }); + it('should return the default title if the custom title is empty', function() { + $rootScope.titles = []; + element = $compile('')($rootScope); + $rootScope.$digest(); + expect(getTitles()).toEqual(['one', 'two', 'three', 'four', 'five']); + }); + it('should return the default title if the custom title is not an array', function() { + element = $compile('')($rootScope); + $rootScope.$digest(); + expect(getTitles()).toEqual(['one', 'two', 'three', 'four', 'five']); + }); + }); }); diff --git a/template/rating/rating.html b/template/rating/rating.html index 7f26bfb..bf351ef 100644 --- a/template/rating/rating.html +++ b/template/rating/rating.html @@ -1,4 +1,4 @@ - - + + ({{ $index < value ? '*' : ' ' }}) + From a011f29381ddc27b5f417cff31d63b868c6a826e Mon Sep 17 00:00:00 2001 From: Max Schulze Date: Mon, 19 Oct 2015 17:58:07 +0200 Subject: [PATCH 2/2] update Gruntfile --- Gruntfile.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Gruntfile.js b/Gruntfile.js index 6dfe8b7..fd52ee2 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -8,7 +8,8 @@ module.exports = function(grunt) { grunt.util.linefeed = '\n'; grunt.initConfig({ - ngversion: '1.3.3', + ngversion: '1.4.6', + nglegacyversion: '1.3.19', fdversion: '5.5.2', faversion: '4.2.0', modules: [],//to be filled in by build task