import { type ChangeEvent, type MouseEvent, SyntheticEvent, useCallback, useEffect, useRef, useState, } from 'react'; import Autocomplete, { AutocompleteInputChangeReason } from '@mui/material/Autocomplete'; import Box from '@mui/material/Box'; import Checkbox from '@mui/material/Checkbox'; import Chip from '@mui/material/Chip'; import IconButton from '@mui/material/IconButton'; import InputAdornment from '@mui/material/InputAdornment'; import MenuItem from '@mui/material/MenuItem'; import TextField, { type TextFieldProps } from '@mui/material/TextField'; import Tooltip from '@mui/material/Tooltip'; import { debounce } from '@mui/material/utils'; import { DatePicker } from '@mui/x-date-pickers/DatePicker'; import { DateTimePicker } from '@mui/x-date-pickers/DateTimePicker'; import { TimePicker } from '@mui/x-date-pickers/TimePicker'; import { type DropdownOption, type MRT_Header, type MRT_RowData, type MRT_TableInstance, } from '../../types'; import { getColumnFilterInfo, useDropdownOptions, } from '../../utils/column.utils'; import { getValueAndLabel, parseFromValuesOrFunc } from '../../utils/utils'; import { MRT_FilterOptionMenu } from '../menus/MRT_FilterOptionMenu'; export interface MRT_FilterTextFieldProps extends TextFieldProps<'standard'> { header: MRT_Header; rangeFilterIndex?: number; table: MRT_TableInstance; } export const MRT_FilterTextField = ({ header, rangeFilterIndex, table, ...rest }: MRT_FilterTextFieldProps) => { const { options: { enableColumnFilterModes, icons: { CloseIcon, FilterListIcon }, localization, manualFiltering, muiFilterAutocompleteProps, muiFilterDatePickerProps, muiFilterDateTimePickerProps, muiFilterTextFieldProps, muiFilterTimePickerProps, }, refs: { filterInputRefs }, setColumnFilterFns, } = table; const { column } = header; const { columnDef } = column; const { filterVariant } = columnDef; const args = { column, rangeFilterIndex, table }; const textFieldProps = { ...parseFromValuesOrFunc(muiFilterTextFieldProps, args), ...parseFromValuesOrFunc(columnDef.muiFilterTextFieldProps, args), ...rest, }; const autocompleteProps = { ...parseFromValuesOrFunc(muiFilterAutocompleteProps, args), ...parseFromValuesOrFunc(columnDef.muiFilterAutocompleteProps, args), }; const datePickerProps = { ...parseFromValuesOrFunc(muiFilterDatePickerProps, args), ...parseFromValuesOrFunc(columnDef.muiFilterDatePickerProps, args), } as any; const dateTimePickerProps = { ...parseFromValuesOrFunc(muiFilterDateTimePickerProps, args), ...parseFromValuesOrFunc(columnDef.muiFilterDateTimePickerProps, args), } as any; const timePickerProps = { ...parseFromValuesOrFunc(muiFilterTimePickerProps, args), ...parseFromValuesOrFunc(columnDef.muiFilterTimePickerProps, args), } as any; const { allowedColumnFilterOptions, currentFilterOption, facetedUniqueValues, isAutocompleteFilter, isDateFilter, isMultiSelectFilter, isRangeFilter, isSelectFilter, isTextboxFilter, } = getColumnFilterInfo({ header, table }); const dropdownOptions = useDropdownOptions({ header, table }); const filterChipLabel = ['empty', 'notEmpty'].includes(currentFilterOption) ? localization[ `filter${ currentFilterOption?.charAt?.(0)?.toUpperCase() + currentFilterOption?.slice(1) }` as keyof typeof localization ] : ''; const filterPlaceholder = !isRangeFilter ? (textFieldProps?.placeholder ?? localization.filterByColumn?.replace( '{column}', String(columnDef.header), )) : rangeFilterIndex === 0 ? localization.min : rangeFilterIndex === 1 ? localization.max : ''; const showChangeModeButton = !!( enableColumnFilterModes && columnDef.enableColumnFilterModes !== false && !rangeFilterIndex && (allowedColumnFilterOptions === undefined || !!allowedColumnFilterOptions?.length) ); const [anchorEl, setAnchorEl] = useState(null); const [filterValue, setFilterValue] = useState(() => isMultiSelectFilter ? (column.getFilterValue() as string[]) || [] : isRangeFilter ? (column.getFilterValue() as [string, string])?.[ rangeFilterIndex as number ] || '' : isAutocompleteFilter ? typeof column.getFilterValue() === 'string' ? column.getFilterValue() as string : '' : ((column.getFilterValue() as string) ?? ''), ); const [autocompleteValue, setAutocompleteValue] = useState( () => isAutocompleteFilter ? ((column.getFilterValue() || null) as DropdownOption | null) : null, ); const handleChangeDebounced = useCallback( debounce( (newValue: any) => { if (isRangeFilter) { column.setFilterValue((old: Array) => { const newFilterValues = old ?? ['', '']; newFilterValues[rangeFilterIndex as number] = newValue ?? undefined; return newFilterValues; }); } else { column.setFilterValue(newValue ?? undefined); } }, isTextboxFilter ? (manualFiltering ? 400 : 200) : 1, ), [], ); const handleChange = (newValue: any) => { setFilterValue(newValue ?? ''); handleChangeDebounced(newValue); }; const handleTextFieldChange = (event: ChangeEvent) => { const newValue = textFieldProps.type === 'date' ? event.target.valueAsDate : textFieldProps.type === 'number' ? event.target.valueAsNumber : event.target.value; handleChange(newValue); textFieldProps?.onChange?.(event); }; const handleAutocompleteInputChange = (_event: SyntheticEvent, newValue: string, _reason: AutocompleteInputChangeReason) => { handleChange(newValue) }; const handleAutocompleteChange = (newValue: DropdownOption | null) => { setAutocompleteValue(newValue); handleChangeDebounced(getValueAndLabel(newValue).value); }; const handleClear = () => { if (isMultiSelectFilter) { setFilterValue([]); column.setFilterValue([]); } else if (isRangeFilter) { setFilterValue(''); column.setFilterValue((old: [string | undefined, string | undefined]) => { const newFilterValues = (Array.isArray(old) && old) || ['', '']; newFilterValues[rangeFilterIndex as number] = undefined; return newFilterValues; }); } else if (isAutocompleteFilter) { setAutocompleteValue(null) setFilterValue('') // when using 'autocomplete' this function is called only inside effect and only if the filterValue === undefined // so the following call is excessive; should be uncommented if the handleClear becomes part of the Autocomplete component callbacks // column.setFilterValue(undefined) } else { setFilterValue(''); column.setFilterValue(undefined); } }; const handleClearEmptyFilterChip = () => { setFilterValue(''); column.setFilterValue(undefined); setColumnFilterFns((prev) => ({ ...prev, [header.id]: allowedColumnFilterOptions?.[0] ?? 'fuzzy', })); }; const handleFilterMenuOpen = (event: MouseEvent) => { setAnchorEl(event.currentTarget); }; const isMounted = useRef(false); useEffect(() => { if (isMounted.current) { const filterValue = column.getFilterValue(); if (filterValue === undefined) { handleClear(); } else if (isRangeFilter && rangeFilterIndex !== undefined) { setFilterValue((filterValue as [string, string])[rangeFilterIndex]); } else { setFilterValue(filterValue as string); } } isMounted.current = true; }, [column.getFilterValue()]); if (columnDef.Filter) { return ( <>{columnDef.Filter?.({ column, header, rangeFilterIndex, table })} ); } const endAdornment = !isAutocompleteFilter && !isDateFilter && !filterChipLabel ? ( 0 ? 'visible' : 'hidden', }} > ) : null; const startAdornment = showChangeModeButton ? ( {filterChipLabel && ( )} ) : null; const commonTextFieldProps: TextFieldProps = { fullWidth: true, helperText: showChangeModeButton ? ( ) : null, inputRef: (inputRef) => { filterInputRefs.current![`${column.id}-${rangeFilterIndex ?? 0}`] = inputRef; if (textFieldProps.inputRef) { textFieldProps.inputRef = inputRef; } }, margin: 'none', placeholder: filterChipLabel || isSelectFilter || isMultiSelectFilter ? undefined : filterPlaceholder, variant: 'standard', ...textFieldProps, slotProps: { ...textFieldProps.slotProps, formHelperText: { sx: { fontSize: '0.75rem', lineHeight: '0.8rem', whiteSpace: 'nowrap', }, ...textFieldProps.slotProps?.formHelperText, }, input: endAdornment //hack because mui looks for presence of endAdornment key instead of undefined ? {endAdornment, startAdornment, ...textFieldProps.slotProps?.input} : {startAdornment, ...textFieldProps.slotProps?.input}, htmlInput: { 'aria-label': filterPlaceholder, autoComplete: 'off', disabled: !!filterChipLabel, sx: { textOverflow: 'ellipsis', width: filterChipLabel ? 0 : undefined, }, title: filterPlaceholder, ...textFieldProps.slotProps?.htmlInput, }, }, onKeyDown: (e) => { e.stopPropagation(); textFieldProps.onKeyDown?.(e); }, sx: (theme) => ({ minWidth: isDateFilter ? '160px' : enableColumnFilterModes && rangeFilterIndex === 0 ? '110px' : isRangeFilter ? '100px' : !filterChipLabel ? '120px' : 'auto', mx: '-2px', p: 0, width: 'calc(100% + 4px)', ...(parseFromValuesOrFunc(textFieldProps?.sx, theme) as any), }), }; const commonDatePickerProps = { onChange: (newDate: any) => { handleChange(newDate); }, value: filterValue || null, }; return ( <> {filterVariant?.startsWith('time') ? ( handleClear(), ...timePickerProps?.slotProps?.field, }, textField: { ...commonTextFieldProps, ...timePickerProps?.slotProps?.textField, }, }} /> ) : filterVariant?.startsWith('datetime') ? ( handleClear(), ...dateTimePickerProps?.slotProps?.field, }, textField: { ...commonTextFieldProps, ...dateTimePickerProps?.slotProps?.textField, }, }} /> ) : filterVariant?.startsWith('date') ? ( handleClear(), ...datePickerProps?.slotProps?.field, }, textField: { ...commonTextFieldProps, ...datePickerProps?.slotProps?.textField, }, }} /> ) : isAutocompleteFilter ? ( getValueAndLabel(option).label } onChange={(_e, newValue) => handleAutocompleteChange(newValue as DropdownOption | null) } options={ dropdownOptions?.map((option) => getValueAndLabel(option)) ?? [] } inputValue={filterValue as string} onInputChange={handleAutocompleteInputChange} {...autocompleteProps} renderInput={(builtinTextFieldProps: TextFieldProps) => ( ) => e.stopPropagation()} /> )} value={autocompleteValue} /> ) : ( !Array.isArray(selected) || selected?.length === 0 ? ( {filterPlaceholder} ) : ( {selected.map((value: string) => { const selectedValue = dropdownOptions?.find( (option) => getValueAndLabel(option).value === value, ); return ( ); })} ) : undefined, ...commonTextFieldProps.slotProps?.select, }, }} onChange={handleTextFieldChange} onClick={(e: MouseEvent) => e.stopPropagation()} value={ isMultiSelectFilter ? Array.isArray(filterValue) ? filterValue : [] : filterValue } > {(isSelectFilter || isMultiSelectFilter) && [ , ...[ textFieldProps.children ?? dropdownOptions?.map((option, index) => { const { label, value } = getValueAndLabel(option); return ( {isMultiSelectFilter && ( )} {label}{' '} {!columnDef.filterSelectOptions && `(${facetedUniqueValues.get(value)})`} ); }), ], ]} )} ); };