UNPKG

react-native-sectioned-multi-select

Version:

a multi (or single) select component with support for sub categories, search, chips.

809 lines (767 loc) 21 kB
import React, { Component } from 'react' import { Platform, StyleSheet, Text, View, ScrollView, Switch, TouchableWithoutFeedback, TouchableOpacity, ActivityIndicator, Dimensions, LayoutAnimation, Image, Appearance } from 'react-native' import SectionedMultiSelect from 'react-native-sectioned-multi-select' import Icon from 'react-native-vector-icons/MaterialIcons' const img = require('./z.jpg') // Sorry for the mess const items = [ { title: 'Fruits', id: 0, children: [ { title: 'Apple', id: 10 }, { title: 'Strawberry', id: 11 }, { title: 'Pineapple', id: 13 }, { title: 'Banana', id: 14 }, { title: 'Wátermelon', id: 15 }, { title: 'אבטיח', id: 17 }, { title: 'Raspberry', id: 18 }, { title: 'Orange', id: 19 }, { title: 'Mandarin', id: 20 }, { title: 'Papaya', id: 21 }, { title: 'Lychee', id: 22 }, { title: 'Cherry', id: 23 }, { title: 'Peach', id: 24 }, { title: 'Apricot', id: 25 } ] }, { title: 'Gèms', id: 1, icon: 'cake', children: [ { title: 'Quartz', id: 26 }, { title: 'Zircon', id: 27 }, { title: 'Sapphirè', id: 28 }, { title: 'Topaz', id: 29 } ] }, { title: 'Plants', id: 2, icon: img, children: [ { title: "Mother In Law's Tongue", id: 30 }, { title: 'Yucca', id: 31 }, { title: 'Monsteria', id: 32 }, { title: 'Palm', id: 33 } ] }, { title: 'No child', id: 34 } ] console.log(items) // const items2 = // [{ // title: 'Plants', // id: 2, // children: [ // { // title: "Mother In Law's Tongue", // id: 30, // }, // { // title: 'Yucca', // id: 31, // }, // { // title: 'Monsteria', // id: 32, // }, // { // title: 'Palm', // id: 33, // }, // ], // }] const items2 = [] for (let i = 0; i < 100; i++) { items2.push({ id: i, title: `item ${i}`, children: [ { id: `10${i}`, title: `child 10${i}` }, { id: `11${i}`, title: `child 11${i}` }, { id: `12${i}`, title: `child 12${i}` } ] }) } const styles = StyleSheet.create({ center: { flex: 1, justifyContent: 'center', alignItems: 'center', marginTop: 30 }, container: { paddingTop: 40, paddingHorizontal: 20 }, welcome: { fontSize: 20, textAlign: 'center', margin: 10, color: '#333' }, border: { borderBottomWidth: 1, borderBottomColor: '#dadada', marginBottom: 20 }, heading: { fontSize: 24, fontWeight: 'bold', marginBottom: 5, marginTop: 20 }, label: { fontWeight: 'bold' }, switch: { marginBottom: 20, flexDirection: 'row', alignItems: 'flex-end', justifyContent: 'space-between' } }) const accentMap = { â: 'a', Â: 'A', à: 'a', À: 'A', á: 'a', Á: 'A', ã: 'a', Ã: 'A', ê: 'e', Ê: 'E', è: 'e', È: 'E', é: 'e', É: 'E', î: 'i', Î: 'I', ì: 'i', Ì: 'I', í: 'i', Í: 'I', õ: 'o', Õ: 'O', ô: 'o', Ô: 'O', ò: 'o', Ò: 'O', ó: 'o', Ó: 'O', ü: 'u', Ü: 'U', û: 'u', Û: 'U', ú: 'u', Ú: 'U', ù: 'u', Ù: 'U', ç: 'c', Ç: 'C' } const tintColor = '#174A87' const Loading = (props) => props.hasErrored ? ( <TouchableWithoutFeedback onPress={props.fetchCategories}> <View style={styles.center}> <Text>oops... something went wrong. Tap to reload</Text> </View> </TouchableWithoutFeedback> ) : ( <View style={styles.center}> <ActivityIndicator size="large" /> </View> ) const Toggle = (props) => ( <TouchableWithoutFeedback onPress={() => props.onPress(!props.val)} disabled={props.disabled} > <View style={styles.switch}> <Text style={styles.label}>{props.name}</Text> <Switch trackColor={tintColor} onValueChange={(v) => props.onPress(v)} value={props.val} disabled={props.disabled} /> </View> </TouchableWithoutFeedback> ) export default class App extends Component { constructor() { super() this.state = { items: null, loading: false, selectedItems: [], selectedItems2: [], selectedItemObjects: [], currentItems: [], showDropDowns: false, single: false, readOnlyHeadings: false, highlightChildren: false, selectChildren: false, hideChipRemove: false, hasErrored: false, isDarkMode: false } this.termId = 100 this.maxItems = 5 } componentDidMount() { this.pretendToLoad() const colorScheme = Appearance.getColorScheme() if (colorScheme === 'dark') { // Use dark color scheme this.setState({ isDarkMode: true }) } // programatically opening the select // this.SectionedMultiSelect._toggleSelector() } // custom icon renderer passed to iconRenderer prop // see the switch for possible icon name // values icon = ({ name, size = 18, style }) => { // flatten the styles const flat = StyleSheet.flatten(style) // remove out the keys that aren't accepted on View const { color, fontSize, ...styles } = flat let iconComponent // the colour in the url on this site has to be a hex w/o hash const iconColor = color && color.substr(0, 1) === '#' ? `${color.substr(1)}/` : '/' const Search = ( <Image source={{ uri: `https://png.icons8.com/search/${iconColor}ios/` }} style={{ width: size, height: size }} /> ) const Down = ( <Image source={{ uri: `https://png.icons8.com/down/${iconColor}ios/` }} style={{ width: size, height: size }} /> ) const Up = ( <Image source={{ uri: `https://png.icons8.com/up/${iconColor}ios/` }} style={{ width: size, height: size }} /> ) const Close = ( <Image source={{ uri: `https://png.icons8.com/multiply/${iconColor}ios/` }} style={{ width: size, height: size }} /> ) const Check = ( <Image source={{ uri: `https://png.icons8.com/checkmark/${iconColor}android/` }} style={{ width: size / 1.5, height: size / 1.5 }} /> ) const Cancel = ( <Image source={{ uri: `https://png.icons8.com/cancel/${iconColor}ios/` }} style={{ width: size, height: size }} /> ) switch (name) { case 'search': iconComponent = Search break case 'keyboard-arrow-up': iconComponent = Up break case 'keyboard-arrow-down': iconComponent = Down break case 'close': iconComponent = Close break case 'check': iconComponent = Check break case 'cancel': iconComponent = Cancel break default: iconComponent = null break } return <View style={styles}>{iconComponent}</View> } getProp = (object, key) => object && this.removerAcentos(object[key]) rejectProp = (items, fn) => items.filter(fn) pretendToLoad = () => { this.setState({ loading: true }) setTimeout(() => { this.setState({ loading: false, items }) }, 4000) } // testing a custom filtering function that ignores accents removerAcentos = (s) => s.replace(/[\W\[\] ]/g, (a) => accentMap[a] || a) filterItems = (searchTerm, items, { subKey, displayKey, uniqueKey }) => { let filteredItems = [] let newFilteredItems = [] items.forEach((item) => { const parts = this.removerAcentos(searchTerm.trim()).split( /[[ \][)(\\/?\-:]+/ ) const regex = new RegExp(`(${parts.join('|')})`, 'i') if (regex.test(this.getProp(item, displayKey))) { filteredItems.push(item) } if (item[subKey]) { const newItem = Object.assign({}, item) newItem[subKey] = [] item[subKey].forEach((sub) => { if (regex.test(this.getProp(sub, displayKey))) { newItem[subKey] = [...newItem[subKey], sub] newFilteredItems = this.rejectProp( filteredItems, (singleItem) => item[uniqueKey] !== singleItem[uniqueKey] ) newFilteredItems.push(newItem) filteredItems = newFilteredItems } }) } }) return filteredItems } onSelectedItemsChange = (selectedItems) => { console.log(selectedItems, selectedItems.length) if (selectedItems.length >= this.maxItems) { if (selectedItems.length === this.maxItems) { this.setState({ selectedItems }) } this.setState({ maxItems: true }) return } this.setState({ maxItems: false }) const filteredItems = selectedItems.filter( (val) => !this.state.selectedItems2.includes(val) ) this.setState({ selectedItems: filteredItems }) } onSelectedItemsChange2 = (selectedItems) => { const filteredItems = selectedItems.filter( (val) => !this.state.selectedItems.includes(val) ) this.setState({ selectedItems2: filteredItems }) } onConfirm = () => { this.setState({ currentItems: this.state.selectedItems }) } onCancel = () => { this.SectionedMultiSelect._removeAllItems() this.setState({ selectedItems: this.state.currentItems }) console.log(this.state.selectedItems) } onSelectedItemObjectsChange = (selectedItemObjects) => { this.setState({ selectedItemObjects }) console.log(selectedItemObjects) } onSwitchToggle = (k) => { const v = !this.state[k] this.setState({ [k]: v }) } fetchCategories = () => { this.setState({ hasErrored: false }) fetch('http://www.mocky.io/v2/5a5573a22f00005c04beea49?mocky-delay=500ms', { headers: 'no-cache' }) .then((response) => response.json()) .then((responseJson) => { this.setState({ cats: responseJson }) }) .catch((error) => { this.setState({ hasErrored: true }) throw error.message }) } filterDuplicates = (items) => items.sort().reduce((accumulator, current) => { const length = accumulator.length if (length === 0 || accumulator[length - 1] !== current) { accumulator.push(current) } return accumulator }, []) noResults = ( <View key="a" style={styles.center}> <Text>Sorry! No results...</Text> </View> ) handleAddSearchTerm = () => { const searchTerm = this.SectionedMultiSelect._getSearchTerm() const id = (this.termId += 1) if ( searchTerm.length && !(this.state.items || []).some((item) => item.title.includes(searchTerm)) ) { const newItem = { id, title: searchTerm } this.setState((prevState) => ({ items: [...(prevState.items || []), newItem] })) this.onSelectedItemsChange([...this.state.selectedItems, id]) this.SectionedMultiSelect._submitSelection() } } searchAdornment = (searchTerm) => searchTerm.length ? ( <TouchableOpacity style={{ alignItems: 'center', justifyContent: 'center' }} onPress={this.handleAddSearchTerm} > <View style={{}}> <Image source={{ uri: 'https://png.icons8.com/plus' }} style={{ width: 16, height: 16, marginHorizontal: 15 }} /> {/* <Icon size={18} style={{ marginHorizontal: 15 }} name="add" /> */} </View> </TouchableOpacity> ) : null renderSelectText = () => { const { selectedItemObjects } = this.state const selectText = selectedItemObjects.length ? `I like ${selectedItemObjects .map((item, i) => { let label = `${item.title}, ` if (i === selectedItemObjects.length - 2) label = `${item.title} and ` if (i === selectedItemObjects.length - 1) label = `${item.title}.` return label }) .join('')}` : 'Select a fruit' return ( <Text style={{ color: this.state.isDarkMode ? 'white' : 'black', fontSize: 18 }} > {selectText} </Text> ) } SelectOrRemoveAll = () => this.SectionedMultiSelect && ( <TouchableOpacity style={{ justifyContent: 'center', height: 44, borderWidth: 0, paddingHorizontal: 10, backgroundColor: 'darkgrey', alignItems: 'center' }} onPress={ this.state.selectedItems.length ? this.SectionedMultiSelect._removeAllItems : this.SectionedMultiSelect._selectAllItems } > <Text style={{ color: 'white', fontWeight: 'bold' }}> {this.state.selectedItems.length ? 'Remove' : 'Select'} all </Text> </TouchableOpacity> ) onToggleSelector = (toggled) => { console.log('selector is ', toggled ? 'open' : 'closed') } customChipsRenderer = (props) => { console.log('props', props) return ( <View style={{ backgroundColor: 'yellow', padding: 15 }}> <Text>Selected:</Text> {props.selectedItems.map((singleSelectedItem) => { const item = this.SectionedMultiSelect._findItem(singleSelectedItem) if (!item || !item[props.displayKey]) return null return ( <View key={item[props.uniqueKey]} style={{ flex: 0, marginRight: 5, padding: 10, backgroundColor: 'orange' }} > <TouchableOpacity onPress={() => { this.SectionedMultiSelect._removeItem(item) }} > <Text>{item[props.displayKey]}</Text> </TouchableOpacity> </View> ) })} </View> ) } render() { return ( <ScrollView keyboardShouldPersistTaps="always" style={{ backgroundColor: this.state.isDarkMode ? '#333' : '#f8f8f8' }} contentContainerStyle={styles.container} > <Text style={styles.welcome}> React native sectioned multi select example. </Text> <SectionedMultiSelect items={this.state.items} ref={(SectionedMultiSelect) => (this.SectionedMultiSelect = SectionedMultiSelect) } uniqueKey="id" subKey="children" displayKey="title" iconKey="icon" autoFocus modalWithTouchable modalWithSafeAreaView // showCancelButton // headerComponent={this.SelectOrRemoveAll} // hideConfirm loading={this.state.loading} // filterItems={this.filterItems} // alwaysShowSelectText // customChipsRenderer={this.customChipsRenderer} chipsPosition="top" searchAdornment={(searchTerm) => this.searchAdornment(searchTerm)} renderSelectText={this.renderSelectText} // noResultsComponent={this.noResults} loadingComponent={ <Loading hasErrored={this.state.hasErrored} fetchCategories={this.fetchCategories} /> } IconRenderer={Icon} // cancelIconComponent={<Text style={{color:'white'}}>Cancel</Text>} showDropDowns={this.state.showDropDowns} expandDropDowns={this.state.expandDropDowns} animateDropDowns={false} readOnlyHeadings={this.state.readOnlyHeadings} single={this.state.single} showRemoveAll hideChipRemove={this.state.hideChipRemove} selectChildren={this.state.selectChildren} highlightChildren={this.state.highlightChildren} // hideSearch // itemFontFamily={fonts.boldCondensed} onSelectedItemsChange={this.onSelectedItemsChange} onSelectedItemObjectsChange={this.onSelectedItemObjectsChange} onCancel={this.onCancel} onConfirm={this.onConfirm} confirmText={`${this.state.selectedItems.length}/${this.maxItems} - ${ this.state.maxItems ? 'Max selected' : 'Confirm' }`} selectedItems={this.state.selectedItems} colors={{ primary: '#5c3a9e', success: '#5c3a9e', chipColor: this.state.isDarkMode ? '#f7f7f7' : '#333' }} itemNumberOfLines={3} selectLabelNumberOfLines={3} styles={{ // chipText: { // maxWidth: Dimensions.get('screen').width - 90, // }, // itemText: { // color: this.state.selectedItems.length ? 'black' : 'lightgrey' // }, // selectedItemText: { // color: 'blue', // }, // subItemText: { // color: this.state.selectedItems.length ? 'black' : 'lightgrey' // }, item: { paddingHorizontal: 10 }, subItem: { paddingHorizontal: 10 }, selectedItem: { backgroundColor: 'rgba(0,0,0,0.1)' }, selectedSubItem: { backgroundColor: 'rgba(0,0,0,0.1)' }, // selectedSubItemText: { // color: 'blue', // }, scrollView: { paddingHorizontal: 0 } }} // cancelIconComponent={<Icon size={20} name="close" style={{ color: 'white' }} />} /> <View> <View style={styles.border}> <Text style={styles.heading}>Settings</Text> </View> <Toggle name="Single" onPress={() => this.onSwitchToggle('single')} val={this.state.single} /> <Toggle name="Read only headings" onPress={() => this.onSwitchToggle('readOnlyHeadings')} val={this.state.readOnlyHeadings} /> <Toggle name="Expand dropdowns" onPress={() => this.onSwitchToggle('expandDropDowns')} val={this.state.expandDropDowns} disabled={!this.state.showDropDowns} /> <Toggle name="Show dropdown toggles" onPress={() => this.onSwitchToggle('showDropDowns')} val={this.state.showDropDowns} /> <Toggle name="Auto-highlight children" onPress={() => this.onSwitchToggle('highlightChildren')} val={this.state.highlightChildren} disabled={this.state.selectChildren} /> <Toggle name="Auto-select children" onPress={() => this.onSwitchToggle('selectChildren')} disabled={this.state.highlightChildren} val={this.state.selectChildren} /> <Toggle name="Hide Chip Remove Buttons" onPress={() => this.onSwitchToggle('hideChipRemove')} val={this.state.hideChipRemove} /> <TouchableWithoutFeedback onPress={() => this.SectionedMultiSelect._removeAllItems()} > <View style={styles.switch}> <Text style={styles.label}>Remove All</Text> </View> </TouchableWithoutFeedback> </View> </ScrollView> ) } }