UNPKG

17.4 kBJavaScriptView Raw
1import _extends from 'babel-runtime/helpers/extends';
2import _classCallCheck from 'babel-runtime/helpers/classCallCheck';
3import _possibleConstructorReturn from 'babel-runtime/helpers/possibleConstructorReturn';
4import _inherits from 'babel-runtime/helpers/inherits';
5
6var _class, _temp;
7
8import React, { Component } from 'react';
9import PropTypes from 'prop-types';
10import classNames from 'classnames';
11import { polyfill } from 'react-lifecycles-compat';
12
13import Icon from '../icon';
14import { func, KEYCODE, obj } from '../util';
15import zhCN from '../locale/zh-cn';
16
17var noop = func.noop,
18 bindCtx = func.bindCtx;
19var ENTER = KEYCODE.ENTER,
20 LEFT = KEYCODE.LEFT,
21 UP = KEYCODE.UP,
22 RIGHT = KEYCODE.RIGHT,
23 DOWN = KEYCODE.DOWN;
24
25var supportKeys = [ENTER, LEFT, UP, RIGHT, DOWN];
26
27// 评分组件的大小与icon的大小映射关系
28var ICON_SIZE_MAP = {
29 small: 'xs',
30 medium: 'small',
31 large: 'medium'
32};
33
34/** Rating */
35var Rating = (_temp = _class = function (_Component) {
36 _inherits(Rating, _Component);
37
38 Rating.currentValue = function currentValue(min, max, hoverValue, stateValue) {
39 var value = hoverValue ? hoverValue : stateValue;
40
41 value = value >= max ? max : value;
42 value = value <= min ? min : value;
43
44 return value || 0;
45 };
46
47 function Rating(props) {
48 _classCallCheck(this, Rating);
49
50 var _this = _possibleConstructorReturn(this, _Component.call(this, props));
51
52 _this.saveRef = function (ref, i) {
53 _this['refs-rating-icon-' + i] = ref;
54 };
55
56 _this.state = {
57 value: 'value' in props ? props.value : props.defaultValue,
58 hoverValue: 0,
59 cleanedValue: null,
60 iconSpace: 0,
61 iconSize: 0,
62 clicked: false // 标记组件是否被点击过
63 };
64 _this.timer = null;
65
66 bindCtx(_this, ['handleClick', 'handleHover', 'handleLeave', 'onKeyDown']);
67 return _this;
68 }
69
70 Rating.getDerivedStateFromProps = function getDerivedStateFromProps(nextProps, prevState) {
71 var state = {};
72 if ('value' in nextProps) {
73 state.value = nextProps.value || 0;
74 }
75
76 if ('disabled' in nextProps || 'readOnly' in nextProps || 'isPreview' in nextProps || 'renderPreview' in nextProps) {
77 state.disabled = nextProps.disabled || nextProps.readOnly || nextProps.isPreview && !('renderPreview' in nextProps);
78 }
79
80 return state;
81 };
82
83 Rating.prototype.componentDidMount = function componentDidMount() {
84 this.getRenderResult();
85 };
86
87 Rating.prototype.componentWillUnmount = function componentWillUnmount() {
88 this.clearTimer();
89 };
90
91 // 清除延时
92
93
94 Rating.prototype.clearTimer = function clearTimer() {
95 if (this.timer) {
96 clearTimeout(this.timer);
97 this.timer = null;
98 }
99 };
100
101 Rating.prototype.getRenderResult = function getRenderResult() {
102 var count = this.props.count;
103 var _state = this.state,
104 iconSpace = _state.iconSpace,
105 iconSize = _state.iconSize;
106
107 var icon = this['refs-rating-icon-0'];
108
109 if (icon && this.underlayNode) {
110 var newIconSize = icon.offsetWidth;
111 var newIconSpace = (this.underlayNode.offsetWidth - count * newIconSize) / (count + 1);
112
113 if (newIconSize !== iconSize || newIconSpace !== iconSpace) {
114 this.setState({
115 iconSpace: newIconSpace,
116 iconSize: newIconSize
117 });
118 }
119 }
120 };
121
122 Rating.prototype.getValue = function getValue(e) {
123 // 如定位不准,优先纠正定位
124 this.getRenderResult();
125
126 var _props = this.props,
127 allowHalf = _props.allowHalf,
128 count = _props.count,
129 rtl = _props.rtl;
130 var _state2 = this.state,
131 iconSpace = _state2.iconSpace,
132 iconSize = _state2.iconSize;
133
134
135 var pos = e.pageX - this.underlayNode.getBoundingClientRect().left;
136 var fullNum = Math.floor(pos / (iconSpace + iconSize));
137 var surplusNum = (pos - fullNum * (iconSpace + iconSize) - iconSpace) / iconSize;
138 var value = Number(fullNum) + Number(surplusNum.toFixed(1));
139 if (value >= count) {
140 value = count;
141 } else if (allowHalf) {
142 var floorValue = Math.floor(value);
143 if (rtl) {
144 value = value - 0.5 >= floorValue ? floorValue + 1.5 : floorValue + 1;
145 } else {
146 value = value - 0.5 >= floorValue ? floorValue + 1 : floorValue + 0.5;
147 }
148 } else {
149 value = Math.floor(value) + 1;
150 }
151
152 return rtl ? count - value + 1 : value;
153 };
154
155 Rating.prototype.handleHover = function handleHover(e) {
156 var _this2 = this;
157
158 if (this.state.disabled) {
159 return;
160 }
161
162 var value = this.getValue(e);
163 var onHoverChange = this.props.onHoverChange;
164 var cleanedValue = this.state.cleanedValue;
165
166 if (cleanedValue !== value) {
167 this.clearTimer();
168
169 this.timer = setTimeout(function () {
170 _this2.setState({ hoverValue: value, cleanedValue: null }, function () {
171 onHoverChange(value);
172 });
173 }, 0);
174 }
175 };
176
177 Rating.prototype.handleLeave = function handleLeave() {
178 var onHoverChange = this.props.onHoverChange;
179
180 if (this.state.disabled) {
181 return;
182 }
183
184 this.clearTimer();
185
186 this.setState({
187 hoverValue: 0,
188 cleanedValue: null
189 });
190 onHoverChange(undefined);
191 };
192
193 Rating.prototype.onKeyDown = function onKeyDown(e) {
194 if (this.state.disabled) {
195 return;
196 }
197
198 var _props2 = this.props,
199 onKeyDown = _props2.onKeyDown,
200 count = _props2.count;
201 var disabled = this.state.disabled;
202
203 if (disabled || supportKeys.indexOf(e.keyCode) < 0) {
204 return !onKeyDown || onKeyDown(e);
205 }
206
207 var _state3 = this.state,
208 hoverValue = _state3.hoverValue,
209 value = _state3.value;
210
211 var changingValue = hoverValue;
212 if (changingValue === 0) {
213 changingValue = value;
214 }
215
216 switch (e.keyCode) {
217 case DOWN:
218 case RIGHT:
219 if (changingValue < count) {
220 changingValue += 1;
221 } else {
222 changingValue = 1;
223 }
224 this.handleChecked(changingValue);
225 break;
226 case UP:
227 case LEFT:
228 if (changingValue > 1) {
229 changingValue -= 1;
230 } else {
231 changingValue = count;
232 }
233 this.handleChecked(changingValue);
234 break;
235 case ENTER:
236 this.props.onChange(changingValue);
237 this.setState({
238 value: changingValue,
239 hoverValue: changingValue
240 });
241 break;
242 }
243 return !onKeyDown || onKeyDown(e);
244 };
245
246 Rating.prototype.handleChecked = function handleChecked(index) {
247 if (this.state.disabled) {
248 return;
249 }
250
251 this.setState({ hoverValue: index });
252 };
253
254 Rating.prototype.handleClick = function handleClick(e) {
255 var _this3 = this;
256
257 if (this.state.disabled) {
258 return;
259 }
260 var allowClear = this.props.allowClear;
261 var value = this.state.value;
262
263 var newValue = this.getValue(e);
264 var isReset = false;
265 if (allowClear) {
266 isReset = newValue === value;
267 }
268 this.handleLeave();
269 if (newValue < 0) {
270 return;
271 }
272
273 if (!('value' in this.props)) {
274 this.setState({ value: isReset ? 0 : newValue, clicked: true });
275 }
276
277 this.props.onChange(isReset ? 0 : newValue);
278 setTimeout(function () {
279 _this3.setState({ clicked: false });
280 }, 100);
281 this.setState({
282 cleanedValue: isReset ? newValue : null
283 });
284 };
285
286 Rating.prototype.getOverlayWidth = function getOverlayWidth() {
287 var _state4 = this.state,
288 hoverValue = _state4.hoverValue,
289 iconSpace = _state4.iconSpace,
290 iconSize = _state4.iconSize;
291
292
293 if (!iconSpace || !iconSize) {
294 return 'auto';
295 }
296
297 var value = Rating.currentValue(0, this.props.count, hoverValue, this.state.value);
298
299 var floorValue = Math.floor(value);
300
301 return iconSize * value + (floorValue + 1) * iconSpace;
302 };
303
304 Rating.prototype.getInfoLeft = function getInfoLeft() {
305 var _state5 = this.state,
306 value = _state5.value,
307 hoverValue = _state5.hoverValue,
308 iconSpace = _state5.iconSpace,
309 iconSize = _state5.iconSize;
310
311 var infoValue = hoverValue || value;
312 var ceilValue = Math.ceil(infoValue);
313
314 return iconSize * (ceilValue - 1) + ceilValue * iconSpace;
315 };
316
317 Rating.prototype.render = function render() {
318 var _classNames,
319 _classNames2,
320 _classNames3,
321 _this4 = this;
322
323 var _props3 = this.props,
324 id = _props3.id,
325 prefix = _props3.prefix,
326 className = _props3.className,
327 showGrade = _props3.showGrade,
328 count = _props3.count,
329 size = _props3.size,
330 iconType = _props3.iconType,
331 strokeMode = _props3.strokeMode,
332 readAs = _props3.readAs,
333 rtl = _props3.rtl,
334 isPreview = _props3.isPreview,
335 renderPreview = _props3.renderPreview,
336 locale = _props3.locale;
337 var disabled = this.state.disabled;
338
339 var others = obj.pickOthers(Rating.propTypes, this.props);
340 var _state6 = this.state,
341 hoverValue = _state6.hoverValue,
342 clicked = _state6.clicked;
343
344 var underlay = [],
345 overlay = [];
346
347 var enableA11y = !!id;
348
349 // 获得Value
350 var value = Rating.currentValue(0, count, hoverValue, this.state.value);
351
352 // icon的sizeMap
353 var sizeMap = ICON_SIZE_MAP[size];
354
355 var _loop = function _loop(i) {
356 var _classNames4;
357
358 var isCurrent = Math.ceil(value - 1) === i;
359 var iconCls = classNames((_classNames4 = {
360 hover: hoverValue > 0 && isCurrent,
361 clicked: clicked && isCurrent
362 }, _classNames4[prefix + 'rating-symbol-icon'] = !iconType, _classNames4));
363 var iconNode = iconType ? React.createElement(Icon, { type: iconType, size: sizeMap, className: iconCls }) : React.createElement(Icon, { type: 'favorites-filling', size: sizeMap, className: iconCls });
364
365 var saveRefs = function saveRefs(ref) {
366 _this4.saveRef(ref, i);
367 };
368
369 underlay.push(React.createElement(
370 'span',
371 { ref: saveRefs, key: 'underlay-' + i, className: prefix + 'rating-icon' },
372 iconNode
373 ));
374 if (enableA11y) {
375 overlay.push(React.createElement('input', {
376 id: id + '-' + prefix + 'star' + (i + 1),
377 key: 'input-' + i,
378 className: prefix + 'sr-only',
379 'aria-checked': i + 1 === parseInt(hoverValue),
380 checked: i + 1 === parseInt(hoverValue),
381 onChange: _this4.handleChecked.bind(_this4, i + 1),
382 type: 'radio',
383 name: 'rating'
384 }));
385 }
386
387 overlay.push(React.createElement(
388 'label',
389 {
390 key: 'overlay-' + i,
391 htmlFor: enableA11y ? id + '-' + prefix + 'star' + (i + 1) : null,
392 className: prefix + 'rating-icon'
393 },
394 iconNode,
395 enableA11y ? React.createElement(
396 'span',
397 { className: prefix + 'sr-only' },
398 readAs(i + 1)
399 ) : null
400 ));
401 };
402
403 for (var i = 0; i < count; i++) {
404 _loop(i);
405 }
406
407 var ratingCls = classNames([prefix + 'rating', prefix + 'rating-' + size], (_classNames = {}, _classNames[prefix + 'rating-grade-low'] = value <= count * 0.4, _classNames[prefix + 'rating-grade-high'] = value > count * 0.4, _classNames[prefix + 'rating-stroke-mode'] = strokeMode, _classNames.hover = hoverValue > 0, _classNames), className);
408
409 var baseCls = classNames(prefix + 'rating-base', (_classNames2 = {}, _classNames2[prefix + 'rating-base-disabled'] = disabled, _classNames2));
410
411 var previewCls = classNames((_classNames3 = {}, _classNames3[prefix + 'form-preview'] = true, _classNames3[className] = !!className, _classNames3));
412
413 var overlayStyle = {
414 width: this.getOverlayWidth()
415 };
416 var infoStyle = {
417 left: this.getInfoLeft(),
418 display: hoverValue ? 'block' : 'none'
419 };
420
421 var finalProps = disabled ? {} : {
422 onClick: this.handleClick,
423 onMouseOver: this.handleHover,
424 onMouseMove: this.handleHover,
425 onMouseLeave: this.handleLeave
426 };
427
428 if (rtl) {
429 others.dir = 'rtl';
430 }
431
432 if (isPreview && 'renderPreview' in this.props) {
433 return React.createElement(
434 'div',
435 _extends({ id: id }, others, { className: previewCls }),
436 renderPreview(value, this.props)
437 );
438 }
439
440 return React.createElement(
441 'div',
442 _extends({
443 id: id
444 }, others, {
445 className: ratingCls,
446 onKeyDown: this.onKeyDown,
447 tabIndex: '0',
448 role: 'group',
449 'aria-label': locale.description
450 }),
451 React.createElement(
452 'div',
453 _extends({ className: baseCls }, finalProps),
454 React.createElement(
455 'div',
456 { className: prefix + 'rating-underlay', ref: function ref(n) {
457 return _this4.underlayNode = n;
458 }, 'aria-hidden': true },
459 underlay
460 ),
461 React.createElement(
462 'div',
463 { className: prefix + 'rating-overlay', style: overlayStyle, onClick: function onClick(e) {
464 return e.preventDefault();
465 } },
466 overlay
467 )
468 ),
469 showGrade ? React.createElement(
470 'div',
471 { className: prefix + 'rating-info', style: infoStyle },
472 readAs(value)
473 ) : null
474 );
475 };
476
477 return Rating;
478}(Component), _class.propTypes = {
479 prefix: PropTypes.string,
480 /**
481 * 默认值
482 */
483 defaultValue: PropTypes.number,
484 /**
485 * 值
486 */
487 value: PropTypes.number,
488 /**
489 * 评分的总数
490 */
491 count: PropTypes.number,
492 /**
493 * 是否显示 grade
494 */
495 showGrade: PropTypes.bool,
496 /**
497 * 尺寸
498 */
499 size: PropTypes.oneOf(['small', 'medium', 'large']),
500 /**
501 * 是否允许半星评分
502 */
503 allowHalf: PropTypes.bool,
504 /**
505 * 是否允许再次点击后清除
506 */
507 allowClear: PropTypes.bool,
508 /**
509 * 用户点击评分时触发的回调
510 * @param {Number} value 评分值
511 */
512 onChange: PropTypes.func,
513 /**
514 * 用户hover评分时触发的回调
515 * @param {Number} value 评分值
516 */
517 onHoverChange: PropTypes.func,
518 /**
519 * 是否禁用
520 */
521 disabled: PropTypes.bool,
522 /**
523 * 评分文案生成方法,传入id支持无障碍时,读屏软件可读
524 */
525 readAs: PropTypes.func,
526 // 实验属性: 自定义评分icon
527 iconType: PropTypes.string,
528 // 实验属性: 开启 `-webkit-text-stroke` 显示边框颜色,在IE中无效
529 strokeMode: PropTypes.bool,
530 className: PropTypes.string,
531 id: PropTypes.string,
532 rtl: PropTypes.bool,
533 /**
534 * 自定义国际化文案对象
535 */
536 locale: PropTypes.object,
537 /**
538 * 是否为预览态
539 */
540 isPreview: PropTypes.bool,
541 /**
542 * 预览态模式下渲染的内容
543 * @param {number} value 评分值
544 */
545 renderPreview: PropTypes.func,
546 /**
547 * 是否为只读态,效果上同 disabeld
548 */
549 readOnly: PropTypes.bool
550}, _class.defaultProps = {
551 prefix: 'next-',
552 size: 'medium',
553 disabled: false,
554 readOnly: false,
555 isPreview: false,
556 count: 5,
557 showGrade: false,
558 defaultValue: 0,
559 readAs: function readAs(val) {
560 return val;
561 },
562 allowHalf: false,
563 allowClear: false,
564 onChange: noop,
565 onHoverChange: noop,
566 locale: zhCN.Rating
567}, _temp);
568Rating.displayName = 'Rating';
569
570
571export default polyfill(Rating);
\No newline at end of file