Open buildnewapp opened 4 months ago
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)' } });
If there is something that can be merged into the main branch.
source code:
demo