forked from Ivasoft/mattermost-mobile
478 lines
15 KiB
JavaScript
478 lines
15 KiB
JavaScript
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
// See LICENSE.txt for license information.
|
|
|
|
import React, {PureComponent} from 'react';
|
|
import PropTypes from 'prop-types';
|
|
import {
|
|
Animated,
|
|
InteractionManager,
|
|
Keyboard,
|
|
TouchableWithoutFeedback,
|
|
StyleSheet,
|
|
View,
|
|
Platform,
|
|
} from 'react-native';
|
|
import {intlShape} from 'react-intl';
|
|
|
|
import {SearchBar} from 'react-native-elements';
|
|
|
|
import {memoizeResult} from '@mm-redux/utils/helpers';
|
|
|
|
import CompassIcon from '@components/compass_icon';
|
|
|
|
const LEFT_COMPONENT_INITIAL_POSITION = Platform.OS === 'ios' ? 7 : 0;
|
|
|
|
export default class Search extends PureComponent {
|
|
static propTypes = {
|
|
testID: PropTypes.string,
|
|
onBlur: PropTypes.func,
|
|
onFocus: PropTypes.func,
|
|
onSearchButtonPress: PropTypes.func,
|
|
onChangeText: PropTypes.func,
|
|
onCancelButtonPress: PropTypes.func,
|
|
onSelectionChange: PropTypes.func,
|
|
backgroundColor: PropTypes.string,
|
|
placeholderTextColor: PropTypes.string,
|
|
titleCancelColor: PropTypes.string,
|
|
tintColorSearch: PropTypes.string,
|
|
tintColorDelete: PropTypes.string,
|
|
selectionColor: PropTypes.string,
|
|
inputStyle: PropTypes.oneOfType([PropTypes.object, PropTypes.number, PropTypes.array]),
|
|
containerStyle: PropTypes.oneOfType([PropTypes.object, PropTypes.number, PropTypes.array]),
|
|
cancelButtonStyle: PropTypes.oneOfType([PropTypes.object, PropTypes.number, PropTypes.array]),
|
|
autoFocus: PropTypes.bool,
|
|
placeholder: PropTypes.string,
|
|
cancelTitle: PropTypes.oneOfType([
|
|
PropTypes.string,
|
|
PropTypes.object,
|
|
]),
|
|
returnKeyType: PropTypes.string,
|
|
keyboardType: PropTypes.string,
|
|
autoCapitalize: PropTypes.string,
|
|
inputHeight: PropTypes.number,
|
|
editable: PropTypes.bool,
|
|
blurOnSubmit: PropTypes.bool,
|
|
keyboardShouldPersist: PropTypes.bool,
|
|
value: PropTypes.string,
|
|
keyboardAppearance: PropTypes.string,
|
|
showArrow: PropTypes.bool,
|
|
searchBarRightMargin: PropTypes.number,
|
|
leftComponent: PropTypes.element,
|
|
searchIconSize: PropTypes.number,
|
|
backArrowSize: PropTypes.number,
|
|
deleteIconSize: PropTypes.number,
|
|
showCancel: PropTypes.bool,
|
|
containerHeight: PropTypes.number,
|
|
};
|
|
|
|
static contextTypes = {
|
|
intl: intlShape,
|
|
};
|
|
|
|
static defaultProps = {
|
|
onSelectionChange: () => true,
|
|
onBlur: () => true,
|
|
editable: true,
|
|
blurOnSubmit: false,
|
|
keyboardShouldPersist: false,
|
|
placeholderTextColor: 'grey',
|
|
value: '',
|
|
showArrow: false,
|
|
showCancel: true,
|
|
searchIconSize: 24,
|
|
backArrowSize: 24,
|
|
deleteIconSize: 20,
|
|
searchBarRightMargin: 0,
|
|
returnKeyType: 'search',
|
|
keyboardType: 'default',
|
|
containerHeight: 40,
|
|
};
|
|
|
|
constructor(props) {
|
|
super(props);
|
|
this.state = {
|
|
leftComponentWidth: 0,
|
|
};
|
|
|
|
this.leftComponentAnimated = new Animated.Value(LEFT_COMPONENT_INITIAL_POSITION);
|
|
this.searchContainerAnimated = new Animated.Value(0);
|
|
}
|
|
|
|
setSearchContainerRef = (ref) => {
|
|
this.searchContainerRef = ref;
|
|
}
|
|
|
|
setInputKeywordRef = (ref) => {
|
|
this.inputKeywordRef = ref;
|
|
}
|
|
|
|
blur = () => {
|
|
this.inputKeywordRef.blur();
|
|
};
|
|
|
|
focus = () => {
|
|
this.inputKeywordRef.focus();
|
|
};
|
|
|
|
onBlur = async () => {
|
|
this.props.onBlur();
|
|
|
|
if (this.props.leftComponent) {
|
|
await this.collapseAnimation();
|
|
}
|
|
};
|
|
|
|
onLeftComponentLayout = (event) => {
|
|
const leftComponentWidth = event.nativeEvent.layout.width;
|
|
this.setState({leftComponentWidth});
|
|
};
|
|
|
|
onSearch = async () => {
|
|
if (this.props.keyboardShouldPersist === false) {
|
|
await Keyboard.dismiss();
|
|
}
|
|
|
|
this.props.onSearchButtonPress(this.props.value);
|
|
};
|
|
|
|
onChangeText = (text) => {
|
|
if (this.props.onChangeText) {
|
|
this.props.onChangeText(text);
|
|
}
|
|
};
|
|
|
|
onFocus = () => {
|
|
InteractionManager.runAfterInteractions(async () => {
|
|
if (this.props.onFocus) {
|
|
this.props.onFocus();
|
|
}
|
|
|
|
if (this.props.leftComponent) {
|
|
await this.expandAnimation();
|
|
}
|
|
});
|
|
};
|
|
|
|
onClear = () => {
|
|
this.focus();
|
|
this.props.onChangeText('', true);
|
|
};
|
|
|
|
onCancel = () => {
|
|
Keyboard.dismiss();
|
|
InteractionManager.runAfterInteractions(() => {
|
|
if (this.props.onCancelButtonPress) {
|
|
this.props.onCancelButtonPress();
|
|
}
|
|
});
|
|
};
|
|
|
|
onSelectionChange = (event) => {
|
|
this.props.onSelectionChange(event);
|
|
};
|
|
|
|
expandAnimation = () => {
|
|
return new Promise((resolve) => {
|
|
Animated.parallel([
|
|
Animated.timing(
|
|
this.leftComponentAnimated,
|
|
{
|
|
toValue: -115,
|
|
duration: 200,
|
|
},
|
|
),
|
|
Animated.timing(
|
|
this.searchContainerAnimated,
|
|
{
|
|
toValue: this.state.leftComponentWidth * -1,
|
|
duration: 200,
|
|
},
|
|
),
|
|
]).start(resolve);
|
|
});
|
|
}
|
|
|
|
collapseAnimation = () => {
|
|
return new Promise((resolve) => {
|
|
Animated.parallel([
|
|
Animated.timing(
|
|
this.leftComponentAnimated,
|
|
{
|
|
toValue: LEFT_COMPONENT_INITIAL_POSITION,
|
|
duration: 200,
|
|
},
|
|
),
|
|
Animated.timing(
|
|
this.searchContainerAnimated,
|
|
{
|
|
toValue: 0,
|
|
duration: 200,
|
|
},
|
|
),
|
|
]).start(resolve);
|
|
});
|
|
}
|
|
|
|
render() {
|
|
const {testID, backgroundColor, inputHeight, inputStyle, placeholderTextColor, tintColorSearch, cancelButtonStyle, tintColorDelete, titleCancelColor, searchBarRightMargin, containerHeight} = this.props;
|
|
const searchClearButtonTestID = `${testID}.search.clear.button`;
|
|
const searchCancelButtonTestID = `${testID}.search.cancel.button`;
|
|
const searchInputTestID = `${testID}.search.input`;
|
|
const searchBarStyle = getSearchBarStyle(
|
|
backgroundColor,
|
|
cancelButtonStyle,
|
|
inputHeight,
|
|
inputStyle,
|
|
placeholderTextColor,
|
|
tintColorDelete,
|
|
tintColorSearch,
|
|
titleCancelColor,
|
|
searchBarRightMargin,
|
|
containerHeight,
|
|
);
|
|
const {intl} = this.context;
|
|
|
|
let clearIcon = null;
|
|
let searchIcon = null;
|
|
let cancelIcon = null;
|
|
|
|
if (Platform.OS === 'ios') {
|
|
clearIcon = (
|
|
<CompassIcon
|
|
testID={searchClearButtonTestID}
|
|
name='close-circle'
|
|
size={18}
|
|
style={{color: searchBarStyle.clearIconColorIos}}
|
|
onPress={this.onClear}
|
|
/>
|
|
);
|
|
|
|
searchIcon = (
|
|
<CompassIcon
|
|
name='magnify'
|
|
size={24}
|
|
style={[
|
|
styles.fullWidth,
|
|
searchBarStyle.searchIcon,
|
|
]}
|
|
/>
|
|
);
|
|
} else {
|
|
searchIcon = this.props.showArrow ?
|
|
(
|
|
<TouchableWithoutFeedback onPress={this.onCancel}>
|
|
<CompassIcon
|
|
testID={searchCancelButtonTestID}
|
|
name='arrow-left'
|
|
size={this.props.backArrowSize}
|
|
color={searchBarStyle.clearIconColorAndroid}
|
|
/>
|
|
</TouchableWithoutFeedback>
|
|
) :
|
|
(
|
|
<CompassIcon
|
|
name='magnify'
|
|
size={this.props.searchIconSize}
|
|
color={searchBarStyle.searchIconColor}
|
|
/>
|
|
);
|
|
|
|
// Making sure the icon won't change depending on whether the input is in focus on Android devices
|
|
cancelIcon = (
|
|
<CompassIcon
|
|
testID={searchCancelButtonTestID}
|
|
name='arrow-left'
|
|
size={25}
|
|
color={searchBarStyle.clearIconColorAndroid}
|
|
onPress={this.onCancel}
|
|
/>
|
|
);
|
|
|
|
clearIcon = (
|
|
<CompassIcon
|
|
testID={searchClearButtonTestID}
|
|
name='close'
|
|
size={this.props.deleteIconSize}
|
|
color={searchBarStyle.clearIconColorAndroid}
|
|
onPress={this.onClear}
|
|
/>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<View
|
|
testID={testID}
|
|
style={[searchBarStyle.container, this.props.containerStyle]}
|
|
>
|
|
{((this.props.leftComponent) ?
|
|
<Animated.View
|
|
style={[styles.leftComponent, {
|
|
left: this.leftComponentAnimated,
|
|
}]}
|
|
onLayout={this.onLeftComponentLayout}
|
|
>
|
|
{this.props.leftComponent}
|
|
</Animated.View> :
|
|
null
|
|
)}
|
|
<Animated.View
|
|
style={[
|
|
styles.fullWidth,
|
|
searchBarStyle.searchBarWrapper,
|
|
{
|
|
marginLeft: this.searchContainerAnimated,
|
|
},
|
|
]}
|
|
>
|
|
<SearchBar
|
|
testID={searchInputTestID}
|
|
autoCapitalize={this.props.autoCapitalize}
|
|
autoCorrect={false}
|
|
autoFocus={this.props.autoFocus}
|
|
blurOnSubmit={this.props.blurOnSubmit}
|
|
cancelButtonProps={{
|
|
buttonTextStyle: {
|
|
...styles.text,
|
|
...searchBarStyle.cancelButtonText,
|
|
},
|
|
}}
|
|
cancelButtonTitle={this.props.cancelTitle || intl.formatMessage({id: 'mobile.post.cancel', defaultMessage: 'Cancel'})}
|
|
cancelIcon={cancelIcon}
|
|
clearIcon={clearIcon}
|
|
containerStyle={{
|
|
...styles.searchContainer,
|
|
...styles.fullWidth,
|
|
...searchBarStyle.searchBarContainer,
|
|
}}
|
|
disableFullscreenUI={true}
|
|
editable={this.props.editable}
|
|
enablesReturnKeyAutomatically={true}
|
|
inputContainerStyle={{
|
|
...styles.inputContainer,
|
|
...searchBarStyle.inputContainer,
|
|
}}
|
|
inputStyle={{
|
|
...styles.text,
|
|
...styles.inputMargin,
|
|
...searchBarStyle.inputStyle,
|
|
}}
|
|
keyboardAppearance={this.props.keyboardAppearance}
|
|
keyboardType={this.props.keyboardType}
|
|
leftIconContainerStyle={styles.leftIcon}
|
|
placeholder={this.props.placeholder || intl.formatMessage({id: 'search_bar.search', defaultMessage: 'Search'})}
|
|
placeholderTextColor={this.props.placeholderTextColor}
|
|
platform={Platform.OS}
|
|
onBlur={this.onBlur}
|
|
onCancel={this.onCancel}
|
|
onChangeText={this.onChangeText}
|
|
onClear={this.onClear}
|
|
onFocus={this.onFocus}
|
|
onSelectionChange={this.onSelectionChange}
|
|
onSubmitEditing={this.onSearch}
|
|
searchIcon={searchIcon}
|
|
selectionColor={this.props.selectionColor}
|
|
showCancel={this.props.showCancel}
|
|
ref={this.setInputKeywordRef}
|
|
returnKeyType={this.props.returnKeyType}
|
|
underlineColorAndroid='transparent'
|
|
value={this.props.value}
|
|
/>
|
|
</Animated.View>
|
|
</View>
|
|
);
|
|
}
|
|
}
|
|
|
|
const getSearchBarStyle = memoizeResult((
|
|
backgroundColor,
|
|
cancelButtonStyle,
|
|
inputHeight,
|
|
inputStyle,
|
|
placeholderTextColor,
|
|
tintColorDelete,
|
|
tintColorSearch,
|
|
titleCancelColor,
|
|
searchBarRightMargin,
|
|
containerHeight,
|
|
) => ({
|
|
cancelButtonText: {
|
|
...cancelButtonStyle,
|
|
color: titleCancelColor,
|
|
},
|
|
container: {
|
|
flexDirection: 'row',
|
|
justifyContent: 'flex-start',
|
|
alignItems: 'center',
|
|
height: containerHeight,
|
|
overflow: 'hidden',
|
|
},
|
|
clearIconColorIos: tintColorDelete || styles.defaultColor.color,
|
|
clearIconColorAndroid: titleCancelColor || placeholderTextColor,
|
|
inputStyle: {
|
|
...inputStyle,
|
|
backgroundColor: 'transparent',
|
|
height: inputHeight,
|
|
},
|
|
inputContainer: {
|
|
backgroundColor: inputStyle.backgroundColor,
|
|
height: inputHeight,
|
|
},
|
|
searchBarWrapper: {
|
|
marginRight: searchBarRightMargin,
|
|
height: Platform.select({
|
|
ios: inputHeight || containerHeight - 10,
|
|
android: inputHeight,
|
|
}),
|
|
},
|
|
searchBarContainer: {
|
|
backgroundColor,
|
|
},
|
|
searchIcon: {
|
|
color: tintColorSearch || placeholderTextColor,
|
|
top: 8,
|
|
},
|
|
searchIconColor: tintColorSearch || placeholderTextColor,
|
|
}));
|
|
|
|
const styles = StyleSheet.create({
|
|
defaultColor: {
|
|
color: 'grey',
|
|
},
|
|
fullWidth: {
|
|
flex: 1,
|
|
},
|
|
inputContainer: {
|
|
borderRadius: Platform.select({
|
|
ios: 2,
|
|
android: 0,
|
|
}),
|
|
},
|
|
inputMargin: {
|
|
marginLeft: 4,
|
|
paddingTop: 0,
|
|
marginTop: Platform.select({
|
|
ios: 0,
|
|
android: 8,
|
|
}),
|
|
},
|
|
leftIcon: {
|
|
marginLeft: 4,
|
|
width: 30,
|
|
},
|
|
searchContainer: {
|
|
paddingTop: 0,
|
|
paddingBottom: 0,
|
|
},
|
|
text: {
|
|
fontSize: Platform.select({
|
|
ios: 14,
|
|
android: 15,
|
|
}),
|
|
color: '#fff',
|
|
},
|
|
leftComponent: {
|
|
position: 'relative',
|
|
marginLeft: 2,
|
|
},
|
|
});
|