UNPKG

@ggmdev/react-native-rating-bar

Version:

A React Native component for generating and displaying interactive Tap or Swipe enabled Ratings.

390 lines (362 loc) 18.8 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.default = void 0; var _react = _interopRequireWildcard(require("react")); var _reactNative = require("react-native"); var _reactNativeGestureHandler = require("react-native-gesture-handler"); var _HalfRatingElement = _interopRequireDefault(require("./components/HalfRatingElement")); var _NoRatingElement = _interopRequireDefault(require("./components/NoRatingElement")); var _cloneElement = require("./helper/cloneElement"); var _utils = require("./helper/utils"); var _styles = require("./theme/styles"); function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } function _getRequireWildcardCache(nodeInterop) { if (typeof WeakMap !== "function") return null; var cacheBabelInterop = new WeakMap(); var cacheNodeInterop = new WeakMap(); return (_getRequireWildcardCache = function (nodeInterop) { return nodeInterop ? cacheNodeInterop : cacheBabelInterop; })(nodeInterop); } function _interopRequireWildcard(obj, nodeInterop) { if (!nodeInterop && obj && obj.__esModule) { return obj; } if (obj === null || typeof obj !== "object" && typeof obj !== "function") { return { default: obj }; } var cache = _getRequireWildcardCache(nodeInterop); if (cache && cache.has(obj)) { return cache.get(obj); } var newObj = {}; var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var key in obj) { if (key !== "default" && Object.prototype.hasOwnProperty.call(obj, key)) { var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null; if (desc && (desc.get || desc.set)) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } newObj.default = obj; if (cache) { cache.set(obj, newObj); } return newObj; } const DISABLED_COLOR = 'rgba(0, 0, 0, 0.38)'; const RatingBar = _ref => { let { rateStyles, onRatingUpdate, itemCount = 5, glowColor = '#FFC107', // change this default later minRating = 0, maxRating = itemCount, layoutDirection = _reactNative.I18nManager.isRTL ? 'rtl' : 'ltr', unratedColor = DISABLED_COLOR, allowHalfRating = false, direction = 'horizontal', glow = true, glowRadius = 2, ignoreGestures = false, initialRating = 0, itemPadding = 0, itemSize = 40, tapOnlyMode = false, updateOnDrag = false, ratingView, useSolution = 1, // wrapAlignment, ratingElement, itemBuilder } = _ref; const window = (0, _reactNative.useWindowDimensions)(); // Stores user's current rating const [rating, setRating] = (0, _react.useState)(initialRating); // isRTL:- if user passed layoutDirection='rtl' as value, if not then based on I18nManager.isRTL (whole app's direction) const [isRTL, setIsRTL] = (0, _react.useState)(layoutDirection === 'rtl'); // Whether to show Glow effect around Star (Rating Component) or not (iOS only) const [isGlow, setGlow] = (0, _react.useState)(false); /* Below two states are used to store clones of the components passed by the user, on intial render * to avoid unneccessary cloning them again and again on every render */ // This stores the clones of the components passed by user in itemBuilder function const [itemClones, setItemClones] = (0, _react.useState)([]); // This stores the clones of the components passed by user in ratingElement const [ratingElClones, setRatingElClones] = (0, _react.useState)({ full: ratingElement === null || ratingElement === void 0 ? void 0 : ratingElement.full, half: ratingElement === null || ratingElement === void 0 ? void 0 : ratingElement.half, empty: ratingElement === null || ratingElement === void 0 ? void 0 : ratingElement.empty }); // Solution 2 only const [containerWidth, setContainerWidth] = (0, _react.useState)((itemSize + itemPadding * 2) * (itemCount - initialRating)); // Stores exact rating like 1, 2... or 0.5, 1, 1.5 etc. contrary to rating state which contains exact fraction value const iconRating = (0, _react.useRef)(initialRating); const isVertical = ['vertical', 'vertical-reverse'].includes(direction); const isVerticalReverse = direction === 'vertical-reverse'; const ratingOffset = allowHalfRating ? 0.5 : 1.0; const ratingArr = (0, _react.useMemo)(() => [...Array(itemCount)], [itemCount]); const androidRTL = _utils.isAndroid && (isRTL && !_reactNative.I18nManager.isRTL || _reactNative.I18nManager.isRTL && !isRTL); // Rating shadow if glow === true const shouldGlow = glow && isGlow; const shadow = { shadowColor: glowColor, shadowOffset: { width: glowRadius, height: glowRadius }, shadowOpacity: 0.3, shadowRadius: 10 }; const innerShadow = { ...shadow, shadowOpacity: 0.2 }; // Solution 2 only (0, _react.useEffect)(() => { if (useSolution === 2) { const totalItemSize = itemSize + itemPadding * 2; // const totalWidth = totalItemSize * iconRating.current; // for width const totalWidth = totalItemSize * (itemCount - iconRating.current); // for positions setContainerWidth(totalWidth); } }, [rating, itemSize, itemPadding, itemCount, useSolution]); // Creating and storing clones for Elements passed in itemBuilder function prop (0, _react.useEffect)(() => { const clones = []; ratingArr.forEach((_, index) => { const item = itemBuilder && itemBuilder(index); clones.push((0, _cloneElement.getClonedElement)(item, itemSize)); }); setItemClones(clones); }, [itemBuilder, ratingArr, itemSize]); // Creating and storing clones for Elements passed in ratingElement prop (0, _react.useEffect)(() => { const rateEmptyCloned = (0, _cloneElement.getClonedElement)(ratingElement === null || ratingElement === void 0 ? void 0 : ratingElement.empty, itemSize); const rateHalfCloned = (0, _cloneElement.getClonedElement)(ratingElement === null || ratingElement === void 0 ? void 0 : ratingElement.half, itemSize); const rateFullCloned = (0, _cloneElement.getClonedElement)(ratingElement === null || ratingElement === void 0 ? void 0 : ratingElement.full, itemSize); setRatingElClones({ full: rateFullCloned, half: rateHalfCloned, empty: rateEmptyCloned }); }, [ratingElement, itemSize]); (0, _react.useEffect)(() => { setRating(initialRating); }, [initialRating]); (0, _react.useEffect)(() => { setIsRTL(layoutDirection === 'rtl'); }, [layoutDirection]); // Horizontal // touch position respective to whole screen width const getAbsoluteX = (0, _react.useCallback)(x => isRTL ? window.width - x : x, [isRTL, window.width]); // touch position respective to item's width const getX = (0, _react.useCallback)((x, totalWidth) => isRTL ? totalWidth - x : x, [isRTL]); // Vertical // touch position respective to whole screen height const getAbsoluteY = (0, _react.useCallback)(y => isVerticalReverse ? window.height - y : y, [isVerticalReverse, window.height]); // touch position respective to item's height const getY = (0, _react.useCallback)((y, totalHeight) => isVerticalReverse ? totalHeight - y : y, [isVerticalReverse]); // callback when user starts swiping const onGestureStart = () => { !_utils.isAndroid && glow && setGlow(true); }; // callback when user is swiping on rating const onGestureMove = (e, index) => { if (!tapOnlyMode) { const totalItemSize = itemSize + itemPadding * 2; // if (posDx >= 0) { let i = 0; if (!isVertical) { // const boxStartX = e.absoluteX - e.x - totalItemSize * index; const boxStartX = getAbsoluteX(e.absoluteX) - getX(e.x, totalItemSize) - totalItemSize * index; // const posDx = e.absoluteX - boxStartX; const posDx = getAbsoluteX(e.absoluteX) - boxStartX; i = posDx / totalItemSize; } else { // for vertical /* const boxStartY = e.absoluteY - e.y - totalItemSize * index; const posDy = e.absoluteY - boxStartY; */ const boxStartY = getAbsoluteY(e.absoluteY) - getY(e.y, totalItemSize) - totalItemSize * index; const posDy = getAbsoluteY(e.absoluteY) - boxStartY; i = posDy / totalItemSize; } let currentRating = allowHalfRating ? i : Math.round(i); if (currentRating > itemCount) { currentRating = itemCount; } if (currentRating < 0) { currentRating = 0; } const newRating = (0, _utils.clamp)(currentRating, minRating, maxRating); if (newRating !== rating) { setRating(newRating); const exactRating = Math.ceil(newRating * (1 / 0.5)) / (1 / 0.5); /* const rateValue = interval === 0 && fractions ? Number(newRating.toFixed(Math.min(Math.abs(fractions), 100))) : exactRating; */ iconRating.current = exactRating; updateOnDrag && onRatingUpdate && onRatingUpdate(exactRating); // TODO:- this here is slowing things down when drag fast, cause of double re-rendering, try to fix it. } } }; // when user swipe ends const onGestureEnd = () => { !_utils.isAndroid && glow && setGlow(false); onRatingUpdate && onRatingUpdate(iconRating.current); }; const onRatingTap = (e, index) => { let value = 0; if (index === 0 && (rating === 1 || rating === 0.5)) { value = 0; } else { const totalItemSize = itemSize + itemPadding * 2; const tappedPosition = getX(e.x, totalItemSize); const tappedOnFirstHalf = tappedPosition <= itemSize / 2; value = index + (tappedOnFirstHalf && allowHalfRating ? 0.5 : 1.0); // // for exact fraction // value = index + (tappedPosition / 58); // value = Math.ceil(value * (1 / 0.2)) / (1 / 0.2); } value = Math.max(value, minRating); iconRating.current = value; onRatingUpdate && onRatingUpdate(value); setRating(value); }; const ratingList = index => { // const item = itemBuilder && itemBuilder(index); let ratingItem = /*#__PURE__*/_react.default.createElement(_react.default.Fragment, null); // const clonedChild = getClonedElement(item, itemSize); const clonedChild = itemClones[index]; // Solution 1 only if (useSolution === 1) { if (index >= rating) { // const rateEmptyCloned = getClonedElement(ratingElement?.empty, itemSize); const rateEmptyCloned = ratingElClones.empty; ratingItem = /*#__PURE__*/_react.default.createElement(_NoRatingElement.default, { size: itemSize, enableMask: ratingElement == null, unratedColor: unratedColor }, rateEmptyCloned ?? (ratingElement === null || ratingElement === void 0 ? void 0 : ratingElement.empty) ?? clonedChild); } else if (index >= rating - ratingOffset && allowHalfRating) { // } else if ((index >= rating || rating < index + 1) && allowHalfRating) { // for exact fraction // const accurateRating = internal === 0 ? rating : iconRating.current; if ((ratingElement === null || ratingElement === void 0 ? void 0 : ratingElement.half) == null) { ratingItem = /*#__PURE__*/_react.default.createElement(_HalfRatingElement.default, { size: itemSize, enableMask: ratingElement == null, rtlMode: isRTL, unratedColor: unratedColor // fraction={rating - Math.trunc(rating)} // for exact fraction, value Ex. 2.34 = 0.34 }, clonedChild); } else { // const rateHalfCloned = getClonedElement(ratingElement?.half, itemSize); const rateHalfCloned = ratingElClones.half; ratingItem = /*#__PURE__*/_react.default.createElement(_reactNative.View, { style: (0, _styles.elementStyle)(itemSize).container }, /*#__PURE__*/_react.default.createElement(_reactNative.View, { style: { transform: [{ scaleX: isRTL ? -1 : 1 }] } }, rateHalfCloned ?? (ratingElement === null || ratingElement === void 0 ? void 0 : ratingElement.half))); } } else { // const rateFullCloned = getClonedElement(ratingElement?.full, itemSize); const rateFullCloned = ratingElClones.full; ratingItem = /*#__PURE__*/_react.default.createElement(_reactNative.View, { style: (0, _styles.elementStyle)(itemSize).container }, rateFullCloned ?? (ratingElement === null || ratingElement === void 0 ? void 0 : ratingElement.full) ?? clonedChild); } } const panGesture = _reactNativeGestureHandler.Gesture.Pan().onStart(onGestureStart).onUpdate(e => onGestureMove(e, index)).onEnd(onGestureEnd); // const singleTap = Gesture.Tap() // .maxDuration(250) // .onStart((e) => onRatingTap(e, index)); return /*#__PURE__*/_react.default.createElement(_reactNative.View, { key: index, style: [shouldGlow && shadow, typeof (rateStyles === null || rateStyles === void 0 ? void 0 : rateStyles.starContainer) === 'function' ? rateStyles === null || rateStyles === void 0 ? void 0 : rateStyles.starContainer(index) : rateStyles === null || rateStyles === void 0 ? void 0 : rateStyles.starContainer], pointerEvents: ignoreGestures ? 'none' : 'auto' }, /*#__PURE__*/_react.default.createElement(_reactNativeGestureHandler.GestureDetector, { gesture: _reactNativeGestureHandler.Gesture.Simultaneous(panGesture) }, /*#__PURE__*/_react.default.createElement(_reactNative.View, { style: [shouldGlow && innerShadow, isVertical ? { paddingVertical: itemPadding } : { paddingHorizontal: itemPadding }] }, useSolution === 1 ? ratingItem : /*#__PURE__*/_react.default.createElement(_NoRatingElement.default, { size: itemSize, enableMask: ratingElement == null, unratedColor: unratedColor }, ratingElClones.empty ?? (ratingElement === null || ratingElement === void 0 ? void 0 : ratingElement.empty) ?? clonedChild)))); }; // Solution 2 only const ratingFillList = index => { const clonedChild = itemClones[index]; let item = clonedChild; if (index >= rating - ratingOffset && allowHalfRating) { item = ratingElClones.half ?? (ratingElement === null || ratingElement === void 0 ? void 0 : ratingElement.half) ?? item; } else if (index < rating) { item = ratingElClones.full ?? (ratingElement === null || ratingElement === void 0 ? void 0 : ratingElement.full) ?? item; } return /*#__PURE__*/_react.default.createElement(_reactNative.View, { key: `fill_${index}`, style: [isVertical ? { paddingVertical: itemPadding } : { paddingHorizontal: itemPadding }] }, /*#__PURE__*/_react.default.createElement(_reactNative.View, { style: [(0, _styles.elementStyle)(itemSize).container] }, item)); }; // Rating component showing current rating / total rating (customizable by user using props) const displayCurrentRating = () => { return (ratingView === null || ratingView === void 0 ? void 0 : ratingView.custom) || /*#__PURE__*/_react.default.createElement(_reactNative.View, { style: [styles.ratingTxtContainer, { marginBottom: (ratingView === null || ratingView === void 0 ? void 0 : ratingView.position) === 'top' ? 8 : 0, marginTop: (ratingView === null || ratingView === void 0 ? void 0 : ratingView.position) === 'bottom' ? 8 : 0 }, ratingView === null || ratingView === void 0 ? void 0 : ratingView.containerStyle] }, /*#__PURE__*/_react.default.createElement(_reactNative.Text, { style: [{ fontSize: 16 }, ratingView === null || ratingView === void 0 ? void 0 : ratingView.titleStyle] }, `${(ratingView === null || ratingView === void 0 ? void 0 : ratingView.titleText) || 'Rating:'} `), /*#__PURE__*/_react.default.createElement(_reactNative.Text, { style: [{ fontSize: 20, fontWeight: 'bold' }, ratingView === null || ratingView === void 0 ? void 0 : ratingView.ratingStyle] }, `${iconRating.current} `), /*#__PURE__*/_react.default.createElement(_reactNative.Text, { style: [{ fontSize: 16 }, ratingView === null || ratingView === void 0 ? void 0 : ratingView.totalStyle] }, `/ ${itemCount}`)); }; const flexDirection = isVerticalReverse ? 'column-reverse' : isVertical ? 'column' : androidRTL ? 'row-reverse' : 'row'; const renderRating = () => { return /*#__PURE__*/_react.default.createElement(_reactNative.View, { style: [{ backgroundColor: 'transparent', // justifyContent: 'center', flexDirection, direction: isRTL ? 'rtl' : 'ltr' } // androidRTL && isVertical && { transform: [{ scaleX: -1 }] }, // for some reason is working without this condition on android ] }, ratingArr.map((_, i) => ratingList(i)), useSolution === 2 ? /*#__PURE__*/_react.default.createElement(_reactNative.View, { style: [styles.filledRatingContainer, { flexDirection, // width: containerWidth, right: isVertical ? 0 : containerWidth, top: isVertical && isVerticalReverse ? containerWidth : 0, bottom: isVertical && !isVerticalReverse ? containerWidth : 0 } // androidRTL && isVertical && { transform: [{ scaleX: -1 }] }, ], removeClippedSubviews: true, pointerEvents: 'none' }, ratingArr.map((_, i) => ratingFillList(i))) : null); }; return /*#__PURE__*/_react.default.createElement(_reactNative.View, { style: rateStyles === null || rateStyles === void 0 ? void 0 : rateStyles.container }, (ratingView === null || ratingView === void 0 ? void 0 : ratingView.showRating) && (ratingView === null || ratingView === void 0 ? void 0 : ratingView.position) !== 'bottom' && displayCurrentRating(), useSolution === 1 ? renderRating() : /*#__PURE__*/ // ratingFillList()'s component fills whole screen's width, making rating effect inconsistent // so here 'flex-start' align it at the start in par with renderRating()'s component _react.default.createElement(_reactNative.View, { style: { alignItems: 'flex-start' } }, renderRating()), (ratingView === null || ratingView === void 0 ? void 0 : ratingView.showRating) && (ratingView === null || ratingView === void 0 ? void 0 : ratingView.position) === 'bottom' && displayCurrentRating()); }; var _default = RatingBar; exports.default = _default; const styles = _reactNative.StyleSheet.create({ ratingTxtContainer: { flexDirection: 'row', alignSelf: 'center', alignItems: 'center' }, // Solution 2 filledRatingContainer: { position: 'absolute', backgroundColor: 'transparent', overflow: 'hidden', left: 0 } }); //# sourceMappingURL=RatingBar.js.map