react-native-region-picker-modal
Version:
470 lines (413 loc) • 13.2 kB
JavaScript
// @flow
/* eslint import/newline-after-import: 0 */
import React, { Component } from 'react'
import PropTypes from 'prop-types'
import SafeAreaView from 'react-native-safe-area-view'
import {
StyleSheet,
View,
Image,
TouchableOpacity,
Modal,
Text,
TextInput,
FlatList,
ScrollView,
Platform
} from 'react-native'
import Fuse from 'fuse.js'
import { getHeightPercent } from './ratio'
import CloseButton from './CloseButton'
import regionPickerStyles from './RegionPicker.style'
import KeyboardAvoidingView from './KeyboardAvoidingView'
let regions = null
let Emoji = null
let styles = {}
let isEmojiable = Platform.OS === 'ios'
const FLAG_TYPES = {
flat: 'flat',
emoji: 'emoji'
}
const setRegions = flagType => {
if (typeof flagType !== 'undefined') {
isEmojiable = flagType === FLAG_TYPES.emoji
}
if (isEmojiable) {
regions = require('../data/countries-emoji.json')
Emoji = require('./emoji').default
} else {
regions = require('../data/countries.json')
Emoji = <View />
}
}
setRegions()
export default class RegionPicker extends Component {
static propTypes = {
cca2: PropTypes.string.isRequired,
selectedRegion: PropTypes.string.isRequired,
translation: PropTypes.string,
onChange: PropTypes.func.isRequired,
onClose: PropTypes.func,
closeable: PropTypes.bool,
filterable: PropTypes.bool,
children: PropTypes.node,
regionList: PropTypes.array,
excludeRegions: PropTypes.array,
styles: PropTypes.object,
filterPlaceholder: PropTypes.string,
autoFocusFilter: PropTypes.bool,
// to provide a functionality to disable/enable the onPress of Region Picker.
disabled: PropTypes.bool,
filterPlaceholderTextColor: PropTypes.string,
closeButtonImage: PropTypes.oneOfType([PropTypes.number, PropTypes.object]),
transparent: PropTypes.bool,
animationType: PropTypes.oneOf(['slide', 'fade', 'none']),
flagType: PropTypes.oneOf(Object.values(FLAG_TYPES)),
hideAlphabetFilter: PropTypes.bool,
hideCountryFlag: PropTypes.bool,
renderFilter: PropTypes.func,
filterOptions: PropTypes.object,
showRegionNameWithFlag: PropTypes.bool,
iconComponent: PropTypes.element
}
static defaultProps = {
translation: 'eng',
hideCountryFlag: false,
excludeRegions: [],
filterPlaceholder: 'Filter',
autoFocusFilter: true,
transparent: false,
animationType: 'none'
}
static renderEmojiFlag(cca2, emojiStyle) {
return (
<Text style={[regionPickerStyles.emojiFlag, emojiStyle]} allowFontScaling={false}>
{ // TO DO: pass right string to show flag
/* {cca2 !== '' && regions[cca2.toUpperCase()] ? (
<Emoji name={regions[cca2.toUpperCase()].flag} />
) : null} */}
</Text>
)
}
static renderImageFlag(cca2, imageStyle) {
return cca2 !== '' ? (
<Image
resizeMode={'contain'}
style={[regionPickerStyles.imgStyle, imageStyle]}
source={{ uri: regions[cca2].flag }}
/>
) : null
}
static renderFlag(cca2, itemStyle, emojiStyle, imageStyle) {
return (
<View style={[regionPickerStyles.itemCountryFlag, itemStyle]}>
{isEmojiable
? RegionPicker.renderEmojiFlag(cca2, emojiStyle)
: RegionPicker.renderImageFlag(cca2, imageStyle)}
</View>
)
}
static renderFlagWithName(cca2, regionName, itemStyle, emojiStyle, imageStyle, textStyle, iconComponent) {
return (
<View style={{flexDirection:'row', flexWrap:'wrap',alignItems: "center",}}>
<View style={[regionPickerStyles.itemCountryFlag, itemStyle]}>
{isEmojiable
? RegionPicker.renderEmojiFlag(cca2, emojiStyle)
: RegionPicker.renderImageFlag(cca2, imageStyle)}
</View>
<Text style={textStyle}>{regionName}</Text>
{iconComponent}
</View>
)
}
constructor(props) {
super(props)
this.openModal = this.openModal.bind(this)
setRegions(props.flagType)
let regionList = this.props.regionList
regions = regionList
const excludeRegions = [...props.excludeRegions]
excludeRegions.forEach(excludeRegion => {
const index = regionList.indexOf(excludeRegion)
if (index !== -1) {
regionList.splice(index, 1)
}
})
this.state = {
modalVisible: false,
regionList,
flatListMap: regionList.map(n => ({ key: n })),
dataSource: regionList,
filter: '',
letters: this.getLetters(regionList)
}
if (this.props.styles) {
Object.keys(regionPickerStyles).forEach(key => {
styles[key] = StyleSheet.flatten([
regionPickerStyles[key],
this.props.styles[key]
])
})
styles = StyleSheet.create(styles)
} else {
styles = regionPickerStyles
}
const options = Object.assign({
shouldSort: true,
threshold: 0.6,
location: 0,
distance: 100,
maxPatternLength: 32,
minMatchCharLength: 1,
keys: ['name'],
id: 'id'
}, this.props.filterOptions);
this.fuse = new Fuse(
regionList.reduce(
(acc, item) => [
...acc,
{ id: item, name: this.getRegionName(this.props.showCities ? this.props.selectedCity : this.props.selectedRegion) }
],
[]
),
options
)
}
componentDidUpdate(prevProps) {
if (prevProps.regionList[0].name !== this.props.regionList[0].name) {
let regionList = this.props.regionList
regions = regionList
this.setState({
cca2: this.props.cca2,
dataSource: regionList,
flatListMap: regionList.map(n => ({ key: n }))
})
}
}
onSelectRegion(selectedRegion) {
this.setState({
modalVisible: false,
filter: '',
dataSource: this.state.regionList,
flatListMap: this.state.regionList.map(n => ({ key: n }))
})
this.props.onChange({
selectedRegion,
flag: undefined,
name: this.getRegionName(selectedRegion)
})
}
onClose = () => {
this.setState({
modalVisible: false,
filter: '',
dataSource: regions
})
if (this.props.onClose) {
this.props.onClose()
}
}
getRegionName = (selectedRegion, optionalTranslation) => {
const translation = optionalTranslation || this.props.translation || 'eng'
const regionList = this.props.regionList;
//find region based on selectedRegion (region code) else default to first region
let region = regionList.find(region => {
return region.shortCode === selectedRegion
}) || regionList[0]
return region.name
}
setVisibleListHeight(offset) {
this.visibleListHeight = getHeightPercent(100) - offset
}
getLetters(list) {
return Object.keys(
list.reduce(
(acc, val) => ({
...acc,
[val.shortCode[0].toUpperCase()]: ''
}),
{}
)
).sort()
}
openModal = this.openModal.bind(this)
// dimensions of region list and window
itemHeight = getHeightPercent(7)
listHeight = regions.length * this.itemHeight
openModal() {
this.setState({ modalVisible: true })
}
scrollTo(letter) {
const regionList = this.props.regionList;
// find position of first region that starts with letter
const index = regionList
.map(region => this.getRegionName(region.shortCode)[0])
.indexOf(letter);
if (index === -1) {
return
}
let position = index * this.itemHeight
// do not scroll past the end of the list
if (position + this.visibleListHeight > this.listHeight) {
position = this.listHeight - this.visibleListHeight
}
this._flatList.scrollToIndex({ index });
}
handleFilterChange = value => {
const filteredRegions =
value === '' ? regions : this.fuse.search(value)
this._flatList.scrollToOffset({ offset: 0 });
this.setState({
filter: value,
dataSource: filteredRegions,
flatListMap: filteredRegions.map(n => ({ key: n }))
})
}
renderRegion(selectedRegion) {
return (
<TouchableOpacity
key={selectedRegion}
onPress={() => this.onSelectRegion(selectedRegion)}
activeOpacity={0.99}
testID={`country-selector-${this.getRegionName(selectedRegion)}`}
>
{this.renderRegionDetail(selectedRegion)}
</TouchableOpacity>
)
}
renderLetters(letter, index) {
return (
<TouchableOpacity
testID={`letter-${letter}`}
key={index.toString()}
onPress={() => this.scrollTo(letter)}
activeOpacity={0.6}
>
<View style={styles.letter}>
<Text style={styles.letterText} allowFontScaling={false}>
{letter}
</Text>
</View>
</TouchableOpacity>
)
}
renderRegionDetail(selectedRegion) {
return (
<View style={styles.itemCountry}>
{!this.props.hideCountryFlag &&
RegionPicker.renderFlag(selectedRegion,
styles.itemCountryFlag,
styles.emojiFlag,
styles.imgStyle)}
<View style={styles.itemCountryName}>
<Text style={styles.countryName} allowFontScaling={false}>
{this.getRegionName(selectedRegion)}
</Text>
</View>
</View>
)
}
renderFilter = () => {
const {
renderFilter,
autoFocusFilter,
filterPlaceholder,
filterPlaceholderTextColor
} = this.props
const value = this.state.filter
const onChange = this.handleFilterChange
const onClose = this.onClose
return renderFilter ? (
renderFilter({ value, onChange, onClose })
) : (
<TextInput
testID="text-input-country-filter"
autoFocus={autoFocusFilter}
autoCorrect={false}
placeholder={filterPlaceholder}
placeholderTextColor={filterPlaceholderTextColor}
style={[styles.input, !this.props.closeable && styles.inputOnly]}
onChangeText={onChange}
value={value}
/>
)
}
render() {
return (
<View>
<TouchableOpacity
style={styles.container}
disabled={this.props.disabled}
onPress={() => this.setState({ modalVisible: true })}
activeOpacity={0.7}
>
{this.props.children ? (
this.props.children
) : (
<View
style={[styles.touchFlag, { marginTop: isEmojiable ? 0 : 5 }]}
>
{this.props.showRegionNameWithFlag && RegionPicker.renderFlagWithName(this.props.cca2, this.getRegionName(this.props.showCities ? this.props.selectedCity : this.props.selectedRegion),
styles.itemCountryFlag,
styles.emojiFlag,
styles.imgStyle,
styles.textStyle,
this.props.iconComponent)}
{!this.props.showRegionNameWithFlag && RegionPicker.renderFlag(this.props.cca2,
styles.itemCountryFlag,
styles.emojiFlag,
styles.imgStyle)}
</View>
)}
</TouchableOpacity>
<Modal
transparent={this.props.transparent}
animationType={this.props.animationType}
visible={this.state.modalVisible}
onRequestClose={() => this.setState({ modalVisible: false })}
>
<SafeAreaView style={styles.modalContainer}>
<View style={styles.header}>
{this.props.closeable && (
<CloseButton
image={this.props.closeButtonImage}
styles={[styles.closeButton, styles.closeButtonImage]}
onPress={() => this.onClose()}
/>
)}
{this.props.filterable && this.renderFilter()}
</View>
<KeyboardAvoidingView behavior="padding">
<View style={styles.contentContainer}>
<FlatList
testID="list-countries"
keyboardShouldPersistTaps="handled"
data={this.state.flatListMap}
ref={flatList => (this._flatList = flatList)}
initialNumToRender={30}
onScrollToIndexFailed={()=>{}}
renderItem={(region) => this.renderRegion(region.item.key.shortCode)}
keyExtractor={(item, index) => index.toString()}
getItemLayout={(data, index) => (
{ length: this.itemHeight, offset: this.itemHeight * index, index }
)}
/>
{!this.props.hideAlphabetFilter && (
<ScrollView
contentContainerStyle={styles.letters}
keyboardShouldPersistTaps="always"
>
{this.state.filter === '' &&
this.state.letters.map((letter, index) =>
this.renderLetters(letter, index)
)}
</ScrollView>
)}
</View>
</KeyboardAvoidingView>
</SafeAreaView>
</Modal>
</View>
)
}
}