import { useState, useEffect, useCallback, useContext } from 'react'; import { useWixPatternsContainer } from '@wix/bex-core/react'; import { NumberRangeFilterProps } from './NumberRangeFilter'; import { NumberInputProps } from '@wix/design-system'; import type { Translate } from '@wix/bex-core'; import { ToolbarCollectionContext } from '../ToolbarCollectionContext'; function getValidateAndFormatNumber({ decimal, percision, min, max, translate, }: { decimal: boolean; percision: number; min?: number; max?: number; translate: Translate; }) { const validateAndFormatNumber = ( value?: number | null, ): { value: number | null; error: string | null } => { if (!value && value !== 0) { return { value: null, error: null }; } let error = null; const num = decimal ? parseFloat(value.toString()) : value; if (isNaN(num)) { return { value: null, error }; } if (min !== undefined && num < min) { error = translate('cairo.filtersPanel.numRange.minRange.consumer.error', { minAmount: min, }); return { value: min, error }; } if (max !== undefined && num > max) { error = translate('cairo.filtersPanel.numRange.maxRange.consumer.error', { maxAmount: max, }); return { value: max, error }; } return decimal ? { value: Number(num.toFixed(percision)), error } : { value: num, error }; }; return { validateAndFormatNumber }; } function useInputState(initialValue?: number | null) { const [value, setValue] = useState(initialValue ?? null); const [error, setError] = useState(null); return { value, setValue, error, setError }; } function useDebounce void>( callback: T, delay: number, ) { const container = useWixPatternsContainer(); const debouncedCallback = useCallback( container.lodash.debounce(callback, delay), [callback, delay], ); return debouncedCallback; } export function useNumberRangeFilter({ filter, decimal = false, min, max, }: NumberRangeFilterProps) { const { translate: t } = useWixPatternsContainer(); const percision = decimal ? 2 : 0; const { collection } = useContext(ToolbarCollectionContext) ?? {}; const { value: fromValue, setValue: setFromValue, error: fromError, setError: setFromError, } = useInputState(filter.value.from); const { value: toValue, setValue: setToValue, error: toError, setError: setToError, } = useInputState(filter.value.to); const step = decimal ? 0.1 : 1; const handleInputChange = ({ from, to, isFrom = true, }: { from?: number | null; to?: number | null; isFrom: boolean; }) => { if (isFrom) { setFromValue(from ?? null); } else { setToValue(to ?? null); } }; const updateFilterValue = ({ from, to, }: { from?: number | null; to?: number | null; }) => { const { validateAndFormatNumber } = getValidateAndFormatNumber({ decimal, percision, min, max, translate: t, }); const { value: validatedFrom, error: fromError } = validateAndFormatNumber(from); const { value: validatedTo, error: toError } = validateAndFormatNumber(to); setFromError(null); setToError(null); if (fromError) { setFromError(fromError); return; } if (toError) { setToError(toError); return; } if ( validatedFrom !== null && validatedTo !== null && validatedFrom > validatedTo ) { // Only set error for the most recently changed value if (fromValue !== filter.value?.from) { setFromError(t('cairo.filtersPanel.numRange.minRange.error')); } else { setToError(t('cairo.filtersPanel.numRange.maxRange.error')); } return; } if (validatedFrom !== fromValue) { setFromValue(validatedFrom); } if (validatedTo !== toValue) { setToValue(validatedTo); } filter.changeValue( { from: validatedFrom, to: validatedTo }, { emitEvents: ['beforeRefresh', 'refresh'], clickedValueKey: filter.itemKey({ from: validatedFrom, to: validatedTo, }), }, ); }; const debouncedUpdateFilters = useDebounce(updateFilterValue, 500); useEffect(() => { const from = typeof fromValue === 'number' ? parseFloat(fromValue.toFixed(percision)) : null; const to = typeof toValue === 'number' ? parseFloat(toValue.toFixed(percision)) : null; debouncedUpdateFilters({ from, to }); return () => { debouncedUpdateFilters.cancel(); }; }, [fromValue, toValue]); const onBeforeClearFilters = useCallback(() => { setFromValue(null); setToValue(null); }, [setFromValue, setToValue]); useEffect(() => { if (filter.value.from === null && filter.value.to === null) { onBeforeClearFilters(); } }, [filter.value.from, filter.value.to, onBeforeClearFilters]); useEffect(() => { if (!collection) { return; } // This is needed because we manage the filter value manually in order to have debounced filter updates collection.emitter.on('beforeClearFilters', onBeforeClearFilters); return () => { collection.emitter.off('beforeClearFilters', onBeforeClearFilters); }; }, [collection?.emitter, onBeforeClearFilters]); const commonInputProps: Partial = { step, min, max, }; return { fromValue, toValue, handleInputChange, placeholder: { min: t('cairo.filtersPanel.numRange.minRange-from.placeholder'), max: t('cairo.filtersPanel.numRange.maxRange-to.placeholder'), }, commonInputProps, fromError, toError, t, }; }