react-rating
Version:
A rating react component with custom symbols
583 lines (498 loc) • 19.4 kB
JavaScript
(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory(require('react')) :
typeof define === 'function' && define.amd ? define(['react'], factory) :
(global = global || self, global.ReactRating = factory(global.React));
}(this, function (React) { 'use strict';
React = React && React.hasOwnProperty('default') ? React['default'] : React;
function _typeof(obj) {
if (typeof Symbol === "function" && typeof Symbol.iterator === "symbol") {
_typeof = function (obj) {
return typeof obj;
};
} else {
_typeof = function (obj) {
return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj;
};
}
return _typeof(obj);
}
function _classCallCheck(instance, Constructor) {
if (!(instance instanceof Constructor)) {
throw new TypeError("Cannot call a class as a function");
}
}
function _defineProperties(target, props) {
for (var i = 0; i < props.length; i++) {
var descriptor = props[i];
descriptor.enumerable = descriptor.enumerable || false;
descriptor.configurable = true;
if ("value" in descriptor) descriptor.writable = true;
Object.defineProperty(target, descriptor.key, descriptor);
}
}
function _createClass(Constructor, protoProps, staticProps) {
if (protoProps) _defineProperties(Constructor.prototype, protoProps);
if (staticProps) _defineProperties(Constructor, staticProps);
return Constructor;
}
function _defineProperty(obj, key, value) {
if (key in obj) {
Object.defineProperty(obj, key, {
value: value,
enumerable: true,
configurable: true,
writable: true
});
} else {
obj[key] = value;
}
return obj;
}
function _extends() {
_extends = Object.assign || function (target) {
for (var i = 1; i < arguments.length; i++) {
var source = arguments[i];
for (var key in source) {
if (Object.prototype.hasOwnProperty.call(source, key)) {
target[key] = source[key];
}
}
}
return target;
};
return _extends.apply(this, arguments);
}
function _objectSpread(target) {
for (var i = 1; i < arguments.length; i++) {
var source = arguments[i] != null ? arguments[i] : {};
var ownKeys = Object.keys(source);
if (typeof Object.getOwnPropertySymbols === 'function') {
ownKeys = ownKeys.concat(Object.getOwnPropertySymbols(source).filter(function (sym) {
return Object.getOwnPropertyDescriptor(source, sym).enumerable;
}));
}
ownKeys.forEach(function (key) {
_defineProperty(target, key, source[key]);
});
}
return target;
}
function _inherits(subClass, superClass) {
if (typeof superClass !== "function" && superClass !== null) {
throw new TypeError("Super expression must either be null or a function");
}
subClass.prototype = Object.create(superClass && superClass.prototype, {
constructor: {
value: subClass,
writable: true,
configurable: true
}
});
if (superClass) _setPrototypeOf(subClass, superClass);
}
function _getPrototypeOf(o) {
_getPrototypeOf = Object.setPrototypeOf ? Object.getPrototypeOf : function _getPrototypeOf(o) {
return o.__proto__ || Object.getPrototypeOf(o);
};
return _getPrototypeOf(o);
}
function _setPrototypeOf(o, p) {
_setPrototypeOf = Object.setPrototypeOf || function _setPrototypeOf(o, p) {
o.__proto__ = p;
return o;
};
return _setPrototypeOf(o, p);
}
function _assertThisInitialized(self) {
if (self === void 0) {
throw new ReferenceError("this hasn't been initialised - super() hasn't been called");
}
return self;
}
function _possibleConstructorReturn(self, call) {
if (call && (typeof call === "object" || typeof call === "function")) {
return call;
}
return _assertThisInitialized(self);
}
var style = {
display: 'inline-block',
borderRadius: '50%',
border: '5px double white',
width: 30,
height: 30
};
var Style = {
empty: _objectSpread({}, style, {
backgroundColor: '#ccc'
}),
full: _objectSpread({}, style, {
backgroundColor: 'black'
}),
placeholder: _objectSpread({}, style, {
backgroundColor: 'red'
})
};
// Return the corresponding React node for an icon.
var _iconNode = function _iconNode(icon) {
// If it is already a React Element just return it.
if (React.isValidElement(icon)) {
return icon;
} // If it is an object, try to use it as a CSS style object.
if (_typeof(icon) === 'object' && icon !== null) {
return React.createElement("span", {
style: icon
});
} // If it is a string, use it as class names.
if (Object.prototype.toString.call(icon) === '[object String]') {
return React.createElement("span", {
className: icon
});
}
};
var RatingSymbol =
/*#__PURE__*/
function (_React$PureComponent) {
_inherits(RatingSymbol, _React$PureComponent);
function RatingSymbol() {
_classCallCheck(this, RatingSymbol);
return _possibleConstructorReturn(this, _getPrototypeOf(RatingSymbol).apply(this, arguments));
}
_createClass(RatingSymbol, [{
key: "render",
value: function render() {
var _iconContainerStyle;
var _this$props = this.props,
index = _this$props.index,
inactiveIcon = _this$props.inactiveIcon,
activeIcon = _this$props.activeIcon,
percent = _this$props.percent,
direction = _this$props.direction,
readonly = _this$props.readonly,
onClick = _this$props.onClick,
onMouseMove = _this$props.onMouseMove;
var backgroundNode = _iconNode(inactiveIcon);
var showbgIcon = percent < 100;
var bgIconContainerStyle = showbgIcon ? {} : {
visibility: 'hidden'
};
var iconNode = _iconNode(activeIcon);
var iconContainerStyle = (_iconContainerStyle = {
display: 'inline-block',
position: 'absolute',
overflow: 'hidden',
top: 0
}, _defineProperty(_iconContainerStyle, direction === 'rtl' ? 'right' : 'left', 0), _defineProperty(_iconContainerStyle, "width", "".concat(percent, "%")), _iconContainerStyle);
var style = {
cursor: !readonly ? 'pointer' : 'inherit',
display: 'inline-block',
position: 'relative'
};
function handleMouseMove(e) {
if (onMouseMove) {
onMouseMove(index, e);
}
}
function handleMouseClick(e) {
if (onClick) {
// [Supporting both TouchEvent and MouseEvent](https://developer.mozilla.org/en-US/docs/Web/API/Touch_events/Supporting_both_TouchEvent_and_MouseEvent)
// We must prevent firing click event twice on touch devices.
e.preventDefault();
onClick(index, e);
}
}
return React.createElement("span", {
style: style,
onClick: handleMouseClick,
onMouseMove: handleMouseMove,
onTouchMove: handleMouseMove,
onTouchEnd: handleMouseClick
}, React.createElement("span", {
style: bgIconContainerStyle
}, backgroundNode), React.createElement("span", {
style: iconContainerStyle
}, iconNode));
}
}]);
return RatingSymbol;
}(React.PureComponent); // Define propTypes only in development. They will be void in production.
var Rating =
/*#__PURE__*/
function (_React$PureComponent) {
_inherits(Rating, _React$PureComponent);
function Rating(props) {
var _this;
_classCallCheck(this, Rating);
_this = _possibleConstructorReturn(this, _getPrototypeOf(Rating).call(this, props));
_this.state = {
// Indicates the value that is displayed to the user in the form of symbols.
// It can be either 0 (for no displayed symbols) or (0, end]
displayValue: _this.props.value,
// Indicates if the user is currently hovering over the rating element
interacting: false
};
_this.onMouseLeave = _this.onMouseLeave.bind(_assertThisInitialized(_assertThisInitialized(_this)));
_this.symbolMouseMove = _this.symbolMouseMove.bind(_assertThisInitialized(_assertThisInitialized(_this)));
_this.symbolClick = _this.symbolClick.bind(_assertThisInitialized(_assertThisInitialized(_this)));
return _this;
}
_createClass(Rating, [{
key: "UNSAFE_componentWillReceiveProps",
value: function UNSAFE_componentWillReceiveProps(nextProps) {
var valueChanged = this.props.value !== nextProps.value;
this.setState(function (prevState) {
return {
displayValue: valueChanged ? nextProps.value : prevState.displayValue
};
});
}
}, {
key: "componentDidUpdate",
value: function componentDidUpdate(prevProps, prevState) {
// Ignore state update due to value changed from props.
// Usually originated through an onClick event.
if (prevProps.value !== this.props.value) {
return;
} // When hover ends, call this.props.onHover with no value.
if (prevState.interacting && !this.state.interacting) {
return this.props.onHover();
} // When hover over.
if (this.state.interacting) {
this.props.onHover(this.state.displayValue);
}
}
}, {
key: "symbolClick",
value: function symbolClick(symbolIndex, event) {
var value = this.calculateDisplayValue(symbolIndex, event);
this.props.onClick(value, event);
}
}, {
key: "symbolMouseMove",
value: function symbolMouseMove(symbolIndex, event) {
var value = this.calculateDisplayValue(symbolIndex, event); // This call should cause an update only if the state changes.
// Mainly the first time the mouse enters and whenever the value changes.
// So DidComponentUpdate is NOT called for every mouse movement.
this.setState({
interacting: !this.props.readonly,
displayValue: value
});
}
}, {
key: "onMouseLeave",
value: function onMouseLeave() {
this.setState({
displayValue: this.props.value,
interacting: false
});
}
}, {
key: "calculateDisplayValue",
value: function calculateDisplayValue(symbolIndex, event) {
var percentage = this.calculateHoverPercentage(event); // Get the closest top fraction.
var fraction = Math.ceil(percentage % 1 * this.props.fractions) / this.props.fractions; // Truncate decimal trying to avoid float precission issues.
var precision = Math.pow(10, 3);
var displayValue = symbolIndex + (Math.floor(percentage) + Math.floor(fraction * precision) / precision); // ensure the returned value is greater than 0 and lower than totalSymbols
return displayValue > 0 ? displayValue > this.props.totalSymbols ? this.props.totalSymbols : displayValue : 1 / this.props.fractions;
}
}, {
key: "calculateHoverPercentage",
value: function calculateHoverPercentage(event) {
var clientX = event.nativeEvent.type.indexOf("touch") > -1 ? event.nativeEvent.type.indexOf("touchend") > -1 ? event.changedTouches[0].clientX : event.touches[0].clientX : event.clientX;
var targetRect = event.target.getBoundingClientRect();
var delta = this.props.direction === 'rtl' ? targetRect.right - clientX : clientX - targetRect.left; // Returning 0 if the delta is negative solves the flickering issue
return delta < 0 ? 0 : delta / targetRect.width;
}
}, {
key: "render",
value: function render() {
var _this$props = this.props,
readonly = _this$props.readonly,
quiet = _this$props.quiet,
totalSymbols = _this$props.totalSymbols,
value = _this$props.value,
placeholderValue = _this$props.placeholderValue,
direction = _this$props.direction,
emptySymbol = _this$props.emptySymbol,
fullSymbol = _this$props.fullSymbol,
placeholderSymbol = _this$props.placeholderSymbol,
className = _this$props.className,
id = _this$props.id,
style = _this$props.style,
tabIndex = _this$props.tabIndex;
var _this$state = this.state,
displayValue = _this$state.displayValue,
interacting = _this$state.interacting;
var symbolNodes = [];
var empty = [].concat(emptySymbol);
var full = [].concat(fullSymbol);
var placeholder = [].concat(placeholderSymbol);
var shouldDisplayPlaceholder = placeholderValue !== 0 && value === 0 && !interacting; // The value that will be used as base for calculating how to render the symbols
var renderedValue;
if (shouldDisplayPlaceholder) {
renderedValue = placeholderValue;
} else {
renderedValue = quiet ? value : displayValue;
} // The amount of full symbols
var fullSymbols = Math.floor(renderedValue);
for (var i = 0; i < totalSymbols; i++) {
var percent = void 0; // Calculate each symbol's fullness percentage
if (i - fullSymbols < 0) {
percent = 100;
} else if (i - fullSymbols === 0) {
percent = (renderedValue - i) * 100;
} else {
percent = 0;
}
symbolNodes.push(React.createElement(RatingSymbol, _extends({
key: i,
index: i,
readonly: readonly,
inactiveIcon: empty[i % empty.length],
activeIcon: shouldDisplayPlaceholder ? placeholder[i % full.length] : full[i % full.length],
percent: percent,
direction: direction
}, !readonly && {
onClick: this.symbolClick,
onMouseMove: this.symbolMouseMove,
onTouchMove: this.symbolMouseMove,
onTouchEnd: this.symbolClick
})));
}
return React.createElement("span", _extends({
id: id,
style: _objectSpread({}, style, {
display: 'inline-block',
direction: direction
}),
className: className,
tabIndex: tabIndex,
"aria-label": this.props['aria-label']
}, !readonly && {
onMouseLeave: this.onMouseLeave
}), symbolNodes);
}
}]);
return Rating;
}(React.PureComponent); // Define propTypes only in development.
function noop() {}
noop._name = 'react_rating_noop';
var RatingAPILayer =
/*#__PURE__*/
function (_React$PureComponent) {
_inherits(RatingAPILayer, _React$PureComponent);
function RatingAPILayer(props) {
var _this;
_classCallCheck(this, RatingAPILayer);
_this = _possibleConstructorReturn(this, _getPrototypeOf(RatingAPILayer).call(this, props));
_this.state = {
value: props.initialRating
};
_this.handleClick = _this.handleClick.bind(_assertThisInitialized(_assertThisInitialized(_this)));
_this.handleHover = _this.handleHover.bind(_assertThisInitialized(_assertThisInitialized(_this)));
return _this;
}
_createClass(RatingAPILayer, [{
key: "UNSAFE_componentWillReceiveProps",
value: function UNSAFE_componentWillReceiveProps(nextProps) {
this.setState({
value: nextProps.initialRating
});
}
}, {
key: "handleClick",
value: function handleClick(value, e) {
var _this2 = this;
var newValue = this.translateDisplayValueToValue(value);
this.props.onClick(newValue); // Avoid calling setState if not necessary. Micro optimisation.
if (this.state.value !== newValue) {
// If we have a new value trigger onChange callback.
this.setState({
value: newValue
}, function () {
return _this2.props.onChange(_this2.state.value);
});
}
}
}, {
key: "handleHover",
value: function handleHover(displayValue) {
var value = displayValue === undefined ? displayValue : this.translateDisplayValueToValue(displayValue);
this.props.onHover(value);
}
}, {
key: "translateDisplayValueToValue",
value: function translateDisplayValueToValue(displayValue) {
var translatedValue = displayValue * this.props.step + this.props.start; // minimum value cannot be equal to start, since it's exclusive
return translatedValue === this.props.start ? translatedValue + 1 / this.props.fractions : translatedValue;
}
}, {
key: "tranlateValueToDisplayValue",
value: function tranlateValueToDisplayValue(value) {
if (value === undefined) {
return 0;
}
return (value - this.props.start) / this.props.step;
}
}, {
key: "render",
value: function render() {
var _this$props = this.props,
step = _this$props.step,
emptySymbol = _this$props.emptySymbol,
fullSymbol = _this$props.fullSymbol,
placeholderSymbol = _this$props.placeholderSymbol,
readonly = _this$props.readonly,
quiet = _this$props.quiet,
fractions = _this$props.fractions,
direction = _this$props.direction,
start = _this$props.start,
stop = _this$props.stop,
id = _this$props.id,
className = _this$props.className,
style = _this$props.style,
tabIndex = _this$props.tabIndex;
function calculateTotalSymbols(start, stop, step) {
return Math.floor((stop - start) / step);
}
return React.createElement(Rating, {
id: id,
style: style,
className: className,
tabIndex: tabIndex,
"aria-label": this.props['aria-label'],
totalSymbols: calculateTotalSymbols(start, stop, step),
value: this.tranlateValueToDisplayValue(this.state.value),
placeholderValue: this.tranlateValueToDisplayValue(this.props.placeholderRating),
readonly: readonly,
quiet: quiet,
fractions: fractions,
direction: direction,
emptySymbol: emptySymbol,
fullSymbol: fullSymbol,
placeholderSymbol: placeholderSymbol,
onClick: this.handleClick,
onHover: this.handleHover
});
}
}]);
return RatingAPILayer;
}(React.PureComponent);
RatingAPILayer.defaultProps = {
start: 0,
stop: 5,
step: 1,
readonly: false,
quiet: false,
fractions: 1,
direction: 'ltr',
onHover: noop,
onClick: noop,
onChange: noop,
emptySymbol: Style.empty,
fullSymbol: Style.full,
placeholderSymbol: Style.placeholder
}; // Define propTypes only in development.
return RatingAPILayer;
}));
//# sourceMappingURL=react-rating.umd.js.map