-
Notifications
You must be signed in to change notification settings - Fork 77
/
bootstrap-rating.js
247 lines (233 loc) · 9.84 KB
/
bootstrap-rating.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
(function ($, undefined) {
'use strict';
var OFFSET = 5;
function Rating(element, options) {
this.$input = $(element);
this.$rating = $('<span></span>').css({
cursor: 'default'
}).insertBefore(this.$input);
// Merge data and parameter options.
// Those provided as parameter prevail over the data ones.
this.options = (function (opts) {
// Sanitize start, stop, step, and fractions.
// All of them start, stop, and step must be integers.
opts.start = parseInt(opts.start, 10);
opts.start = isNaN(opts.start) ? undefined : opts.start;
// In case we don't have a valid stop rate try to get a reasonable
// one based on the existence of a valid start rate.
opts.stop = parseInt(opts.stop, 10);
opts.stop = isNaN(opts.stop) ?
opts.start + OFFSET || undefined : opts.stop;
// 0 step is ignored.
opts.step = parseInt(opts.step, 10) || undefined;
// Symbol fractions and scale (number of significant digits).
// 0 is ignored and negative numbers are turned to positive.
opts.fractions = Math.abs(parseInt(opts.fractions, 10)) || undefined;
opts.scale = Math.abs(parseInt(opts.scale, 10)) || undefined;
// Extend/Override the default options with those provided either as
// data attributes or function parameters.
opts = $.extend({}, $.fn.rating.defaults, opts);
// Inherit default filled if none is defined for the selected symbol.
opts.filledSelected = opts.filledSelected || opts.filled;
return opts;
}($.extend({}, this.$input.data(), options)));
this._init();
};
Rating.prototype = {
_init: function () {
var rating = this,
$input = this.$input,
$rating = this.$rating;
var ifEnabled = function (f) {
return function (e) {
// According to the W3C attribute readonly is not allowed on input
// elements with type hidden.
// Keep readonly prop for legacy but its use should be deprecated.
if (!$input.prop('disabled') && !$input.prop('readonly') &&
$input.data('readonly') === undefined) {
f.call(this, e);
}
}
};
// Build the rating control.
for (var i = 1; i <= this._rateToIndex(this.options.stop); i++) {
// Create the rating symbol container.
var $symbol = $('<div class="rating-symbol"></div>').css({
display: 'inline-block',
position: 'relative'
});
// Add background symbol to the symbol container.
$('<div class="rating-symbol-background ' + this.options.empty + '"></div>')
.appendTo($symbol);
// Add foreground symbol to the symbol container.
// The filled icon is wrapped with a div to allow fractional selection.
$('<div class="rating-symbol-foreground"></div>')
.append('<span class="' + this.options.filled + '"></span>')
.css({
display: 'inline-block',
position: 'absolute',
overflow: 'hidden',
left: 0,
// Overspecify right and left to 0 and let the container direction
// decide which one is going to take precedence according to the
// ltr/rtl direction.
// (https://developer.mozilla.org/en-US/docs/Web/CSS/right)
// When both the right CSS property and the left CSS property are
// defined, the position of the element is overspecified. In that
// case, the left value has precedence when the container is
// left-to-right (that is that the right computed value is set to
// -left), and the right value has precedence when the container is
// right-to-left (that is that the left computed value is set to
// -right).
right: 0,
width: 0
}).appendTo($symbol);
$rating.append($symbol);
this.options.extendSymbol.call($symbol, this._indexToRate(i));
}
// Initialize the rating control with the associated input value rate.
this._updateRate($input.val());
// Keep rating control and its associated input in sync.
$input
.on('change', function () {
rating._updateRate($(this).val());
});
var fractionalIndex = function (e) {
var $symbol = $(e.currentTarget);
// Calculate the distance from the mouse pointer to the origin of the
// symbol. We need to be careful with the CSS direction. If we are
// right-to-left then the symbol starts at the right. So we have to add
// the symbol width to the left offset to get the CSS rigth position.
var x = Math.abs((e.pageX || e.originalEvent.touches[0].pageX) -
(($symbol.css('direction') === 'rtl' && $symbol.width()) +
$symbol.offset().left));
// NOTE: When the mouse pointer is close to the left side of the symbol
// a negative x is returned. Probably some precision error in the
// calculation.
// x should never be less than 0 because this would mean that we are in
// the previous symbol.
x = x > 0 ? x : rating.options.scale * 0.1;
return $symbol.index() + x / $symbol.width();
};
// Keep the current highlighted index (fractional or not).
var index;
$rating
.on('mousedown touchstart', '.rating-symbol', ifEnabled(function (e) {
// Set input 'trigger' the change event.
$input.val(rating._indexToRate(fractionalIndex(e))).change();
}))
.on('mousemove touchmove', '.rating-symbol', ifEnabled(function (e) {
var current = rating._roundToFraction(fractionalIndex(e));
if (current !== index) {
// Trigger pseudo rate leave event if the mouse pointer is not
// leaving from another symbol (mouseleave).
if (index !== undefined) $(this).trigger('rating.rateleave');
// Update index and trigger rate enter event.
index = current;
$(this).trigger('rating.rateenter', [rating._indexToRate(index)]);
}
// Fill the symbols as fractions chunks.
rating._fillUntil(current);
}))
.on('mouseleave touchend', '.rating-symbol', ifEnabled(function () {
// When a symbol is left, reset index and trigger rate leave event.
index = undefined;
$(this).trigger('rating.rateleave');
// Restore on hover out.
rating._fillUntil(rating._rateToIndex(parseFloat($input.val())));
}));
},
// Fill rating symbols until index.
_fillUntil: function (index) {
var $rating = this.$rating;
// Get the index of the last whole symbol.
var i = Math.floor(index);
// Hide completely hidden symbols background.
$rating.find('.rating-symbol-background')
.css('visibility', 'visible')
.slice(0, i).css('visibility', 'hidden');
var $rates = $rating.find('.rating-symbol-foreground');
// Reset foreground
$rates.width(0);
// Fill all the foreground symbols up to the selected one.
$rates.slice(0, i).width('auto')
.find('span').attr('class', this.options.filled);
// Amend selected symbol.
$rates.eq(index % 1 ? i : i - 1)
.find('span').attr('class', this.options.filledSelected);
// Partially fill the fractional one.
$rates.eq(i).width(index % 1 * 100 + '%');
},
// Calculate the rate of an index according the the start and step.
_indexToRate: function (index) {
return this.options.start + Math.floor(index) * this.options.step +
this.options.step * this._roundToFraction(index % 1);
},
// Calculate the corresponding index for a rate.
_rateToIndex: function (rate) {
return (rate - this.options.start) / this.options.step;
},
// Round index to the configured opts.fractions.
_roundToFraction: function (index) {
// Get the closest top fraction.
var fraction = Math.ceil(index % 1 * this.options.fractions) / this.options.fractions;
// Truncate decimal trying to avoid float precission issues.
var p = Math.pow(10, this.options.scale);
return Math.floor(index) + Math.floor(fraction * p) / p;
},
// Check the rate is in the proper range [start..stop].
_contains: function (rate) {
var start = this.options.step > 0 ? this.options.start : this.options.stop;
var stop = this.options.step > 0 ? this.options.stop : this.options.start;
return start <= rate && rate <= stop;
},
// Update empty and filled rating symbols according to a rate.
_updateRate: function (rate) {
var value = parseFloat(rate);
if (this._contains(value)) {
this._fillUntil(this._rateToIndex(value));
this.$input.val(value);
} else if (rate === '') {
this._fillUntil(0);
this.$input.val('');
}
},
rate: function (value) {
if (value === undefined) {
return this.$input.val();
}
this._updateRate(value);
}
};
$.fn.rating = function (options) {
var args = Array.prototype.slice.call(arguments, 1),
result;
this.each(function () {
var $input = $(this);
var rating = $input.data('rating');
if (!rating) {
$input.data('rating', (rating = new Rating(this, options)));
}
// Underscore are used for private methods.
if (typeof options === 'string' && options[0] !== '_') {
result = rating[options].apply(rating, args);
}
});
return result !== undefined ? result : this;
};
// Plugin defaults.
$.fn.rating.defaults = {
filled: 'glyphicon glyphicon-star',
filledSelected: undefined,
empty: 'glyphicon glyphicon-star-empty',
start: 0,
stop: OFFSET,
step: 1,
fractions: 1,
scale: 3,
extendSymbol: function (rate) {},
};
$(function () {
$('input.rating').rating();
});
}(jQuery));