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 ? '*' : ' ' }})
+