/* eslint-disable @typescript-eslint/no-shadow */ import _assign from 'lodash/assign'; import _differenceWith from 'lodash/differenceWith'; import _get from 'lodash/get'; import React, { JSXElementConstructor, ReactElement, useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState, } from 'react'; import { Dimensions, FlatList, I18nManager, Image, Keyboard, KeyboardEvent, Modal, StyleSheet, Text, TouchableHighlight, TouchableWithoutFeedback, View, ViewStyle, StatusBar, } from 'react-native'; import { useDetectDevice } from '../../toolkits'; import { useDeviceOrientation } from '../../useDeviceOrientation'; import CInput from '../TextInput'; import { MultiSelectProps } from './model'; import { styles } from './styles'; const { isTablet } = useDetectDevice; const ic_down = require('../../assets/down.png'); const statusBarHeight: number = StatusBar.currentHeight || 0; const MultiSelectComponent: ( props: MultiSelectProps ) => ReactElement> | null = React.forwardRef((props, currentRef) => { const orientation = useDeviceOrientation(); const { testID, itemTestIDField, onChange, data = [], value, style = {}, labelField, valueField, searchField, selectedStyle, selectedTextStyle, itemContainerStyle, itemTextStyle, iconStyle, selectedTextProps = {}, activeColor = '#F6F7F8', containerStyle, fontFamily, placeholderStyle, iconColor = 'gray', inputSearchStyle, searchPlaceholder, placeholder = 'Select item', search = false, maxHeight = 340, minHeight = 0, maxSelect, disable = false, keyboardAvoiding = true, inside = false, inverted = true, renderItem, renderLeftIcon, renderRightIcon, renderSelectedItem, renderInputSearch, onFocus, onBlur, showsVerticalScrollIndicator = true, dropdownPosition = 'auto', flatListProps, alwaysRenderSelectedItem = false, searchQuery, backgroundColor, onChangeText, confirmSelectItem, confirmUnSelectItem, onConfirmSelectItem, accessibilityLabel, itemAccessibilityLabelField, visibleSelectedItem = true, mode = 'default', excludeItems = [], excludeSearchItems = [], } = props; const ref = useRef(null); const [visible, setVisible] = useState(false); const [currentValue, setCurrentValue] = useState([]); const [listData, setListData] = useState(data); const [, setKey] = useState(Math.random()); const [position, setPosition] = useState(); const [keyboardHeight, setKeyboardHeight] = useState(0); const [searchText, setSearchText] = useState(''); const { width: W, height: H } = Dimensions.get('window'); const styleContainerVertical: ViewStyle = useMemo(() => { return { backgroundColor: 'rgba(0,0,0,0.1)', alignItems: 'center', }; }, []); const styleHorizontal: ViewStyle = useMemo(() => { return { width: orientation === 'LANDSCAPE' ? W / 2 : '100%', alignSelf: 'center', }; }, [W, orientation]); useImperativeHandle(currentRef, () => { return { open: eventOpen, close: eventClose }; }); useEffect(() => { return eventClose; // eslint-disable-next-line react-hooks/exhaustive-deps }, []); const excludeData = useCallback( (data: any[]) => { if (excludeItems.length > 0) { const getData = _differenceWith( data, excludeItems, (obj1, obj2) => _get(obj1, valueField) === _get(obj2, valueField) ); return getData || []; } else { return data || []; } }, [excludeItems, valueField] ); useEffect(() => { const filterData = excludeData(data); setListData([...filterData]); if (searchText) { onSearch(searchText); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [data, searchText]); const eventOpen = () => { if (!disable) { setVisible(true); if (onFocus) { onFocus(); } if (searchText.length > 0) { onSearch(searchText); } } }; const eventClose = () => { if (!disable) { setVisible(false); if (onBlur) { onBlur(); } } }; const font = useCallback(() => { if (fontFamily) { return { fontFamily: fontFamily, }; } else { return {}; } }, [fontFamily]); const getValue = useCallback(() => { setCurrentValue(value ? [...value] : []); }, [value]); const _measure = useCallback(() => { if (ref && ref?.current) { ref.current.measureInWindow((pageX, pageY, width, height) => { let isFull = isTablet ? false : mode === 'modal' || orientation === 'LANDSCAPE'; if (mode === 'auto') { isFull = false; } const top = isFull ? 20 : height + pageY + 2; const bottom = H - top + height; const left = I18nManager.isRTL ? W - width - pageX : pageX; setPosition({ isFull, width: Math.floor(width), top: Math.floor(top + statusBarHeight), bottom: Math.floor(bottom - statusBarHeight), left: Math.floor(left), height: Math.floor(height), }); }); } }, [H, W, orientation, mode]); const onKeyboardDidShow = useCallback( (e: KeyboardEvent) => { _measure(); setKeyboardHeight(e.endCoordinates.height); }, [_measure] ); const onKeyboardDidHide = useCallback(() => { setKeyboardHeight(0); _measure(); }, [_measure]); useEffect(() => { const susbcriptionKeyboardDidShow = Keyboard.addListener( 'keyboardDidShow', onKeyboardDidShow ); const susbcriptionKeyboardDidHide = Keyboard.addListener( 'keyboardDidHide', onKeyboardDidHide ); return () => { if (typeof susbcriptionKeyboardDidShow?.remove === 'function') { susbcriptionKeyboardDidShow.remove(); } if (typeof susbcriptionKeyboardDidHide?.remove === 'function') { susbcriptionKeyboardDidHide.remove(); } }; }, [onKeyboardDidHide, onKeyboardDidShow]); useEffect(() => { getValue(); }, [getValue, value]); const showOrClose = useCallback(() => { if (!disable) { const visibleStatus = !visible; if (keyboardHeight > 0 && !visibleStatus) { return Keyboard.dismiss(); } _measure(); setVisible(visibleStatus); const filterData = excludeData(data); setListData(filterData); if (visibleStatus) { if (onFocus) { onFocus(); } } else { if (onBlur) { onBlur(); } } if (searchText.length > 0) { onSearch(searchText); } } // eslint-disable-next-line react-hooks/exhaustive-deps }, [ disable, keyboardHeight, visible, _measure, data, searchText, onFocus, onBlur, ]); const onSearch = useCallback( (text: string) => { if (text.length > 0) { const defaultFilterFunction = (e: any) => { const item = _get(e, searchField || labelField) ?.toLowerCase() .replace(/\s/g, '') .normalize('NFD') .replace(/[\u0300-\u036f]/g, ''); const key = text .toLowerCase() .replace(/\s/g, '') .normalize('NFD') .replace(/[\u0300-\u036f]/g, ''); return item.indexOf(key) >= 0; }; const propSearchFunction = (e: any) => { const labelText = _get(e, searchField || labelField); return searchQuery?.(text, labelText); }; const dataSearch = data.filter( searchQuery ? propSearchFunction : defaultFilterFunction ); if (excludeSearchItems.length > 0 || excludeItems.length > 0) { const excludeSearchData = _differenceWith( dataSearch, excludeSearchItems, (obj1, obj2) => _get(obj1, valueField) === _get(obj2, valueField) ); const filterData = excludeData(excludeSearchData); setListData(filterData); } else { setListData(dataSearch); } } else { const filterData = excludeData(data); setListData(filterData); } }, [ data, searchQuery, excludeSearchItems, excludeItems, searchField, labelField, valueField, excludeData, ] ); const onSelect = useCallback( (item: any) => { const newCurrentValue = [...currentValue]; const index = newCurrentValue.findIndex( (e) => e === _get(item, valueField) ); if (index > -1) { newCurrentValue.splice(index, 1); } else { if (maxSelect) { if (newCurrentValue.length < maxSelect) { newCurrentValue.push(_get(item, valueField)); } } else { newCurrentValue.push(_get(item, valueField)); } } if (onConfirmSelectItem) { if (index > -1) { if (confirmUnSelectItem) { onConfirmSelectItem(newCurrentValue); } else { onChange(newCurrentValue); } } else { if (confirmSelectItem) { onConfirmSelectItem(newCurrentValue); } else { onChange(newCurrentValue); } } } else { onChange(newCurrentValue); } setKey(Math.random()); }, [ confirmSelectItem, confirmUnSelectItem, currentValue, maxSelect, onChange, onConfirmSelectItem, valueField, ] ); const _renderDropdown = () => { return ( {renderLeftIcon?.(visible)} {placeholder} {renderRightIcon ? ( renderRightIcon(visible) ) : ( )} ); }; const checkSelected = useCallback( (item: any) => { const index = currentValue.findIndex( (e) => e === _get(item, valueField) ); return index > -1; }, [currentValue, valueField] ); const _renderItem = useCallback( ({ item, index }: { item: any; index: number }) => { const selected = checkSelected(item); _assign(item, { _index: index }); return ( onSelect(item)} > {renderItem ? ( renderItem(item, selected) ) : ( {_get(item, labelField)} )} ); }, [ accessibilityLabel, activeColor, checkSelected, font, itemAccessibilityLabelField, itemContainerStyle, itemTestIDField, itemTextStyle, labelField, onSelect, renderItem, ] ); const renderSearch = useCallback(() => { if (search) { if (renderInputSearch) { return renderInputSearch((text) => { if (onChangeText) { setSearchText(text); onChangeText(text); } onSearch(text); }); } else { return ( { if (onChangeText) { setSearchText(e); onChangeText(e); } onSearch(e); }} placeholderTextColor="gray" iconStyle={[{ tintColor: iconColor }, iconStyle]} /> ); } } return null; }, [ accessibilityLabel, font, iconColor, iconStyle, inputSearchStyle, onChangeText, onSearch, renderInputSearch, search, searchPlaceholder, testID, ]); const _renderList = useCallback( (isTopPosition: boolean) => { const isInverted = !inverted ? false : isTopPosition; const _renderListHelper = () => { return ( index.toString()} showsVerticalScrollIndicator={showsVerticalScrollIndicator} /> ); }; return ( {isInverted && _renderListHelper()} {renderSearch()} {!isInverted && _renderListHelper()} ); }, [ _renderItem, accessibilityLabel, flatListProps, listData, inverted, renderSearch, showsVerticalScrollIndicator, testID, ] ); const _renderModal = useCallback(() => { if (visible && position) { const { isFull, width, height, top, bottom, left } = position; const onAutoPosition = () => { if (keyboardHeight > 0) { return bottom < keyboardHeight + height; } return bottom < (search ? 150 : 100); }; if (width && top && bottom) { const styleVertical: ViewStyle = { left: left, maxHeight: maxHeight, minHeight: minHeight, }; const isTopPosition = dropdownPosition === 'auto' ? onAutoPosition() : dropdownPosition === 'top'; let keyboardStyle: ViewStyle = {}; let extendHeight = !isTopPosition ? top : bottom; if ( keyboardAvoiding && keyboardHeight > 0 && isTopPosition && dropdownPosition === 'auto' ) { extendHeight = keyboardHeight; } return ( {_renderList(isTopPosition)} ); } return null; } return null; }, [ visible, search, position, keyboardHeight, maxHeight, minHeight, dropdownPosition, keyboardAvoiding, showOrClose, styleContainerVertical, backgroundColor, containerStyle, styleHorizontal, _renderList, ]); const unSelect = (item: any) => { if (!disable) { onSelect(item); } }; const _renderItemSelected = (inside: boolean) => { const list = data.filter((e: any) => { const check = value?.indexOf(_get(e, valueField)); if (check !== -1) { return e; } }); return ( {list.map((e) => { if (renderSelectedItem) { return ( unSelect(e)} > {renderSelectedItem(e, () => { unSelect(e); })} ); } else { return ( unSelect(e)} > {_get(e, labelField)} ⓧ ); } })} ); }; const _renderInside = () => { return ( {_renderDropdownInside()} {_renderModal()} ); }; const _renderDropdownInside = () => { return ( {renderLeftIcon?.()} {value && value?.length > 0 ? ( _renderItemSelected(true) ) : ( {placeholder} )} {renderRightIcon ? ( renderRightIcon() ) : ( )} ); }; if (inside) { return _renderInside(); } return ( <> {_renderDropdown()} {_renderModal()} {(!visible || alwaysRenderSelectedItem) && visibleSelectedItem && _renderItemSelected(false)} ); }); export default MultiSelectComponent;