harshq / react-native-mentions

Mentions textbox for React Native. Works on both ios and android. :whale:
MIT License
309 stars 82 forks source link

Adapt to the latest version of ReactNative in 2024. #50

Open buildnewapp opened 4 months ago

buildnewapp commented 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)'
    }
});