import React, { useCallback, useEffect, useRef, useState } from "react"; import { View, StyleSheet, Dimensions, LayoutChangeEvent, ViewStyle, I18nManager, } from "react-native"; import MultiSlider from "@ptomasroos/react-native-multi-slider"; import CustomInput from "../Input/Input"; import { ThemeType, useTheme } from "../../theme"; const { width: SCREEN_WIDTH } = Dimensions.get("window"); const HORIZONTAL_PADDING = 40; interface PointsRangeSelectorProps { minLimit?: number; maxLimit?: number; initialMin?: number; initialMax?: number; resetKey?: number; isRTL?: boolean; onChange?: (min: number, max: number) => void; containerStyle?: ViewStyle; inputsContainerStyle?: ViewStyle; customInputContainerStyle?: ViewStyle; customInputFieldStyle?: ViewStyle; sliderContainerStyle?: ViewStyle; multiSliderContainerStyle?: ViewStyle; trackStyle?: ViewStyle; selectedTrackStyle?: ViewStyle; unselectedTrackStyle?: ViewStyle; markerStyle?: ViewStyle; markerEnabledStyle?: ViewStyle; markerDisabledStyle?: ViewStyle; markerDotStyle?: ViewStyle; inputProps?: any; multiSliderProps?: any; } const sanitizeNumericInput = (text: string) => { const sanitized = text.replace(/[^0-9.]/g, ""); const parts = sanitized.split("."); if (parts.length <= 2) { return sanitized; } return `${parts[0]}.${parts.slice(1).join("")}`; }; const PointsRangeSelector: React.FC = ({ minLimit = 0, maxLimit = 1000, initialMin = 50, initialMax = 500, resetKey, isRTL = I18nManager.isRTL, onChange, containerStyle, inputsContainerStyle, customInputContainerStyle, customInputFieldStyle, sliderContainerStyle, multiSliderContainerStyle, trackStyle, selectedTrackStyle, unselectedTrackStyle, markerStyle, markerEnabledStyle, markerDisabledStyle, markerDotStyle, inputProps = {}, multiSliderProps = {}, }) => { const { theme, isRTL: rtl } = useTheme(); isRTL = rtl || isRTL; const styles = createStyles(theme, isRTL); const sliderStep = multiSliderProps.step ?? 10; const [values, setValues] = useState<[number, number]>([ initialMin, initialMax, ]); const valuesRef = useRef<[number, number]>([initialMin, initialMax]); const [minInputValue, setMinInputValue] = useState(initialMin.toString()); const [maxInputValue, setMaxInputValue] = useState(initialMax.toString()); const [containerWidth, setContainerWidth] = useState( SCREEN_WIDTH - HORIZONTAL_PADDING, ); const setRange = useCallback( (nextValues: [number, number], syncInputs = true) => { valuesRef.current = nextValues; setValues(nextValues); if (syncInputs) { setMinInputValue(nextValues[0].toString()); setMaxInputValue(nextValues[1].toString()); } onChange?.(nextValues[0], nextValues[1]); }, [onChange], ); const onContainerLayout = useCallback((event: LayoutChangeEvent) => { const { width } = event.nativeEvent.layout; setContainerWidth(width); }, []); useEffect(() => { setRange([initialMin, initialMax]); }, [resetKey, initialMin, initialMax, setRange]); const handleValuesChange = useCallback( (newValues: number[]) => { setRange([newValues[0], newValues[1]]); }, [setRange], ); const handleMinInputChange = useCallback( (text: string) => { const sanitized = sanitizeNumericInput(text); setMinInputValue(sanitized); const newMin = Number(sanitized); if (!sanitized || Number.isNaN(newMin)) return; const [, currentMax] = valuesRef.current; if (newMin < minLimit || newMin >= currentMax) return; setRange([newMin, currentMax], false); }, [minLimit, setRange], ); const handleMaxInputChange = useCallback( (text: string) => { const sanitized = sanitizeNumericInput(text); setMaxInputValue(sanitized); const newMax = Number(sanitized); if (!sanitized || Number.isNaN(newMax)) return; const [currentMin] = valuesRef.current; if (newMax > maxLimit || newMax <= currentMin) return; setRange([currentMin, newMax], false); }, [maxLimit, setRange], ); const handleMinInputBlur = useCallback(() => { const [currentMin] = valuesRef.current; setMinInputValue(currentMin.toString()); }, []); const handleMaxInputBlur = useCallback(() => { const [, currentMax] = valuesRef.current; setMaxInputValue(currentMax.toString()); }, []); const CustomMarker = ({ enabled }: any) => ( {enabled && } ); const renderInputs = () => { const minInput = ( ); const maxInput = ( ); return isRTL ? ( <> {maxInput} {minInput} ) : ( <> {minInput} {maxInput} ); }; return ( {renderInputs()} ); }; const createStyles = (theme: ThemeType, isRTL: boolean) => StyleSheet.create({ container: {}, inputsContainer: { flexDirection: "row", justifyContent: "space-between", marginBottom: 20, }, customInputContainer: { width: "48%", }, customInputField: { minHeight: 40, textAlign: isRTL ? "right" : "left", }, sliderContainer: { marginVertical: 10, alignItems: "center", width: "100%", }, sliderContainerStyle: { height: 30, width: "100%", }, track: { height: 4, borderRadius: 2, }, selectedTrack: { backgroundColor: theme.primary, }, unselectedTrack: { backgroundColor: theme.border, }, marker: { width: 20, height: 20, borderRadius: 12, backgroundColor: theme.surface, borderWidth: 2, shadowColor: "#000", shadowOffset: { width: 0, height: 2, }, shadowOpacity: 0.25, shadowRadius: 2.84, elevation: 3, alignItems: "center", justifyContent: "center", transform: isRTL ? [{ scaleX: -1 }] : [], }, markerEnabled: { borderColor: theme.primary, }, markerDisabled: { borderColor: theme.disabled, }, markerDot: { width: 18, height: 18, borderRadius: 12, backgroundColor: theme.primary, }, }); export default PointsRangeSelector;