react-native-mentions
react-native-mentions copied to clipboard
Adapt to the latest version of ReactNative in 2024.
If there is something that can be merged into the main branch.
source code:
import React, { Component } from 'react';
import {
Text,
View,
Animated,
TextInput,
FlatList,
} from 'react-native';
import PropTypes from 'prop-types';
export default class MentionsTextInput extends Component {
constructor() {
super();
this.state = {
textInputHeight: "",
isTrackingStarted: false,
suggestionRowHeight: new Animated.Value(0),
};
this.isTrackingStarted = false;
this.previousChar = " ";
}
static getDerivedStateFromProps(nextProps, prevState) {
if (!nextProps.value) {
return {
textInputHeight: nextProps.textInputMinHeight,
isTrackingStarted: false,
};
} else if (prevState.isTrackingStarted && !nextProps.horizontal && nextProps.suggestionsData.length !== 0) {
const numOfRows = nextProps.MaxVisibleRowCount >= nextProps.suggestionsData.length ? nextProps.suggestionsData.length : nextProps.MaxVisibleRowCount;
const height = numOfRows * nextProps.suggestionRowHeight;
return {
suggestionRowHeight: new Animated.Value(height),
};
}
return null;
}
componentDidMount() {
this.setState({
textInputHeight: this.props.textInputMinHeight
});
}
startTracking() {
this.isTrackingStarted = true;
this.openSuggestionsPanel();
this.setState({
isTrackingStarted: true
});
}
stopTracking() {
this.isTrackingStarted = false;
this.closeSuggestionsPanel();
this.setState({
isTrackingStarted: false
});
}
openSuggestionsPanel(height) {
Animated.timing(this.state.suggestionRowHeight, {
toValue: height ? height : this.props.suggestionRowHeight,
duration: 100,
useNativeDriver: false,
}).start();
}
closeSuggestionsPanel() {
Animated.timing(this.state.suggestionRowHeight, {
toValue: 0,
duration: 100,
useNativeDriver: false,
}).start();
}
updateSuggestions(lastKeyword) {
this.props.triggerCallback(lastKeyword);
}
identifyKeyword(val) {
if (this.isTrackingStarted) {
const boundary = this.props.triggerLocation === 'new-word-only' ? 'B' : '';
const pattern = new RegExp(`\\${boundary}${this.props.trigger}[a-z0-9_-]+|\\${boundary}${this.props.trigger}`, `gi`);
const keywordArray = val.match(pattern);
if (keywordArray && !!keywordArray.length) {
const lastKeyword = keywordArray[keywordArray.length - 1];
this.updateSuggestions(lastKeyword);
}
}
}
onChangeText(val) {
this.props.onChangeText(val); // pass changed text back
const lastChar = val.substr(val.length - 1);
const wordBoundry = (this.props.triggerLocation === 'new-word-only') ? this.previousChar.trim().length === 0 : true;
if (lastChar === this.props.trigger && wordBoundry) {
this.startTracking();
} else if (lastChar === ' ' && this.state.isTrackingStarted || val === "") {
this.stopTracking();
}
this.previousChar = lastChar;
this.identifyKeyword(val);
}
resetTextbox() {
this.previousChar = " ";
this.stopTracking();
this.setState({ textInputHeight: this.props.textInputMinHeight });
}
render() {
return (
<View>
<Animated.View style={[{ ...this.props.suggestionsPanelStyle }, { height: this.state.suggestionRowHeight }]}>
<FlatList
keyboardShouldPersistTaps={"always"}
horizontal={this.props.horizontal}
ListEmptyComponent={this.props.loadingComponent}
enableEmptySections={true}
data={this.props.suggestionsData}
keyExtractor={this.props.keyExtractor}
renderItem={(rowData) => { return this.props.renderSuggestionsRow(rowData, this.stopTracking.bind(this)) }}
/>
</Animated.View>
<TextInput
{...this.props}
onContentSizeChange={(event) => {
this.setState({
textInputHeight: this.props.textInputMinHeight >= event.nativeEvent.contentSize.height ? this.props.textInputMinHeight : event.nativeEvent.contentSize.height + 10,
});
}}
ref={component => this._textInput = component}
onChangeText={this.onChangeText.bind(this)}
multiline={true}
value={this.props.value}
style={[{ ...this.props.textInputStyle }, { height: Math.min(this.props.textInputMaxHeight, this.state.textInputHeight) }]}
placeholder={this.props.placeholder ? this.props.placeholder : 'Write a comment...'}
/>
</View>
)
}
}
MentionsTextInput.propTypes = {
textInputStyle: PropTypes.object,
suggestionsPanelStyle: PropTypes.object,
loadingComponent: PropTypes.oneOfType([
PropTypes.func,
PropTypes.element,
]),
textInputMinHeight: PropTypes.number,
textInputMaxHeight: PropTypes.number,
trigger: PropTypes.string.isRequired,
triggerLocation: PropTypes.oneOf(['new-word-only', 'anywhere']).isRequired,
value: PropTypes.string.isRequired,
onChangeText: PropTypes.func.isRequired,
triggerCallback: PropTypes.func.isRequired,
renderSuggestionsRow: PropTypes.oneOfType([
PropTypes.func,
PropTypes.element,
]).isRequired,
suggestionsData: PropTypes.array.isRequired,
keyExtractor: PropTypes.func.isRequired,
horizontal: PropTypes.bool,
suggestionRowHeight: PropTypes.number.isRequired,
MaxVisibleRowCount: function(props, propName, componentName) {
if(!props.horizontal && !props.MaxVisibleRowCount) {
return new Error(
`Prop 'MaxVisibleRowCount' is required if horizontal is set to false.`
);
}
}
};
MentionsTextInput.defaultProps = {
textInputStyle: { borderColor: '#ebebeb', borderWidth: 1, fontSize: 15 },
suggestionsPanelStyle: { backgroundColor: 'rgba(100,100,100,0.1)' },
loadingComponent: () => <Text>Loading...</Text>,
textInputMinHeight: 30,
textInputMaxHeight: 80,
horizontal: true,
};
demo
import React, {useRef, useState} from "react";
import {TouchableOpacity, Text, View, ActivityIndicator, StyleSheet} from "react-native";
import tw from "../../utils/tailwind";
import MentionsTextInput from "./MentionsTextInput";
const DemoTags = ()=>{
const [value,setValue]=useState('');
const [data, setData]=useState([
{UserName:'UserName',DisplayName:'DisplayName'},
{UserName:'UserName1',DisplayName:'DisplayName1'},
{UserName:'UserName2',DisplayName:'DisplayName2'},
{UserName:'UserName3',DisplayName:'DisplayName3'},
]);
const [keyword,setKey]=useState('');
function triggerCallback(keyword){
setKey(keyword)
}
function onSuggestionTap(UserName, hidePanel){
hidePanel();
const comment = value.slice(0, - keyword.length)
setValue(comment + '@' + UserName)
}
function renderSuggestionsRow({ item }, hidePanel){
return (
<TouchableOpacity onPress={() => onSuggestionTap(item.UserName, hidePanel)}>
<View style={styles.suggestionsRowContainer}>
<View style={styles.userIconBox}>
<Text style={styles.usernameInitials}>{!!item.DisplayName && item.DisplayName.substring(0, 2).toUpperCase()}</Text>
</View>
<View style={styles.userDetailsBox}>
<Text style={styles.displayNameText}>{item.DisplayName}</Text>
<Text style={styles.usernameText}>@{item.UserName}</Text>
</View>
</View>
</TouchableOpacity>
)
}
return (
<View style={tw`p-10`}>
<MentionsTextInput
textInputStyle={{ borderColor: '#ebebeb', borderWidth: 1, padding: 5, fontSize: 15 }}
suggestionsPanelStyle={{ backgroundColor: 'rgba(100,100,100,0.1)' }}
loadingComponent={() => <View style={{ flex: 1, width, justifyContent: 'center', alignItems: 'center' }}><ActivityIndicator /></View>}
textInputMinHeight={30}
textInputMaxHeight={80}
trigger={'@'}
triggerLocation={'new-word-only'} // 'new-word-only', 'anywhere'
value={value}
onChangeText={(val) => { setValue(val) }}
triggerCallback={triggerCallback}
renderSuggestionsRow={renderSuggestionsRow}
suggestionsData={data} // array of objects
keyExtractor={(item, index) => item.UserName}
suggestionRowHeight={45}
horizontal={false} // default is true, change the orientation of the list
MaxVisibleRowCount={3} // this is required if horizontal={false}
/>
</View>
);
}
export default DemoTags
const styles = StyleSheet.create({
container: {
height: 300,
justifyContent: 'flex-end',
paddingTop: 100
},
suggestionsRowContainer: {
flexDirection: 'row',
},
userAvatarBox: {
width: 35,
paddingTop: 2
},
userIconBox: {
height: 45,
width: 45,
alignItems: 'center',
justifyContent: 'center',
backgroundColor: '#54c19c'
},
usernameInitials: {
color: '#fff',
fontWeight: '800',
fontSize: 14
},
userDetailsBox: {
flex: 1,
justifyContent: 'center',
paddingLeft: 10,
paddingRight: 15
},
displayNameText: {
fontSize: 13,
fontWeight: '500'
},
usernameText: {
fontSize: 12,
color: 'rgba(0,0,0,0.6)'
}
});