import React , { useCallback , useEffect , useRef , useMemo , isValidElement , } from 'react' ; import Downshift , { DownshiftProps } from 'downshift' ; import classNames from 'classnames' ; import get from 'lodash/get' ; import { TextField , Chip , InputProps } from '@material-ui/core' ; import { makeStyles } from '@material-ui/core/styles' ; import { TextFieldProps } from '@material-ui/core/TextField' ; import { useInput , FieldTitle , ChoicesInputProps , useSuggestions , warning , mergeRefs , } from 'ra-core' ; import debounce from 'lodash/debounce' ; import InputHelperText from './InputHelperText' ; import AutocompleteSuggestionList from './AutocompleteSuggestionList' ; import AutocompleteSuggestionItem from './AutocompleteSuggestionItem' ; import { AutocompleteInputLoader } from './AutocompleteInputLoader' ; import { SupportCreateSuggestionOptions , useSupportCreateSuggestion , } from './useSupportCreateSuggestion' ; example example example example example example example const AutocompleteArrayInput = ( props ) => { const { allowDuplicates , allowEmpty , classes : classesOverride , choices = [ ] , create , createLabel , createItemLabel , createValue , debounce : debounceDelay = 250 , disabled , emptyText , emptyValue , format , fullWidth , helperText , id : idOverride , input : inputOverride , isRequired : isRequiredOverride , label , loaded , loading , limitChoicesToValue , margin = 'dense' , matchSuggestion , meta : metaOverride , onBlur , onChange , onCreate , onFocus , options : { suggestionsContainerProps , labelProps , InputProps , ... options } = { } as TextFieldProps & Options , optionText = 'name' , optionValue = 'id' , parse , resource , setFilter , shouldRenderSuggestions : shouldRenderSuggestionsOverride , source , suggestionLimit , translateChoice = true , validate , variant = 'filled' , ... rest } = props ; warning ( isValidElement ( optionText ) && ! matchSuggestion , `If the optionText prop is a React element, you must also specify the matchSuggestion prop:
<AutocompleteArrayInput
    matchSuggestion={(filterValue, suggestion) => true}
/>
        ` ) ; warning ( source === undefined , `If you're not wrapping the AutocompleteArrayInput inside a ReferenceArrayInput, you must provide the source prop` ) ; warning ( choices === undefined , `If you're not wrapping the AutocompleteArrayInput inside a ReferenceArrayInput, you must provide the choices prop` ) ; const classes = useStyles ( props ) ; let inputEl = useRef < HTMLInputElement > ( ) ; let anchorEl = useRef < any > ( ) ; const { id , input , isRequired , meta : { touched , error , submitError } , } = useInput ( { format , id : idOverride , input : inputOverride , meta : metaOverride , onBlur , onChange , onFocus , parse , resource , source , validate , ... rest , } ) ; const values = input . value || emptyArray ; const [ filterValue , setFilterValue ] = React . useState ( '' ) ; const getSuggestionFromValue = useCallback ( value => choices . find ( choice => get ( choice , optionValue ) === value ) , [ choices , optionValue ] ) ; const selectedItems = useMemo ( ( ) => values . map ( getSuggestionFromValue ) , [ getSuggestionFromValue , values , ] ) ; const { getChoiceText , getChoiceValue , getSuggestions } = useSuggestions ( { allowDuplicates , allowEmpty , choices , emptyText , emptyValue , limitChoicesToValue , matchSuggestion , optionText , optionValue , selectedItem : selectedItems , suggestionLimit , translateChoice , } ) ; const debouncedSetFilter = useCallback ( debounce ( setFilter || DefaultSetFilter , debounceDelay ) , [ setFilter , debounceDelay ] ) ; const handleFilterChange = useCallback ( ( eventOrValue ) => { const event = eventOrValue as React . ChangeEvent < { value : string ; } > ; const value = event . target ? event . target . value : ( eventOrValue as string ) ; setFilterValue ( value ) ; if ( setFilter ) { debouncedSetFilter ( value ) ; } } , [ debouncedSetFilter , setFilter , setFilterValue ] ) ; useEffect ( ( ) => { handleFilterChange ( '' ) ; } , [ values . join ( ',' ) , handleFilterChange ] ) ; const handleKeyDown = useCallback ( ( event ) => { if ( selectedItems . length && ! filterValue . length && event . key === 'Backspace' ) { const newSelectedItems = selectedItems . slice ( 0 , selectedItems . length - 1 ) ; input . onChange ( newSelectedItems . map ( getChoiceValue ) ) ; } } , [ filterValue . length , getChoiceValue , input , selectedItems ] ) ; const handleChange = useCallback ( ( item , newItem ) => { const finalItem = newItem || item ; const newSelectedItems = ! allowDuplicates && selectedItems . includes ( finalItem ) ? [ ... selectedItems ] : [ ... selectedItems , finalItem ] ; setFilterValue ( '' ) ; input . onChange ( newSelectedItems . map ( getChoiceValue ) ) ; } , [ allowDuplicates , getChoiceValue , input , selectedItems , setFilterValue ] ) ; const { getCreateItem , handleChange : handleChangeWithCreateSupport , createElement , } = useSupportCreateSuggestion ( { create , createLabel , createItemLabel , createValue , handleChange , filter : filterValue , onCreate , optionText , } ) ; const createItem = create || onCreate ? getCreateItem ( ) : null ; const handleDelete = useCallback ( item => ( ) => { const newSelectedItems = [ ... selectedItems ] ; newSelectedItems . splice ( newSelectedItems . indexOf ( item ) , 1 ) ; input . onChange ( newSelectedItems . map ( getChoiceValue ) ) ; } , [ input , selectedItems , getChoiceValue ] ) ; const updateAnchorEl = ( ) => { if ( ! inputEl . current ) { return ; } const inputPosition = inputEl . current . getBoundingClientRect ( ) as DOMRect ; if ( ! anchorEl . current ) { anchorEl . current = { getBoundingClientRect : ( ) => inputPosition } ; } else { const anchorPosition = anchorEl . current . getBoundingClientRect ( ) ; if ( anchorPosition . x !== inputPosition . x || anchorPosition . y !== inputPosition . y ) { anchorEl . current = { getBoundingClientRect : ( ) => inputPosition , } ; } } } ; const storeInputRef = input => { inputEl . current = input ; updateAnchorEl ( ) ; } ; const handleBlur = useCallback ( event => { setFilterValue ( '' ) ; handleFilterChange ( '' ) ; input . onBlur ( event ) ; } , [ handleFilterChange , input , setFilterValue ] ) ; const handleFocus = useCallback ( openMenu => event => { openMenu ( event ) ; input . onFocus ( event ) ; } , [ input ] ) ; const handleClick = useCallback ( openMenu => event => { if ( event . target === inputEl . current ) { openMenu ( event ) ; } } , [ ] ) ; const shouldRenderSuggestions = val => { if ( shouldRenderSuggestionsOverride !== undefined && typeof shouldRenderSuggestionsOverride === 'function' ) { return shouldRenderSuggestionsOverride ( val ) ; } return true ; } ; const { inputRef , ... InputPropsWithoutInputRef } = InputProps || { } ; return ( < > < Downshift inputValue = { filterValue } onChange = { handleChangeWithCreateSupport } selectedItem = { selectedItems } itemToString = { item => getChoiceValue ( item ) } { ... rest } > { ( { getInputProps , getItemProps , getLabelProps , getMenuProps , isOpen , inputValue : suggestionFilter , highlightedIndex , openMenu , } ) => { const isMenuOpen = isOpen && shouldRenderSuggestions ( suggestionFilter ) ; const { id : idFromDownshift , onBlur , onChange , onFocus , ref , color , size , ... inputProps } = getInputProps ( { onBlur : handleBlur , onFocus : handleFocus ( openMenu ) , onClick : handleClick ( openMenu ) , onKeyDown : handleKeyDown , } ) ; const suggestions = [ ... getSuggestions ( suggestionFilter ) , ... ( ! ! createItem ? [ createItem ] : [ ] ) , ] ; return ( < div className = { classes . container } > < TextField id = { id } fullWidth = { fullWidth } InputProps = { { inputRef : mergeRefs ( [ storeInputRef , inputRef , ] ) , classes : { root : classNames ( classes . inputRoot , { [ classes . inputRootFilled ] : variant === 'filled' , } ) , input : classes . inputInput , } , startAdornment : ( < div className = { classNames ( { [ classes . chipContainerFilled ] : variant === 'filled' , [ classes . chipContainerOutlined ] : variant === 'outlined' , } ) } > { selectedItems . filter ( item => ! ! item ) . map ( ( item , index ) => ( < Chip key = { index } tabIndex = { - 1 } label = { getChoiceText ( item ) } className = { classes . chip } onDelete = { handleDelete ( item ) } / > ) ) } < /div>), endAdornment : loading && ( < AutocompleteInputLoader />), onBlur , onChange : event => { handleFilterChange ( event ) ; onChange ! ( event as React . ChangeEvent < HTMLInputElement > ) ; } , onFocus , ... InputPropsWithoutInputRef , } } error = { ! ! ( touched && ( error || submitError ) ) } label = { < FieldTitle label = { label } { ... labelProps } source = { source } resource = { resource } isRequired = { typeof isRequiredOverride !== 'undefined' ? isRequiredOverride : isRequired } / > } InputLabelProps = { getLabelProps ( { htmlFor : id , } ) } helperText = { < InputHelperText touched = { touched } error = { error || submitError } helperText = { helperText } / > } variant = { variant } margin = { margin } color = { color as any } size = { size as any } disabled = { disabled } { ... inputProps } { ... options } /> < AutocompleteSuggestionList isOpen = { isMenuOpen } menuProps = { getMenuProps ( { } , { suppressRefError : true } ) } inputEl = { inputEl . current } suggestionsContainerProps = { suggestionsContainerProps } className = { classes . suggestionsContainer } > { suggestions . map ( ( suggestion , index ) => ( < AutocompleteSuggestionItem key = { getChoiceValue ( suggestion ) } createValue = { createItem ?. id } suggestion = { suggestion } index = { index } highlightedIndex = { highlightedIndex } isSelected = { selectedItems . map ( getChoiceValue ) . includes ( getChoiceValue ( suggestion ) ) } filterValue = { filterValue } getSuggestionText = { getChoiceText } { ... getItemProps ( { item : suggestion , } ) } / > ) ) } < /AutocompleteSuggestionList> < /div>); } } < /Downshift> { createElement } < />); } ; const emptyArray = [ ] ; const useStyles = makeStyles ( theme => ( { container : { flexGrow : 1 , position : 'relative' , } , suggestionsContainer : { zIndex : theme . zIndex . modal , } , chip : { margin : theme . spacing ( 0.5 , 0.5 , 0.5 , 0 ) , } , chipContainerFilled : { margin : '27px 12px 10px 0' , } , chipContainerOutlined : { margin : '12px 12px 10px 0' , } , inputRoot : { flexWrap : 'wrap' , } , inputRootFilled : { flexWrap : 'wrap' , '& $chip' : { backgroundColor : theme . palette . type === 'light' ? 'rgba(0, 0, 0, 0.09)' : 'rgba(255, 255, 255, 0.09)' , } , } , inputInput : { width : 'auto' , flexGrow : 1 , } , } ) , { name : 'RaAutocompleteArrayInput' } ) ; const DefaultSetFilter = ( ) => { } ; interface Options { InputProps ? : InputProps ; labelProps ? : any ; suggestionsContainerProps ? : any ; } export interface AutocompleteArrayInputProps extends ChoicesInputProps < TextFieldProps > , Omit < SupportCreateSuggestionOptions , 'handleChange' > , Omit < DownshiftProps < any > , 'onChange' > { options ? : Options ; } export default AutocompleteArrayInput ;