@aashu-dubey/react-native-rating-bar
Version:
A React Native component for generating and displaying interactive Tap or Swipe enabled Ratings.
404 lines (380 loc) • 17.3 kB
JavaScript
;
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.js"));
var _NoRatingElement = _interopRequireDefault(require("./components/NoRatingElement.js"));
var _cloneElement = require("./helper/cloneElement.js");
var _utils = require("./helper/utils.js");
var _styles = require("./theme/styles.js");
var _jsxRuntime = require("react/jsx-runtime");
function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
function _getRequireWildcardCache(e) { if ("function" != typeof WeakMap) return null; var r = new WeakMap(), t = new WeakMap(); return (_getRequireWildcardCache = function (e) { return e ? t : r; })(e); }
function _interopRequireWildcard(e, r) { if (!r && e && e.__esModule) return e; if (null === e || "object" != typeof e && "function" != typeof e) return { default: e }; var t = _getRequireWildcardCache(r); if (t && t.has(e)) return t.get(e); var n = { __proto__: null }, a = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var u in e) if ("default" !== u && {}.hasOwnProperty.call(e, u)) { var i = a ? Object.getOwnPropertyDescriptor(e, u) : null; i && (i.get || i.set) ? Object.defineProperty(n, u, i) : n[u] = e[u]; } return n.default = e, t && t.set(e, n), n; }
const DISABLED_COLOR = 'rgba(0, 0, 0, 0.38)';
const RatingBar = ({
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
}) => {
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?.full,
half: ratingElement?.half,
empty: 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
},
// Conditionally adding the style was causing improper behaviour with Pan Gesture, so show/hide(ing) glow conditionally here
shadowOpacity: shouldGlow ? 0.5 : 0,
shadowRadius: 10
};
const innerShadow = {
...shadow,
shadowOpacity: 0.4
};
// 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?.empty, itemSize);
const rateHalfCloned = (0, _cloneElement.getClonedElement)(ratingElement?.half, itemSize);
const rateFullCloned = (0, _cloneElement.getClonedElement)(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__*/(0, _jsxRuntime.jsx)(_jsxRuntime.Fragment, {});
// 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__*/(0, _jsxRuntime.jsx)(_NoRatingElement.default, {
size: itemSize,
enableMask: ratingElement == null,
unratedColor: unratedColor,
children: rateEmptyCloned ?? 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?.half == null) {
ratingItem = /*#__PURE__*/(0, _jsxRuntime.jsx)(_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
,
children: clonedChild
});
} else {
// const rateHalfCloned = getClonedElement(ratingElement?.half, itemSize);
const rateHalfCloned = ratingElClones.half;
ratingItem = /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
style: (0, _styles.elementStyle)(itemSize).container,
children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
style: {
transform: [{
scaleX: isRTL ? -1 : 1
}]
},
children: rateHalfCloned ?? ratingElement?.half
})
});
}
} else {
// const rateFullCloned = getClonedElement(ratingElement?.full, itemSize);
const rateFullCloned = ratingElClones.full;
ratingItem = /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
style: (0, _styles.elementStyle)(itemSize).container,
children: rateFullCloned ?? ratingElement?.full ?? clonedChild
});
}
}
const panGesture = _reactNativeGestureHandler.Gesture.Pan().runOnJS(true).onStart(onGestureStart).onUpdate(e => onGestureMove(e, index)).onEnd(onGestureEnd);
const singleTap = _reactNativeGestureHandler.Gesture.Tap().runOnJS(true).maxDuration(250).onStart(e => onRatingTap(e, index));
return /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
style: [
// shouldGlow && shadow,
shadow, typeof rateStyles?.starContainer === 'function' ? rateStyles?.starContainer(index) : rateStyles?.starContainer],
pointerEvents: ignoreGestures ? 'none' : 'auto',
children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNativeGestureHandler.GestureDetector, {
gesture: _reactNativeGestureHandler.Gesture.Simultaneous(panGesture, singleTap),
children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
style: [shouldGlow && innerShadow, isVertical ? {
paddingVertical: itemPadding
} : {
paddingHorizontal: itemPadding
}],
children: useSolution === 1 ? ratingItem : /*#__PURE__*/(0, _jsxRuntime.jsx)(_NoRatingElement.default, {
size: itemSize,
enableMask: ratingElement == null,
unratedColor: unratedColor,
children: ratingElClones.empty ?? ratingElement?.empty ?? clonedChild
})
})
})
}, index);
};
// Solution 2 only
const ratingFillList = index => {
const clonedChild = itemClones[index];
let item = clonedChild;
if (index >= rating - ratingOffset && allowHalfRating) {
item = ratingElClones.half ?? ratingElement?.half ?? item;
} else if (index < rating) {
item = ratingElClones.full ?? ratingElement?.full ?? item;
}
return /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
style: [isVertical ? {
paddingVertical: itemPadding
} : {
paddingHorizontal: itemPadding
}],
children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
style: [(0, _styles.elementStyle)(itemSize).container],
children: item
})
}, `fill_${index}`);
};
// Rating component showing current rating / total rating (customizable by user using props)
const displayCurrentRating = () => {
return ratingView?.custom || /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
style: [styles.ratingTxtContainer, {
marginBottom: ratingView?.position === 'top' ? 8 : 0,
marginTop: ratingView?.position === 'bottom' ? 8 : 0
}, ratingView?.containerStyle],
children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
style: [{
fontSize: 16
}, ratingView?.titleStyle],
children: `${ratingView?.titleText || 'Rating:'} `
}), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
style: [{
fontSize: 20,
fontWeight: 'bold'
}, ratingView?.ratingStyle],
children: `${iconRating.current} `
}), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
style: [{
fontSize: 16
}, ratingView?.totalStyle],
children: `/ ${itemCount}`
})]
});
};
const flexDirection = isVerticalReverse ? 'column-reverse' : isVertical ? 'column' : androidRTL ? 'row-reverse' : 'row';
const renderRating = () => {
return /*#__PURE__*/(0, _jsxRuntime.jsxs)(_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
],
children: [ratingArr.map((_, i) => ratingList(i)), useSolution === 2 ? /*#__PURE__*/(0, _jsxRuntime.jsx)(_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',
children: ratingArr.map((_, i) => ratingFillList(i))
}) : null]
});
};
return /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
style: rateStyles?.container,
children: [ratingView?.showRating && 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
(0, _jsxRuntime.jsx)(_reactNative.View, {
style: {
alignItems: 'flex-start'
},
children: renderRating()
}), ratingView?.showRating && ratingView?.position === 'bottom' && displayCurrentRating()]
});
};
var _default = exports.default = RatingBar;
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