-
Notifications
You must be signed in to change notification settings - Fork 93
/
Rating.jsx
225 lines (218 loc) · 7.5 KB
/
Rating.jsx
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
'use strict';
var React = require('react');
var Style = require('./style');
var Symbol = require('./PercentageSymbol');
// Returns the index of the rate in the range (start, stop, step).
// Returns undefined index if the rate is outside the range.
// NOTE: A range.step of 0 produces an empty range and consequently returns an
// undefined index.
var indexOf = function (range, rate) {
// Check the rate is in the proper range [start..stop] according to
// the start, stop and step properties in props.
var step = range.step;
var start = step > 0 ? range.start : range.stop;
var stop = step > 0 ? range.stop : range.start;
if (step && start <= rate && rate <= stop) {
// The index corresponds to the number of steps of size props.step
// that fits between rate and start.
// This index does not need to be a whole number because we can have
// fractional symbols, and consequently fractional/float indexes.
return (rate - range.start) / step;
}
};
var Rating = React.createClass({
// Define propTypes only in development.
propTypes: typeof __DEV__ !== 'undefined' && __DEV__ && {
start: React.PropTypes.number,
stop: React.PropTypes.number,
step: React.PropTypes.number,
initialRate: React.PropTypes.number,
placeholderRate: React.PropTypes.number,
empty: React.PropTypes.oneOfType([
// Array of class names and/or style objects.
React.PropTypes.arrayOf(React.PropTypes.oneOfType[
React.PropTypes.string,
React.PropTypes.object,
React.PropTypes.element
]),
// Class names.
React.PropTypes.string,
// Style objects.
React.PropTypes.object]),
placeholder: React.PropTypes.oneOfType([
// Array of class names and/or style objects.
React.PropTypes.arrayOf(React.PropTypes.oneOfType[
React.PropTypes.string,
React.PropTypes.object,
React.PropTypes.element
]),
// Class names.
React.PropTypes.string,
// Style objects.
React.PropTypes.object]),
full: React.PropTypes.oneOfType([
// Array of class names and/or style objects.
React.PropTypes.arrayOf(React.PropTypes.oneOfType[
React.PropTypes.string,
React.PropTypes.object,
React.PropTypes.element
]),
// Class names.
React.PropTypes.string,
// Style objects.
React.PropTypes.object]),
readonly: React.PropTypes.bool,
quiet: React.PropTypes.bool,
fractions: React.PropTypes.number,
scale: React.PropTypes.number,
onChange: React.PropTypes.func,
onClick: React.PropTypes.func,
onRate: React.PropTypes.func
},
getDefaultProps: function () {
return {
start: 0,
stop: 5,
step: 1,
empty: Style.empty,
placeholder: Style.placeholder,
full: Style.full,
fractions: 1,
scale: 3,
onChange: function (rate) {},
onClick: function (rate) {},
onRate: function (rate) {}
};
},
componentDidMount: function () {
this.setState({
// detect the computed direction style for the mounted component
direction: window.getComputedStyle(this.refs.container, null).getPropertyValue("direction")
});
},
componentWillReceiveProps: function (nextProps) {
var rate = nextProps.initialRate !== undefined ?
nextProps.initialRate : nextProps.placeholderRate;
this.setState({
index: indexOf(nextProps, rate),
selected: nextProps.initialRate !== undefined
});
},
getInitialState: function () {
var index = this.props.initialRate !== undefined ?
this.props.initialRate : this.props.placeholderRate;
return {
index: this._rateToIndex(index),
indexOver: undefined,
// Default direction is left to right
direction: 'ltr'
};
},
handleClick: function (i, event) {
var index = i + this._fractionalIndex(event);
this.props.onClick(this._indexToRate(index), event);
if (this.state.index !== index) {
this.props.onChange(this._indexToRate(index));
this.setState({
indexOver: undefined,
index: index,
selected: true
});
}
},
handleMouseLeave: function () {
this.props.onRate();
this.setState({
indexOver: undefined
});
},
handleMouseMove: function (i, event) {
var index = i + this._fractionalIndex(event);
if (this.state.indexOver !== index) {
this.props.onRate(this._indexToRate(index));
this.setState({
indexOver: index
});
}
},
// Calculate the rate of an index according the the start and step.
_indexToRate: function (index) {
return this.props.start + Math.floor(index) * this.props.step +
this.props.step * this._roundToFraction(index % 1);
},
// Calculate the corresponding index for a rate according to the provided
// props or this.props.
_rateToIndex: function (rate) {
return indexOf(this.props, rate);
},
_roundToFraction: function (index) {
// Get the closest top fraction.
var fraction = Math.ceil(index % 1 * this.props.fractions) / this.props.fractions;
// Truncate decimal trying to avoid float precission issues.
var precision = Math.pow(10, this.props.scale);
return Math.floor(index) + Math.floor(fraction * precision) / precision;
},
_fractionalIndex: function (event) {
var x = this.state.direction === 'rtl' ?
event.currentTarget.getBoundingClientRect().right - event.clientX :
event.clientX - event.currentTarget.getBoundingClientRect().left;
return this._roundToFraction(x / event.currentTarget.offsetWidth);
},
render: function () {
var symbolNodes = [];
var empty = [].concat(this.props.empty);
var placeholder = [].concat(this.props.placeholder);
var full = [].concat(this.props.full);
// The symbol with the mouse over prevails over the selected one,
// provided that we are not in quiet mode.
var index = !this.props.quiet && this.state.indexOver !== undefined ?
this.state.indexOver : this.state.index;
// The index of the last full symbol or NaN if index is undefined.
var lastFullIndex = Math.floor(index);
// Render the number of whole symbols.
var icon = !this.state.selected &&
this.props.initialRate === undefined &&
this.props.placeholderRate !== undefined &&
(this.props.quiet || this.state.indexOver === undefined) ?
placeholder : full;
for (var i = 0; i < Math.floor(this._rateToIndex(this.props.stop)); i++) {
// Return the percentage of the decimal part of the last full index,
// 100 percent for those below the last full index or 0 percent for those
// indexes NaN or above the last full index.
var percent = i - lastFullIndex === 0 ? index % 1 * 100 :
i - lastFullIndex < 0 ? 100 : 0;
symbolNodes.push(<Symbol
key={i}
background={empty[i % empty.length]}
icon={icon[i % icon.length]}
percent={percent}
onClick={!this.props.readonly && this.handleClick.bind(this, i)}
onMouseMove={!this.props.readonly && this.handleMouseMove.bind(this, i)}
direction={this.state.direction}
/>);
}
var {
start,
stop,
step,
empty,
initialRate,
placeholderRate,
placeholder,
full,
readonly,
quiet,
fractions,
scale,
onChange,
onClick,
onRate,
...other } = this.props;
return (
<span ref="container" onMouseLeave={!readonly && this.handleMouseLeave} {...other}>
{symbolNodes}
</span>
);
}
});
module.exports = Rating;