@evolvier/react-native-select
Version:
A multi (or single) select component
817 lines (788 loc) • 21.1 kB
JavaScript
/**
* Copyright (c) 2019-present, Evolvier Technologies. All rights reserved.
*
*/
;
import React, { Component } from "react";
import {
Text,
TextInput,
View,
TouchableHighlight,
TouchableOpacity,
StyleSheet,
SafeAreaView,
ScrollView,
ViewPropTypes,
Dimensions,
} from "react-native";
import Modal from "react-native-modal";
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
import PropTypes from "prop-types";
import Chip from "./chip";
// import Tree from './tree';
const windowWidth = Dimensions.get("window").width;
const windowHeight = Dimensions.get("window").height;
export class index extends Component {
constructor(props) {
super(props);
this.state = {
modalVisible: false,
selectedItem: props.defaultSelectedItem,
selectedItems: props.defaultSelectedItems,
tempSelectedItems: [],
scrollOffset: null,
};
this.scrollViewRef = React.createRef();
}
handleOnScroll = (event) => {
this.setState({
scrollOffset: event.nativeEvent.contentOffset.y,
});
};
handleScrollTo = (p) => {
if (this.scrollViewRef.current) {
this.scrollViewRef.current.scrollTo(p);
}
};
_getColor = () => {
const { errorColor, tintColor, baseColor, error } = this.props;
const { hasFocus } = this.state;
if (error) {
return errorColor;
}
if (hasFocus) {
return tintColor;
}
return baseColor;
};
_getLineStyleVariant = () => {
const { error } = this.props;
const { hasFocus } = this.state;
return error || hasFocus
? { borderBottomWidth: 2, paddingBottom: 1 }
: { borderBottomWidth: 0.5, paddingBottom: 2.5 };
};
setModalVisible = (visible) => {
this.state.tempSelectedItems = JSON.parse(
JSON.stringify(this.state.selectedItems)
);
this.setState({ modalVisible: visible });
};
_renderSingleSelectItems = (type, items, onSubmit) => {
const {
renderItem,
checkedIcon,
unCheckedIcon,
listItemTitleTextStyle,
listItemSubtitleTextStyle,
} = this.props;
return (
<SafeAreaView style={{ flex: 1 }}>
{this._renderSearchInput()}
<ScrollView
ref={this.scrollViewRef}
onScroll={this.handleOnScroll}
scrollEventThrottle={16}
>
<View
style={
type === "chip" ? styles.chipsContainer : styles.listContainer
}
>
{items.map((item) =>
renderItem(
type,
item,
() => {
this.setModalVisible(false);
this.setState({
selectedItem: item.key,
});
if (onSubmit) {
onSubmit(item.key);
}
},
item.key === this.state.selectedItem,
checkedIcon,
unCheckedIcon,
listItemTitleTextStyle,
listItemSubtitleTextStyle
)
)}
</View>
</ScrollView>
</SafeAreaView>
);
};
_renderMultiSelectItems = (type, items, onSubmit) => {
const {
renderItem,
checkedIcon,
unCheckedIcon,
listItemTitleTextStyle,
listItemSubtitleTextStyle,
maxSelection,
} = this.props;
const { tempSelectedItems } = this.state;
return (
<SafeAreaView style={{ flex: 1 }}>
{this._renderSearchInput()}
{maxSelection ? (
<View style={styles.maxLimitWarningView}>
<Text style={[styles.warningText, listItemTitleTextStyle]}>
{"You can select maximum " + maxSelection + " items"}
</Text>
</View>
) : null}
<ScrollView
ref={this.scrollViewRef}
onScroll={this.handleOnScroll}
scrollEventThrottle={16}
>
<View
style={
type === "chip" ? styles.chipsContainer : styles.listContainer
}
>
{items.map((item) =>
renderItem(
type,
item,
() => {
let tempSelectedItems = this.state.tempSelectedItems;
if (
tempSelectedItems.find((rawItem) => rawItem === item.key)
) {
tempSelectedItems = tempSelectedItems.filter(
(rawItem) => rawItem !== item.key
);
} else {
if (maxSelection) {
if (tempSelectedItems.length < maxSelection) {
tempSelectedItems.push(item.key);
}
} else {
tempSelectedItems.push(item.key);
}
}
this.setState({
tempSelectedItems,
});
},
this.state.tempSelectedItems.find(
(rawItem) => rawItem === item.key
),
checkedIcon,
unCheckedIcon,
listItemTitleTextStyle,
listItemSubtitleTextStyle
)
)}
</View>
</ScrollView>
{tempSelectedItems && tempSelectedItems.length ? (
<View
style={
tempSelectedItems.length >= maxSelection
? styles.maxCountView
: styles.selectedItemsCountView
}
>
<Text style={[styles.whiteText, listItemTitleTextStyle]}>
{tempSelectedItems.length >= maxSelection
? "Maximum selection reached!"
: tempSelectedItems.length + " items selected"}
</Text>
</View>
) : null}
<TouchableHighlight
activeOpacity={1}
underlayColor={"#182833"}
style={{ ...styles.openButton, backgroundColor: "#243746" }}
onPress={() => {
this.setState({
selectedItems: this.state.tempSelectedItems,
});
if (onSubmit) {
onSubmit(this.state.tempSelectedItems);
}
this.setModalVisible(false);
}}
>
<Text style={styles.textStyle}>DONE</Text>
</TouchableHighlight>
</SafeAreaView>
);
};
_renderSearchInput = () => {
const {
renderSearchInput,
enableSearch,
searchInputPlaceholderText,
clearSearchIcon,
} = this.props;
if (enableSearch) {
return renderSearchInput(
this.state.searchText,
searchInputPlaceholderText,
(searchText) => {
this.setState({
searchText,
});
},
() => {
this.setState({
searchText: "",
});
},
clearSearchIcon
);
} else {
return null;
}
};
_renderItems = (select, type, items, onSubmit) => {
switch (select) {
case "single":
return this._renderSingleSelectItems(type, items, onSubmit);
case "multi":
return this._renderMultiSelectItems(type, items, onSubmit);
// case "tree":
// return (<Tree/>);
}
};
selectInputText = () => {
const { placeholderText, searchInputText, items } = this.props;
if (searchInputText) {
return searchInputText;
} else if (
this.state.selectedItem &&
items.find((item) => item.key === this.state.selectedItem)
) {
return items.find((item) => item.key === this.state.selectedItem).title;
} else {
return placeholderText;
}
};
renderSelectItemsChip = (selectedItems) => {
const {
renderSelectedItem,
items,
chipStyle,
chipIconStyle,
chipTitleTextStyle,
chipSubTitleTextStyle,
onSubmit,
} = this.props;
return selectedItems.map((key) =>
renderSelectedItem(
items.find((item) => item.key === key),
() => {
if (onSubmit) {
onSubmit(this.state.selectedItems.filter((item) => item !== key));
}
this.setState({
selectedItems: this.state.selectedItems.filter(
(item) => item !== key
),
});
},
chipStyle,
chipIconStyle,
chipTitleTextStyle,
chipSubTitleTextStyle
)
);
};
multiSelectItems = () => {
const {
placeholderText,
selectedItemsPosition,
placeholderTextStyle,
placeholderRightIcon,
} = this.props;
if (
selectedItemsPosition === "inside" &&
this.state.selectedItems &&
this.state.selectedItems.length
) {
return this.renderSelectItemsChip(this.state.selectedItems);
} else {
return (
<View style={styles.placeholderView}>
<Text style={placeholderTextStyle}>{placeholderText}</Text>
<View style={styles.placeholderIconView}>{placeholderRightIcon}</View>
</View>
);
}
};
_renderSelectInputText = () => {
const { select, placeholderTextStyle, placeholderRightIcon } = this.props;
switch (select) {
case "single":
return (
<View style={styles.placeholderView}>
<Text style={placeholderTextStyle}>{this.selectInputText()}</Text>
<View style={styles.placeholderIconView}>
{placeholderRightIcon}
</View>
</View>
);
case "multi":
return this.multiSelectItems();
// case "tree":
// return this.multiSelectItems();
}
};
_renderSelectInput = () => {
const { select, inputContainerStyle, selectedItemsPosition } = this.props;
const inputContainerBorderStyle = {
borderBottomColor: this._getColor(),
...this._getLineStyleVariant(),
};
return (
<View>
<TouchableOpacity
activeOpacity={0.5}
onPress={() => {
this.setModalVisible(true);
}}
>
<View>
<View
style={[
styles.inputContainer,
inputContainerBorderStyle,
inputContainerStyle,
]}
>
{this._renderSelectInputText()}
</View>
</View>
</TouchableOpacity>
<View style={styles.inputBelowContainer}>
{select === "multi" && selectedItemsPosition === "below"
? this.renderSelectItemsChip(this.state.selectedItems)
: null}
</View>
</View>
);
};
getContainerStyle = () => {
const { presentationStyle } = this.props;
switch (presentationStyle) {
case "fullScreen":
return {
flex: 1,
backgroundColor: "white",
};
default:
return {
flex: 1,
maxHeight: windowHeight * 0.7,
backgroundColor: "white",
};
}
};
render() {
const { props, state, setModalVisible } = this;
let {
select,
type,
items,
onSubmit,
hideOnBackdropPress,
disableAndroidBack,
swipeToDismiss,
modelProps,
} = props;
if (this.state.searchText) {
items = items.filter(
(item) =>
item.title.match(new RegExp(this.state.searchText, "i")) ||
(item.subtitle &&
item.subtitle.match(new RegExp(this.state.searchText, "i")))
);
}
if (swipeToDismiss) {
modelProps.onSwipeComplete = () => this.setModalVisible(false);
modelProps.swipeDirection = ["down"];
modelProps.scrollTo = this.handleScrollTo;
modelProps.scrollOffset = this.state.scrollOffset;
modelProps.propagateSwipe = true;
}
return (
<SafeAreaView>
{this._renderSelectInput()}
<Modal
style={{ margin: 0, justifyContent: "flex-end" }}
isVisible={state.modalVisible}
onBackButtonPress={() =>
!disableAndroidBack && setModalVisible(false)
}
onBackdropPress={() => hideOnBackdropPress && setModalVisible(false)}
avoidKeyboard
{...modelProps}
>
<View style={this.getContainerStyle()}>
{this._renderItems(select, type, items, onSubmit)}
</View>
</Modal>
</SafeAreaView>
);
}
}
index.propTypes = {
select: PropTypes.string,
type: PropTypes.string,
items: PropTypes.arrayOf(
PropTypes.shape({
key: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
title: PropTypes.string.isRequired,
subtitle: PropTypes.string,
subtitle2: PropTypes.string,
disabled: PropTypes.bool,
children: PropTypes.array,
})
).isRequired,
defaultSelectedItem: PropTypes.oneOfType([
PropTypes.string,
PropTypes.number,
]),
defaultSelectedItems: PropTypes.arrayOf(
PropTypes.oneOfType([PropTypes.string, PropTypes.number])
),
selectedItemsPosition: PropTypes.string,
maxSelection: PropTypes.number,
onSubmit: PropTypes.func.isRequired,
placeholderText: PropTypes.string,
searchInputText: PropTypes.string,
renderSelectedItem: PropTypes.func,
inputContainerBorderStyle: PropTypes.any,
inputContainerStyle: PropTypes.any,
placeholderTextStyle: PropTypes.any,
searchPlaceholderTextStyle: PropTypes.any,
listItemTitleTextStyle: PropTypes.any,
listItemSubtitleTextStyle: PropTypes.any,
listItemSubtitle2TextStyle: PropTypes.any,
chipTitleTextStyle: PropTypes.any,
chipSubTitleTextStyle: PropTypes.any,
checkedIcon: PropTypes.element,
unCheckedIcon: PropTypes.element,
clearSearchIcon: PropTypes.element,
placeholderRightIcon: PropTypes.element,
test: PropTypes.element,
};
index.defaultProps = {
select: "single",
type: "list",
defaultSelectedItems: [],
placeholderText: "Select Item",
enableSearch: false,
hideOnBackdropPress: false,
disableAndroidBack: false,
swipeToDismiss: false,
modelProps: {},
searchInputPlaceholderText: "Search...",
errorColor: "rgb(213, 0, 0)",
tintColor: "rgb(0, 145, 234)",
baseColor: "rgba(0, 0, 0, .38)",
selectedItemsPosition: "inside",
renderSearchInput: (
value,
placeholder,
onChange,
onClear,
clearSearchIcon
) => (
<View style={styles.searchInputView}>
<View style={styles.searchInputTextView}>
<TextInput
value={value}
placeholder={placeholder}
onChangeText={onChange}
/>
</View>
{value ? (
<TouchableHighlight
underlayColor="#f2f2f2"
style={styles.closeTouch}
onPress={onClear}
>
{clearSearchIcon}
</TouchableHighlight>
) : null}
</View>
),
renderItem: (
type,
item,
onPress,
isSelected = false,
checkedIcon,
unCheckedIcon,
listItemTitleTextStyle,
listItemSubtitleTextStyle,
listItemSubtitle2TextStyle
) => (
<View>
{type === "list" ? (
<TouchableHighlight
underlayColor="#f2f2f2"
key={item.key}
onPress={onPress}
>
<View style={styles.itemMainView}>
<View>{isSelected ? checkedIcon : unCheckedIcon}</View>
<View style={styles.itemContentView}>
{item.title ? (
<Text
style={[styles.itemTitleTextStyle, listItemTitleTextStyle]}
>
{item.title}
</Text>
) : null}
{item.subtitle ? (
<Text
style={[
styles.itemSubtitleTextStyle,
listItemSubtitleTextStyle,
]}
>
{item.subtitle}
</Text>
) : null}
{item.subtitle2 ? (
<Text
style={[
styles.itemSubtitle2TextStyle,
listItemSubtitle2TextStyle,
]}
>
{item.subtitle2}
</Text>
) : null}
</View>
</View>
</TouchableHighlight>
) : null}
{type === "chip" ? (
<TouchableHighlight
activeOpacity={1}
underlayColor="#f2f2f2"
key={item.key}
onPress={onPress}
style={
isSelected
? styles.chipItemSelectedMainView
: styles.chipItemMainView
}
>
<View style={styles.chipItemContentView}>
<Text style={[styles.itemTitleTextStyle, listItemTitleTextStyle]}>
{item.title}
</Text>
{item.subtitle ? (
<Text
style={[
styles.itemSubtitleTextStyle,
listItemSubtitleTextStyle,
]}
>
{item.subtitle}
</Text>
) : null}
</View>
</TouchableHighlight>
) : null}
</View>
),
renderSelectedItem: (
item,
onClose,
style,
iconStyle,
titleTextStyle,
subTitleTextStyle
) => (
<Chip
key={item.key}
iconStyle={iconStyle}
onClose={onClose}
text={item.title}
textStyle={titleTextStyle}
subtitle={item.subtitle}
subtitleStyle={subTitleTextStyle}
style={style}
/>
),
checkedIcon: <Icon name={"check-circle"} size={30} />,
unCheckedIcon: (
<Icon name={"check-circle-outline"} size={30} color={"#D0D0D0"} />
),
clearSearchIcon: <Icon name={"close"} size={25} color={"grey"} />,
placeholderRightIcon: <Icon name={"chevron-down"} size={25} color={"grey"} />,
};
const styles = StyleSheet.create({
inputContainer: {
flexDirection: "row",
flexWrap: "wrap",
alignItems: "center",
},
inputBelowContainer: {
flexDirection: "row",
flexWrap: "wrap",
marginTop: 10,
},
openButton: {
backgroundColor: "#F194FF",
borderRadius: 0,
padding: 15,
// marginHorizontal: 15,
// marginTop: 5,
// marginBottom: 10,
},
searchInputView: {
marginHorizontal: 15,
marginTop: 10,
marginBottom: 5,
borderWidth: 1,
borderColor: "grey",
paddingHorizontal: 10,
alignItems: "center",
height: 50,
flexDirection: "row",
},
textStyle: {
color: "white",
fontWeight: "bold",
textAlign: "center",
},
itemMainView: {
flexDirection: "row",
flex: 1,
paddingVertical: 10,
paddingHorizontal: 15,
},
chipItemMainView: {
borderWidth: 1,
borderRadius: 50,
borderColor: "#e0e0e0",
alignItems: "center",
paddingVertical: 7,
paddingHorizontal: 10,
marginVertical: 5,
marginHorizontal: 3,
},
chipItemSelectedMainView: {
borderWidth: 1,
borderRadius: 50,
borderColor: "red",
alignItems: "center",
paddingVertical: 7,
paddingHorizontal: 10,
marginVertical: 5,
marginHorizontal: 3,
},
unselectedIconView: {
borderWidth: 1,
borderColor: "grey",
height: 30,
width: 30,
borderRadius: 30,
alignItems: "center",
justifyContent: "center",
},
selectedIconView: {
height: 30,
width: 30,
borderRadius: 30,
alignItems: "center",
justifyContent: "center",
backgroundColor: "#243746",
},
itemContentView: {
flex: 1,
marginLeft: 10,
justifyContent: "center",
},
chipItemContentView: {
justifyContent: "center",
},
itemTitleTextStyle: {
fontSize: 16,
color: "grey",
},
itemSubtitleTextStyle: {
fontSize: 13,
color: "#243746",
fontWeight: "bold",
},
itemSubtitle2TextStyle: {
fontSize: 10,
color: "#243746",
marginTop: 5,
},
chipsContainer: {
flex: 1,
flexDirection: "row",
flexWrap: "wrap",
paddingHorizontal: 10,
},
listContainer: {
flex: 1,
},
searchInputTextView: {
flex: 1,
},
closeTouch: {
borderRadius: 50,
padding: 3,
},
placeholderView: {
flexDirection: "row",
alignItems: "center",
paddingBottom: 1,
},
placeholderIconView: {
flex: 1,
justifyContent: "center",
alignItems: "flex-end",
},
warningView: {
backgroundColor: "#F0F0F0",
paddingVertical: 7,
paddingHorizontal: 10,
marginHorizontal: 15,
borderRadius: 5,
marginTop: 3,
},
warningActiveView: {
backgroundColor: "#FFAAAA",
paddingVertical: 7,
paddingHorizontal: 10,
marginHorizontal: 15,
borderRadius: 5,
marginTop: 3,
},
maxLimitWarningView: {
marginHorizontal: 15,
paddingTop: 5,
paddingBottom: 5,
},
selectedItemsCountView: {
paddingVertical: 5,
alignItems: "center",
backgroundColor: "#d4b299",
},
maxCountView: {
paddingVertical: 5,
alignItems: "center",
backgroundColor: "#ff6338",
},
whiteText: {
color: "#FFFFFF",
},
});
export default index;