├── .editorconfig ├── .gitignore ├── LICENSE ├── README.md ├── example ├── ChatScreen.js └── ChatScreenStyles.js ├── index.js ├── package.json ├── screens ├── m-zoom-speed-1.gif ├── m-zoom-speed-2.gif ├── m-zoom-speed-3.gif ├── m1.gif ├── m2.gif └── m3.gif ├── src ├── Avatar │ ├── AvatarStyles.js │ └── index.js ├── Editor │ ├── EditorStyles.js │ ├── EditorUtils.js │ └── index.js ├── MentionList │ ├── MentionListStyles.js │ └── index.js └── MentionListItem │ ├── MentionListItemStyles.js │ └── index.js └── yarn.lock /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: http://EditorConfig.org 2 | 3 | # top-most EditorConfig file 4 | root = true 5 | 6 | # Unix-style newlines with a newline ending every file 7 | [*] 8 | end_of_line = lf 9 | insert_final_newline = true 10 | indent_style = space 11 | indent_size = 2 12 | charset = utf-8 13 | trim_trailing_whitespace = true 14 | use-tabs = true 15 | tabWidth = 4 16 | 17 | 18 | [*.gradle] 19 | indent_size = 4 -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # xcode 2 | .DS_Store 3 | 4 | #node 5 | node_modules/ 6 | 7 | #editor config 8 | # .editorconfig -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Muhammad Raza Dar 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # react-native-mentions-editor [![npm version](https://badge.fury.io/js/react-native-mentions-editor.svg)](https://badge.fury.io/js/react-native-mentions-editor) 2 | Mentions TextInput for React Native. Tested on iOS and should work on Android as well. Because it's a plain Javascript base solution with some react-native TextInput support. 3 | - Inspiration [react-native-mentions](https://github.com/harshq/react-native-mentions) 4 | 5 | 6 | ## Installation 7 | 8 | ```yarn add react-native-mentions-editor``` 9 | or 10 | ```npm install --save react-native-mentions-editor``` 11 | 12 | ``` 13 | If you love this component, give a star, you will be a ray of sunshine :) 14 | ``` 15 | 16 | ## Demo 17 | 18 | ![alt text](screens/m1.gif "Screenshots") 19 | ![alt text](screens/m2.gif "Screenshots") 20 | ![alt text](screens/m3.gif "Screenshots") 21 | ![alt text](screens/m-zoom-speed-1.gif "Screenshots") 22 | ![alt text](screens/m-zoom-speed-2.gif "Screenshots") 23 | ![alt text](screens/m-zoom-speed-3.gif "Screenshots") 24 | 25 | ## Usage 26 | 27 | ```js 28 | import Editor, { displayTextWithMentions} from 'react-native-mentions-editor'; 29 | const users = [ 30 | { "id": 1, "name": "Raza Dar", "username": "mrazadar", "gender": "male"}, 31 | { "id": 3, "name": "Atif Rashid", "username": "atif.rashid", "gender": "male"}, 32 | { "id": 4, "name": "Peter Pan", "username": "peter.pan", "gender": "male"}, 33 | { "id": 5, "name": "John Doe", "username": "john.doe", "gender": "male"}, 34 | { "id": 6, "name": "Meesha Shafi", "username": "meesha.shafi", "gender": "female"} 35 | ]; 36 | 47 | 48 | const formatMentionNode = (txt, key)=> ( 49 | 50 | {txt} 51 | 52 | ) 53 | 54 | 55 | {displayTextWithMentions(message.text, formatMentionNode)} 56 | 57 | ``` 58 | ## How it works 59 | 60 | This component allows you to @mention anywhere in the input value. (Not possible using [react-native-mentions](https://github.com/harshq/react-native-mentions)). 61 | Work nicely with selection and highlight of text. This component used special mark-up `@[username](id:1)` to differentiate mentions in the input value. 62 | Whenever input value change the `onChange` callback will be called, with an object containing two properties. 63 | 64 | ```js 65 | this.props.onChange({ 66 | displayText: text,// displayText: "Hey @mrazadar this is good work" 67 | text: this.formatTextWithMentions(text) //text: "Hey @[mrazadar](id:1) this is good work" 68 | }); 69 | ``` 70 | 71 | `displayText` Will have raw text user will see on the screen. You can see that in the comment. 72 | `text` Will have formatted text with some markup to parse mentions on the server and other clients. There is a function called `displayTextWithMentions` you can use this function to parse this mark-up with the parser function (Which format the mention node according to formatter function. Check the example app). 73 | 74 | If you want to only parse mentions in the string but don't want to format them you can use this `EditorUtils.findMentions` function to actually parse the mentions in the string. 75 | This will parse special mark-up `@[username](id:1)` and gives you the exact `positions` and `username` and `id` for that mention. Which you can use for tagging / emailing purposes on the server etc. 76 | You can use this function as: 77 | 78 | 79 | ```js 80 | import { EU as EditorUtils } from 'react-native-mentions-editor'; 81 | EditorUtils.findMentions("Hey @[mrazadar](id:1) this is good work" ); 82 | 83 | //Check the definition of this function 84 | findMentions: (val) => { 85 | /** 86 | * Both Mentions and Selections are 0-th index based in the strings 87 | * meaning their indexes in the string start from 0 88 | * findMentions finds starting and ending positions of mentions in the given text 89 | * @param val string to parse to find mentions 90 | * @returns list of found mentions 91 | */ 92 | let reg = /@\[([^\]]+?)\]\(id:([^\]]+?)\)/igm; 93 | let indexes = []; 94 | while (match = reg.exec(val)) { 95 | indexes.push({ 96 | start: match.index, 97 | end: (reg.lastIndex-1), 98 | username: match[1], 99 | userId: match[2], 100 | type: EU.specialTagsEnum.mention 101 | }); 102 | } 103 | return indexes; 104 | }, 105 | ``` 106 | 107 | ## Props {property : type} 108 | 109 | **`list: array`** This should be the list of objects to be used as options for the mentions list. **Note** This must contain `id` and `username` properties to uniqely identify object in the list. 110 | 111 | **`initialValue: string`** Use this to initialize TextInput with the initial value. Usage. `initalValue: "Hey @[mrazadar](id:1) this is good work"` 112 | 113 | **`clearInput: bool`** When true input will be clear automatically. 114 | 115 | **`onChange: function`** This function will be called on input change event. 116 | 117 | **`showEditor: bool`** Programmatically show/hide editor by using this property. 118 | 119 | **`toggleEditor: function`** Use this to handle `blur` event on input. 120 | 121 | **`showMentions: bool`** Use this property to programmatically trigger the `mentionsList` this will add `@` character in the value. 122 | 123 | **`onHideMentions: function`** This callback will be called when user stop tracking of mention. 124 | 125 | **`placeholder: string`** placeholder for empty input. 126 | 127 | **`renderMentionList: function`** If you want to render totally different list. You can use this property to provide alternative mention list renderer. It will be called with certain properties to controll the functionality of list. 128 | 129 | ```js 130 | renderMentionList Props: object 131 | 132 | mentionListProps= { 133 | list: props.list, //the default list you passed to this component 134 | keyword: state.keyword, //keyword to filter the list. e.g. `@m` 135 | isTrackingStarted: state.isTrackingStarted, // will be true if user started typing `@` 136 | onSuggestionTap: this.onSuggestionTap.bind(this), //this function should be called once user press on the list item 137 | editorStyles: props.editorStyles, // these will be the props passed to the Editor component. 138 | }; 139 | 140 | ``` 141 | **`editorStyles: object`** This object will contain the overriding styles for different nodes. Check the below object to see how you can override styles. 142 | 143 | ```js 144 | editorStyles: { 145 | mainContainer: {}, 146 | editorContainer: {...}, 147 | inputMaskTextWrapper: {}, 148 | inputMaskText: {}, 149 | input: {}, 150 | mentionsListWrapper:{}, 151 | mentionListItemWrapper: {} 152 | mentionListItemTextWrapper: {}, 153 | mentionListItemTitle: {} 154 | mentionListItemUsername: {} 155 | } 156 | ``` 157 | 158 | ## Example 159 | 160 | Check out the full example in [example](https://github.com/mrazadar/react-native-mentions-editor/tree/master/example) folder 161 | 162 | ## License 163 | 164 | [MIT License](http://opensource.org/licenses/mit-license.html). © Muhammad Raza Dar -------------------------------------------------------------------------------- /example/ChatScreen.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import { 3 | ScrollView, 4 | Text, 5 | KeyboardAvoidingView, 6 | View, 7 | TouchableOpacity, 8 | FlatList 9 | } from "react-native"; 10 | // Add Actions - replace 'Your' with whatever your reducer is called :) 11 | // import YourActions from '../Redux/YourRedux' 12 | 13 | import Editor, { EU } from "react-native-mentions-editor"; 14 | 15 | // Styles 16 | import styles from "./Styles/ChatScreenStyles"; 17 | 18 | const users = [ 19 | { id: 1, name: "Raza Dar", username: "mrazadar", gender: "male" }, 20 | { id: 3, name: "Atif Rashid", username: "atif.rashid", gender: "male" }, 21 | { id: 4, name: "Peter Pan", username: "peter.pan", gender: "male" }, 22 | { id: 5, name: "John Doe", username: "john.doe", gender: "male" }, 23 | { id: 6, name: "Meesha Shafi", username: "meesha.shafi", gender: "female" } 24 | ]; 25 | 26 | const formatMentionNode = (txt, key) => ( 27 | 28 | {txt} 29 | 30 | ); 31 | 32 | class ChatScreen extends Component { 33 | constructor(props) { 34 | super(props); 35 | this.state = { 36 | initialValue: 37 | "Hey @[mrazadar](id:1) this is good work. Tell @[john.doe](id:5) to use this package.", 38 | showEditor: true, 39 | message: null, 40 | messages: [], 41 | clearInput: false, 42 | showMentions: false /**use this parameter to programmatically trigger the mentionsList */ 43 | }; 44 | } 45 | onChangeHandler = message => { 46 | /** 47 | * this callback will be called whenever input value change and will have 48 | * formatted value for mentioned syntax 49 | * @message : {text: 'Hey @(mrazadar)(id:1) this is good work.', displayText: `Hey @mrazadar this is good work`} 50 | * */ 51 | 52 | this.setState({ 53 | message, 54 | clearInput: false 55 | }); 56 | }; 57 | sendMessage = () => { 58 | if (!this.state.message) return; 59 | const messages = [this.state.message, ...this.state.messages]; 60 | this.setState({ 61 | messages, 62 | message: null, 63 | clearInput: true 64 | }); 65 | }; 66 | 67 | toggleEditor = () => { 68 | /** 69 | * This callback will be called 70 | * once user left the input field. 71 | * This will handle blur event. 72 | */ 73 | // this.setState({ 74 | // showEditor: false, 75 | // }) 76 | }; 77 | 78 | onHideMentions = () => { 79 | /** 80 | * This callback will be called 81 | * When MentionsList hide due to any user change 82 | */ 83 | this.setState({ 84 | showMentions: false 85 | }); 86 | }; 87 | 88 | renderMessageListItem({ item: message, index }) { 89 | return ( 90 | 91 | 92 | {EU.displayTextWithMentions(message.text, formatMentionNode)} 93 | 94 | 95 | ); 96 | } 97 | renderMessageList() { 98 | return ( 99 | `${message.text}-${index}`} 107 | renderItem={rowData => { 108 | return this.renderMessageListItem(rowData); 109 | }} 110 | /> 111 | ); 112 | } 113 | 114 | render() { 115 | return ( 116 | 117 | 118 | 119 | 120 | React-Native Mentions Package 121 | Built by @mrazadar 122 | 123 | 124 | {this.renderMessageList()} 125 | 126 | 127 | 138 | 142 | Send 143 | 144 | 145 | 146 | 147 | 148 | ); 149 | } 150 | } 151 | 152 | export default ChatScreen; 153 | -------------------------------------------------------------------------------- /example/ChatScreenStyles.js: -------------------------------------------------------------------------------- 1 | import { StyleSheet, Dimensions } from "react-native"; 2 | import { ApplicationStyles } from "../../Themes/"; 3 | // import Colors from '../../Themes/Colors' 4 | 5 | const { width, height } = Dimensions.get("window"); 6 | const screenWidth = width < height ? width : height; 7 | const screenHeight = width < height ? height : width; 8 | 9 | export default StyleSheet.create({ 10 | ...ApplicationStyles.screen, 11 | main: { 12 | flex: 1, 13 | 14 | backgroundColor: "#fff", 15 | height: screenHeight, 16 | 17 | marginTop: 100 18 | }, 19 | container: { 20 | height: screenHeight, 21 | 22 | alignItems: "center", 23 | justifyContent: "space-between" 24 | }, 25 | header: { 26 | // height: 200, 27 | }, 28 | heading: { 29 | fontSize: 24, 30 | fontWeight: "bold" 31 | // color: 'green' 32 | }, 33 | sub: { 34 | color: "rgba(0, 0, 0, 0.4)", 35 | fontSize: 12, 36 | textAlign: "center" 37 | }, 38 | messageList: { 39 | paddingVertical: 50 40 | }, 41 | messageText: {}, 42 | 43 | footer: { 44 | backgroundColor: "lightgreen", 45 | height: 200, 46 | width: screenWidth, 47 | flexDirection: "row", 48 | justifyContent: "space-between", 49 | alignItems: "center", 50 | marginBottom: 100, 51 | padding: 15 52 | }, 53 | sendBtn: { 54 | width: 50, 55 | height: 40, 56 | backgroundColor: "green", 57 | borderRadius: 6, 58 | marginLeft: 5, 59 | justifyContent: "center", 60 | textAlign: "center" 61 | }, 62 | sendBtnText: { 63 | fontSize: 18, 64 | fontWeight: "bold", 65 | color: "#fff", 66 | textAlign: "center" 67 | }, 68 | mention: { 69 | fontSize: 16, 70 | fontWeight: "400", 71 | backgroundColor: "rgba(36, 77, 201, 0.05)", 72 | color: "#244dc9" 73 | } 74 | }); 75 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | import Editor from './src/Editor'; 2 | export * from './src/Editor/EditorUtils'; 3 | export default Editor; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-native-mentions-editor", 3 | "version": "1.0.11", 4 | "description": "Mentions for React-Native. Tested on iOS. Should work on Andriod.", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/mrazadar/react-native-mentions-editor.git" 12 | }, 13 | "keywords": [ 14 | "react-native-mentions", 15 | "react native mentions", 16 | "react-native", 17 | "react-component", 18 | "editor", 19 | "mobile", 20 | "ios", 21 | "android", 22 | "mentions" 23 | ], 24 | "author": "Muhammad Raza Dar (https://about.me/mrazadar)", 25 | "license": "MIT", 26 | "bugs": { 27 | "url": "https://github.com/mrazadar/react-native-mentions-editor/issues" 28 | }, 29 | "homepage": "https://github.com/mrazadar/react-native-mentions-editor#readme" 30 | } 31 | -------------------------------------------------------------------------------- /screens/m-zoom-speed-1.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mrazadar/react-native-mentions-editor/ea1fe89d2e80e80ec46ebef3f1b4e73407bb2a2a/screens/m-zoom-speed-1.gif -------------------------------------------------------------------------------- /screens/m-zoom-speed-2.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mrazadar/react-native-mentions-editor/ea1fe89d2e80e80ec46ebef3f1b4e73407bb2a2a/screens/m-zoom-speed-2.gif -------------------------------------------------------------------------------- /screens/m-zoom-speed-3.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mrazadar/react-native-mentions-editor/ea1fe89d2e80e80ec46ebef3f1b4e73407bb2a2a/screens/m-zoom-speed-3.gif -------------------------------------------------------------------------------- /screens/m1.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mrazadar/react-native-mentions-editor/ea1fe89d2e80e80ec46ebef3f1b4e73407bb2a2a/screens/m1.gif -------------------------------------------------------------------------------- /screens/m2.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mrazadar/react-native-mentions-editor/ea1fe89d2e80e80ec46ebef3f1b4e73407bb2a2a/screens/m2.gif -------------------------------------------------------------------------------- /screens/m3.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mrazadar/react-native-mentions-editor/ea1fe89d2e80e80ec46ebef3f1b4e73407bb2a2a/screens/m3.gif -------------------------------------------------------------------------------- /src/Avatar/AvatarStyles.js: -------------------------------------------------------------------------------- 1 | import { StyleSheet } from "react-native"; 2 | 3 | export default StyleSheet.create({ 4 | wrapper: { 5 | width: 40, 6 | height: 40, 7 | borderRadius: 50, 8 | marginRight: 5, 9 | alignItems: "center", 10 | justifyContent: "center" 11 | }, 12 | name: { 13 | fontWeight: "bold", 14 | fontSize: 16, 15 | color: "#fff" 16 | } 17 | }); 18 | -------------------------------------------------------------------------------- /src/Avatar/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | 4 | import { View, Text } from "react-native"; 5 | 6 | import styles from "./AvatarStyles"; 7 | 8 | const getFirstChar = str => str.charAt(0).toUpperCase(); 9 | 10 | const alphabetColors = [ 11 | "#FFD552", 12 | "#ffca0b", 13 | "#9C0D05", 14 | "#E1DB00", 15 | "#E99600", 16 | "#E1DB00", 17 | "#06BC0C", 18 | "#06BCAE", 19 | "#0695BC", 20 | "#0660BC", 21 | "#3006BC", 22 | "#6606BC", 23 | "#c31616", 24 | "#BC0680", 25 | "#BC0642", 26 | "#BC3406", 27 | "#BCA106", 28 | "#535322", 29 | "#497724", 30 | "#929292", 31 | "#606060", 32 | "#262626", 33 | "#7B9FAB", 34 | "#1393BD", 35 | "#5E13BD", 36 | "#E208A7" 37 | ]; 38 | 39 | const UserThumbnail = props => { 40 | const { user } = props; 41 | let name = user && user.name; 42 | if (!name || name === "") { 43 | if (user && user.first_name && user.last_name) { 44 | name = `${user.first_name} ${user.last_name}`; 45 | } else { 46 | return null; 47 | } 48 | } 49 | const text = getFirstChar(name); 50 | const bgIndex = Math.floor(text.charCodeAt(0) % alphabetColors.length); 51 | const bgColor = alphabetColors[bgIndex]; 52 | 53 | const thumbnail = ( 54 | 61 | {`${text}`} 62 | 63 | ); 64 | return thumbnail; 65 | }; 66 | 67 | UserThumbnail.propTypes = { 68 | user: PropTypes.object, 69 | wrapperStyles: PropTypes.object, 70 | charStyles: PropTypes.object, 71 | to: PropTypes.string 72 | }; 73 | 74 | export default UserThumbnail; 75 | -------------------------------------------------------------------------------- /src/Editor/EditorStyles.js: -------------------------------------------------------------------------------- 1 | import { StyleSheet } from "react-native"; 2 | 3 | export default StyleSheet.create({ 4 | container: { 5 | backgroundColor: "#fff", 6 | borderColor: "green", 7 | borderWidth: 1, 8 | width: 300 9 | }, 10 | textContainer: { 11 | alignSelf: "stretch", 12 | position: "relative", 13 | minHeight: 40, 14 | maxHeight: 140 15 | }, 16 | input: { 17 | fontSize: 16, 18 | color: "#000", 19 | fontWeight: "400", 20 | paddingHorizontal: 20, 21 | minHeight: 40, 22 | position: "absolute", 23 | top: 0, 24 | color: "transparent", 25 | alignSelf: "stretch", 26 | width: "100%" 27 | }, 28 | formmatedTextWrapper: { 29 | minHeight: 40, 30 | position: "absolute", 31 | top: 0, 32 | paddingHorizontal: 20, 33 | paddingVertical: 5, 34 | width: "100%" 35 | }, 36 | formmatedText: { 37 | fontSize: 16, 38 | fontWeight: "400" 39 | }, 40 | mention: { 41 | fontSize: 16, 42 | fontWeight: "400", 43 | backgroundColor: "rgba(36, 77, 201, 0.05)", 44 | color: "#244dc9" 45 | }, 46 | placeholderText: { 47 | color: "rgba(0, 0, 0, 0.1)", 48 | fontSize: 16 49 | } 50 | }); 51 | -------------------------------------------------------------------------------- /src/Editor/EditorUtils.js: -------------------------------------------------------------------------------- 1 | /** 2 | * EditorUtils contains helper 3 | * functions for our Editor 4 | */ 5 | 6 | export const displayTextWithMentions = (inputText, formatMentionNode) => { 7 | /** 8 | * Use this function to parse mentions markup @[username](id) in the string value. 9 | */ 10 | if (inputText === "") return null; 11 | const retLines = inputText.split("\n"); 12 | const formattedText = []; 13 | retLines.forEach((retLine, rowIndex) => { 14 | const mentions = EU.findMentions(retLine); 15 | if (mentions.length) { 16 | let lastIndex = 0; 17 | mentions.forEach((men, index) => { 18 | const initialStr = retLine.substring(lastIndex, men.start); 19 | lastIndex = men.end + 1; 20 | formattedText.push(initialStr); 21 | const formattedMention = formatMentionNode( 22 | `@${men.username}`, 23 | `${index}-${men.id}-${rowIndex}` 24 | ); 25 | formattedText.push(formattedMention); 26 | if (mentions.length - 1 === index) { 27 | const lastStr = retLine.substr(lastIndex); //remaining string 28 | formattedText.push(lastStr); 29 | } 30 | }); 31 | } else { 32 | formattedText.push(retLine); 33 | } 34 | formattedText.push("\n"); 35 | }); 36 | return formattedText; 37 | }; 38 | 39 | export const EU = { 40 | specialTagsEnum: { 41 | mention: "mention", 42 | strong: "strong", 43 | italic: "italic", 44 | underline: "underline" 45 | }, 46 | isKeysAreSame: (src, dest) => src.toString() === dest.toString(), 47 | getLastItemInMap: map => Array.from(map)[map.size - 1], 48 | getLastKeyInMap: map => Array.from(map.keys())[map.size - 1], 49 | getLastValueInMap: map => Array.from(map.values())[map.size - 1], 50 | updateRemainingMentionsIndexes: (map, { start, end }, diff, shouldAdd) => { 51 | var newMap = new Map(map); 52 | const keys = EU.getSelectedMentionKeys(newMap, { start, end }); 53 | keys.forEach(key => { 54 | const newKey = shouldAdd 55 | ? [key[0] + diff, key[1] + diff] 56 | : [key[0] - diff, key[1] - diff]; 57 | const value = newMap.get(key); 58 | newMap.delete(key); 59 | //ToDo+ push them in the same order. 60 | newMap.set(newKey, value); 61 | }); 62 | return newMap; 63 | }, 64 | getSelectedMentionKeys: (map, { start, end }) => { 65 | // mention [2, 5], 66 | // selection [3, 6] 67 | const mantionKeys = [...map.keys()]; 68 | const keys = mantionKeys.filter( 69 | ([a, b]) => EU.between(a, start, end) || EU.between(b, start, end) 70 | ); 71 | return keys; 72 | }, 73 | findMentionKeyInMap: (map, cursorIndex) => { 74 | // const keys = Array.from(map.keys()) 75 | // OR 76 | const keys = [...map.keys()]; 77 | const key = keys.filter(([a, b]) => EU.between(cursorIndex, a, b))[0]; 78 | return key; 79 | }, 80 | addMenInSelection: (selection, prevSelc, mentions) => { 81 | /** 82 | * Both Mentions and Selections are 0-th index based in the strings 83 | * meaning their indexes in the string start from 0 84 | * While user made a selection automatically add mention in the selection. 85 | */ 86 | const sel = { ...selection }; 87 | mentions.forEach((value, [menStart, menEnd]) => { 88 | if (EU.diff(prevSelc.start, prevSelc.end) < EU.diff(sel.start, sel.end)) { 89 | //user selecting. 90 | if (EU.between(sel.start, menStart, menEnd)) { 91 | //move sel to the start of mention 92 | sel.start = menStart; //both men and selection is 0th index 93 | } 94 | if (EU.between(sel.end - 1, menStart, menEnd)) { 95 | //move sel to the end of mention 96 | sel.end = menEnd + 1; 97 | } 98 | } else { 99 | //previousSelection.diff > currentSelection.diff //user deselecting. 100 | if (EU.between(sel.start, menStart, menEnd)) { 101 | //deselect mention to the end of mention 102 | sel.start = menEnd + 1; 103 | } 104 | if (EU.between(sel.end, menStart, menEnd)) { 105 | //deselect mention to the start of mention 106 | sel.end = menStart; 107 | } 108 | } 109 | }); 110 | return sel; 111 | }, 112 | moveCursorToMentionBoundry: ( 113 | selection, 114 | prevSelc, 115 | mentions, 116 | isTrackingStarted 117 | ) => { 118 | /** 119 | * Both Mentions and Selections are 0-th index based in the strings 120 | * moveCursorToMentionBoundry will move cursor to the start 121 | * or to the end of mention based on user traverse direction. 122 | */ 123 | 124 | const sel = { ...selection }; 125 | if (isTrackingStarted) return sel; 126 | mentions.forEach((value, [menStart, menEnd]) => { 127 | if (prevSelc.start > sel.start) { 128 | //traversing Right -to- Left <= 129 | if (EU.between(sel.start, menStart, menEnd)) { 130 | //move cursor to the start of mention 131 | sel.start = menStart; 132 | sel.end = menStart; 133 | } 134 | } else { 135 | //traversing Left -to- Right => 136 | if (EU.between(sel.start - 1, menStart, menEnd)) { 137 | //move cursor to the end of selection 138 | sel.start = menEnd + 1; 139 | sel.end = menEnd + 1; 140 | } 141 | } 142 | }); 143 | return sel; 144 | }, 145 | between: (x, min, max) => x >= min && x <= max, 146 | sum: (x, y) => x + y, 147 | diff: (x, y) => Math.abs(x - y), 148 | isEmpty: str => str === "", 149 | getMentionsWithInputText: inputText => { 150 | /** 151 | * translate provided string e.g. `Hey @[mrazadar](id:1) this is good work.` 152 | * populate mentions map with [start, end] : {...user} 153 | * translate inputText to desired format; `Hey @mrazadar this is good work.` 154 | */ 155 | 156 | const map = new Map(); 157 | let newValue = ""; 158 | 159 | if (inputText === "") return null; 160 | const retLines = inputText.split("\n"); 161 | 162 | retLines.forEach((retLine, rowIndex) => { 163 | const mentions = EU.findMentions(retLine); 164 | if (mentions.length) { 165 | let lastIndex = 0; 166 | let endIndexDiff = 0; 167 | mentions.forEach((men, index) => { 168 | newValue = newValue.concat(retLine.substring(lastIndex, men.start)); 169 | const username = `@${men.username}`; 170 | newValue = newValue.concat(username); 171 | const menEndIndex = men.start + (username.length - 1); 172 | map.set([men.start - endIndexDiff, menEndIndex - endIndexDiff], { 173 | id: men.id, 174 | username: men.username 175 | }); 176 | //indexes diff with the new formatted string. 177 | endIndexDiff = endIndexDiff + Math.abs(men.end - menEndIndex); 178 | //update last index 179 | lastIndex = men.end + 1; 180 | if (mentions.length - 1 === index) { 181 | const lastStr = retLine.substr(lastIndex); //remaining string 182 | newValue = newValue.concat(lastStr); 183 | } 184 | }); 185 | } else { 186 | newValue = newValue.concat(retLine); 187 | } 188 | if (rowIndex !== retLines.length - 1) { 189 | newValue = newValue.concat("\n"); 190 | } 191 | }); 192 | return { 193 | map, 194 | newValue 195 | }; 196 | }, 197 | findMentions: val => { 198 | /** 199 | * Both Mentions and Selections are 0-th index based in the strings 200 | * meaning their indexes in the string start from 0 201 | * findMentions finds starting and ending positions of mentions in the given text 202 | * @param val string to parse to find mentions 203 | * @returns list of found mentions 204 | */ 205 | let reg = /@\[([^\]]+?)\]\(id:([^\]]+?)\)/gim; 206 | let indexes = []; 207 | while ((match = reg.exec(val))) { 208 | indexes.push({ 209 | start: match.index, 210 | end: reg.lastIndex - 1, 211 | username: match[1], 212 | id: match[2], 213 | type: EU.specialTagsEnum.mention 214 | }); 215 | } 216 | return indexes; 217 | }, 218 | whenTrue: (next, current, key) => { 219 | /** 220 | * whenTrue function will be used to check the 221 | * boolean props for the component 222 | * @params {current, next, key} 223 | * @next: this.props 224 | * @current: nextProps 225 | * @key: key to lookup in both objects 226 | * and will only returns true. if nextProp is true 227 | * and nextProp is a different version/value from 228 | * previous prop 229 | */ 230 | return next[key] && next[key] !== current[key]; 231 | }, 232 | displayTextWithMentions: displayTextWithMentions 233 | }; 234 | 235 | export default EU; 236 | -------------------------------------------------------------------------------- /src/Editor/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | 4 | import { 5 | View, 6 | TextInput, 7 | Text, 8 | Animated, 9 | Platform, 10 | ScrollView 11 | } from "react-native"; 12 | 13 | import EU from "./EditorUtils"; 14 | import styles from "./EditorStyles"; 15 | import MentionList from "../MentionList"; 16 | 17 | export class Editor extends React.Component { 18 | static propTypes = { 19 | list: PropTypes.array, 20 | initialValue: PropTypes.string, 21 | clearInput: PropTypes.bool, 22 | onChange: PropTypes.func, 23 | showEditor: PropTypes.bool, 24 | toggleEditor: PropTypes.func, 25 | showMentions: PropTypes.bool, 26 | onHideMentions: PropTypes.func, 27 | editorStyles: PropTypes.object, 28 | placeholder: PropTypes.string, 29 | renderMentionList: PropTypes.func 30 | }; 31 | 32 | constructor(props) { 33 | super(props); 34 | this.mentionsMap = new Map(); 35 | let msg = ""; 36 | let formattedMsg = ""; 37 | if (props.initialValue && props.initialValue !== "") { 38 | const { map, newValue } = EU.getMentionsWithInputText(props.initialValue); 39 | this.mentionsMap = map; 40 | msg = newValue; 41 | formattedMsg = this.formatText(newValue); 42 | setTimeout(()=>{ 43 | this.sendMessageToFooter(newValue); 44 | }); 45 | } 46 | this.state = { 47 | clearInput: props.clearInput, 48 | inputText: msg, 49 | formattedText: formattedMsg, 50 | keyword: "", 51 | textInputHeight: "", 52 | isTrackingStarted: false, 53 | suggestionRowHeight: new Animated.Value(0), 54 | triggerLocation: "anywhere", //'new-words-only', //anywhere 55 | trigger: "@", 56 | selection: { 57 | start: 0, 58 | end: 0 59 | }, 60 | menIndex: 0, 61 | showMentions: false, 62 | editorHeight: 72, 63 | scrollContentInset: { top: 0, bottom: 0, left: 0, right: 0 }, 64 | placeholder: props.placeholder || "Type something..." 65 | }; 66 | this.isTrackingStarted = false; 67 | this.previousChar = " "; 68 | } 69 | static getDerivedStateFromProps(nextProps, prevState) { 70 | if (nextProps.clearInput !== prevState.clearInput) { 71 | return { clearInput: nextProps.clearInput }; 72 | } 73 | 74 | if (nextProps.showMentions && !prevState.showMentions) { 75 | const newInputText = `${prevState.inputText}${prevState.trigger}`; 76 | return { 77 | inputText: newInputText, 78 | showMentions: nextProps.showMentions 79 | }; 80 | } 81 | 82 | if (!nextProps.showMentions) { 83 | return { 84 | showMentions: nextProps.showMentions 85 | }; 86 | } 87 | return null; 88 | } 89 | 90 | componentDidUpdate(prevProps, prevState) { 91 | // only update chart if the data has changed 92 | if (this.state.inputText !== "" && this.state.clearInput) { 93 | this.setState({ 94 | inputText: "", 95 | formattedText: "" 96 | }); 97 | this.mentionsMap.clear(); 98 | } 99 | 100 | if (EU.whenTrue(this.props, prevProps, "showMentions")) { 101 | //don't need to close on false; user show select it. 102 | this.onChange(this.state.inputText, true); 103 | } 104 | } 105 | 106 | updateMentionsMap(selection, count, shouldAdd) { 107 | this.mentionsMap = EU.updateRemainingMentionsIndexes( 108 | this.mentionsMap, 109 | selection, 110 | count, 111 | shouldAdd 112 | ); 113 | } 114 | 115 | startTracking(menIndex) { 116 | this.isTrackingStarted = true; 117 | this.menIndex = menIndex; 118 | this.setState({ 119 | keyword: "", 120 | menIndex, 121 | isTrackingStarted: true 122 | }); 123 | } 124 | 125 | stopTracking() { 126 | this.isTrackingStarted = false; 127 | // this.closeSuggestionsPanel(); 128 | this.setState({ 129 | isTrackingStarted: false 130 | }); 131 | this.props.onHideMentions(); 132 | } 133 | 134 | updateSuggestions(lastKeyword) { 135 | this.setState({ 136 | keyword: lastKeyword 137 | }); 138 | } 139 | 140 | resetTextbox() { 141 | this.previousChar = " "; 142 | this.stopTracking(); 143 | this.setState({ textInputHeight: this.props.textInputMinHeight }); 144 | } 145 | 146 | identifyKeyword(inputText) { 147 | /** 148 | * filter the mentions list 149 | * according to what user type with 150 | * @ char e.g. @billroy 151 | */ 152 | if (this.isTrackingStarted) { 153 | let pattern = null; 154 | if (this.state.triggerLocation === "new-word-only") { 155 | pattern = new RegExp( 156 | `\\B${this.state.trigger}[a-z0-9_-]+|\\B${this.state.trigger}`, 157 | `gi` 158 | ); 159 | } else { 160 | //anywhere 161 | pattern = new RegExp( 162 | `\\${this.state.trigger}[a-z0-9_-]+|\\${this.state.trigger}`, 163 | `i` 164 | ); 165 | } 166 | const str = inputText.substr(this.menIndex); 167 | const keywordArray = str.match(pattern); 168 | if (keywordArray && !!keywordArray.length) { 169 | const lastKeyword = keywordArray[keywordArray.length - 1]; 170 | this.updateSuggestions(lastKeyword); 171 | } 172 | } 173 | } 174 | 175 | checkForMention(inputText, selection) { 176 | /** 177 | * Open mentions list if user 178 | * start typing @ in the string anywhere. 179 | */ 180 | const menIndex = selection.start - 1; 181 | // const lastChar = inputText.substr(inputText.length - 1); 182 | const lastChar = inputText.substr(menIndex, 1); 183 | const wordBoundry = 184 | this.state.triggerLocation === "new-word-only" 185 | ? this.previousChar.trim().length === 0 186 | : true; 187 | if (lastChar === this.state.trigger && wordBoundry) { 188 | this.startTracking(menIndex); 189 | } else if (lastChar.trim() === "" && this.state.isTrackingStarted) { 190 | this.stopTracking(); 191 | } 192 | this.previousChar = lastChar; 193 | this.identifyKeyword(inputText); 194 | } 195 | 196 | getInitialAndRemainingStrings(inputText, menIndex) { 197 | /** 198 | * extractInitialAndRemainingStrings 199 | * this function extract the initialStr and remainingStr 200 | * at the point of new Mention string. 201 | * Also updates the remaining string if there 202 | * are any adjcent mentions text with the new one. 203 | */ 204 | // const {inputText, menIndex} = this.state; 205 | let initialStr = inputText.substr(0, menIndex).trim(); 206 | if (!EU.isEmpty(initialStr)) { 207 | initialStr = initialStr + " "; 208 | } 209 | /** 210 | * remove the characters adjcent with @ sign 211 | * and extract the remaining part 212 | */ 213 | let remStr = 214 | inputText 215 | .substr(menIndex + 1) 216 | .replace(/\s+/, "\x01") 217 | .split("\x01")[1] || ""; 218 | 219 | /** 220 | * check if there are any adjecent mentions 221 | * subtracted in current selection. 222 | * add the adjcent mentions 223 | * @tim@nic 224 | * add nic back 225 | */ 226 | const adjMentIndexes = { 227 | start: initialStr.length - 1, 228 | end: inputText.length - remStr.length - 1 229 | }; 230 | const mentionKeys = EU.getSelectedMentionKeys( 231 | this.mentionsMap, 232 | adjMentIndexes 233 | ); 234 | mentionKeys.forEach(key => { 235 | remStr = `@${this.mentionsMap.get(key).username} ${remStr}`; 236 | }); 237 | return { 238 | initialStr, 239 | remStr 240 | }; 241 | } 242 | 243 | onSuggestionTap = user => { 244 | /** 245 | * When user select a mention. 246 | * Add a mention in the string. 247 | * Also add a mention in the map 248 | */ 249 | const { inputText, menIndex } = this.state; 250 | const { initialStr, remStr } = this.getInitialAndRemainingStrings( 251 | inputText, 252 | menIndex 253 | ); 254 | 255 | const username = `@${user.username}`; 256 | const text = `${initialStr}${username} ${remStr}`; 257 | //'@[__display__](__id__)' ///find this trigger parsing from react-mentions 258 | 259 | //set the mentions in the map. 260 | const menStartIndex = initialStr.length; 261 | const menEndIndex = menStartIndex + (username.length - 1); 262 | 263 | this.mentionsMap.set([menStartIndex, menEndIndex], user); 264 | 265 | // update remaining mentions indexes 266 | let charAdded = Math.abs(text.length - inputText.length); 267 | this.updateMentionsMap( 268 | { 269 | start: menEndIndex + 1, 270 | end: text.length 271 | }, 272 | charAdded, 273 | true 274 | ); 275 | 276 | this.setState({ 277 | inputText: text, 278 | formattedText: this.formatText(text) 279 | }); 280 | this.stopTracking(); 281 | this.sendMessageToFooter(text); 282 | }; 283 | 284 | handleSelectionChange = ({ nativeEvent: { selection } }) => { 285 | const prevSelc = this.state.selection; 286 | let newSelc = { ...selection }; 287 | if (newSelc.start !== newSelc.end) { 288 | /** 289 | * if user make or remove selection 290 | * Automatically add or remove mentions 291 | * in the selection. 292 | */ 293 | newSelc = EU.addMenInSelection(newSelc, prevSelc, this.mentionsMap); 294 | } 295 | // else{ 296 | /** 297 | * Update cursor to not land on mention 298 | * Automatically skip mentions boundry 299 | */ 300 | // setTimeout(()=>{ 301 | 302 | // }) 303 | // newSelc = EU.moveCursorToMentionBoundry(newSelc, prevSelc, this.mentionsMap, this.isTrackingStarted); 304 | // } 305 | this.setState({ selection: newSelc }); 306 | }; 307 | 308 | formatMentionNode = (txt, key) => ( 309 | 310 | {txt} 311 | 312 | ); 313 | 314 | formatText(inputText) { 315 | /** 316 | * Format the Mentions 317 | * and display them with 318 | * the different styles 319 | */ 320 | if (inputText === "" || !this.mentionsMap.size) return inputText; 321 | const formattedText = []; 322 | let lastIndex = 0; 323 | this.mentionsMap.forEach((men, [start, end]) => { 324 | const initialStr = 325 | start === 1 ? "" : inputText.substring(lastIndex, start); 326 | lastIndex = end + 1; 327 | formattedText.push(initialStr); 328 | const formattedMention = this.formatMentionNode( 329 | `@${men.username}`, 330 | `${start}-${men.id}-${end}` 331 | ); 332 | formattedText.push(formattedMention); 333 | if ( 334 | EU.isKeysAreSame(EU.getLastKeyInMap(this.mentionsMap), [start, end]) 335 | ) { 336 | const lastStr = inputText.substr(lastIndex); //remaining string 337 | formattedText.push(lastStr); 338 | } 339 | }); 340 | return formattedText; 341 | } 342 | 343 | formatTextWithMentions(inputText) { 344 | if (inputText === "" || !this.mentionsMap.size) return inputText; 345 | let formattedText = ""; 346 | let lastIndex = 0; 347 | this.mentionsMap.forEach((men, [start, end]) => { 348 | const initialStr = 349 | start === 1 ? "" : inputText.substring(lastIndex, start); 350 | lastIndex = end + 1; 351 | formattedText = formattedText.concat(initialStr); 352 | formattedText = formattedText.concat(`@[${men.username}](id:${men.id})`); 353 | if ( 354 | EU.isKeysAreSame(EU.getLastKeyInMap(this.mentionsMap), [start, end]) 355 | ) { 356 | const lastStr = inputText.substr(lastIndex); //remaining string 357 | formattedText = formattedText.concat(lastStr); 358 | } 359 | }); 360 | return formattedText; 361 | } 362 | 363 | sendMessageToFooter(text) { 364 | this.props.onChange({ 365 | displayText: text, 366 | text: this.formatTextWithMentions(text) 367 | }); 368 | } 369 | 370 | onChange = (inputText, fromAtBtn) => { 371 | let text = inputText; 372 | const prevText = this.state.inputText; 373 | let selection = { ...this.state.selection }; 374 | if (fromAtBtn) { 375 | //update selection but don't set in state 376 | //it will be auto set by input 377 | selection.start = selection.start + 1; 378 | selection.end = selection.end + 1; 379 | } 380 | if (text.length < prevText.length) { 381 | /** 382 | * if user is back pressing and it 383 | * deletes the mention remove it from 384 | * actual string. 385 | */ 386 | 387 | let charDeleted = Math.abs(text.length - prevText.length); 388 | const totalSelection = { 389 | start: selection.start, 390 | end: charDeleted > 1 ? selection.start + charDeleted : selection.start 391 | }; 392 | /** 393 | * REmove all the selected mentions 394 | */ 395 | if (totalSelection.start === totalSelection.end) { 396 | //single char deleting 397 | const key = EU.findMentionKeyInMap( 398 | this.mentionsMap, 399 | totalSelection.start 400 | ); 401 | if (key && key.length) { 402 | this.mentionsMap.delete(key); 403 | /** 404 | * don't need to worry about multi-char selection 405 | * because our selection automatically select the 406 | * whole mention string. 407 | */ 408 | const initial = text.substring(0, key[0]); //mention start index 409 | text = initial + text.substr(key[1]); // mentions end index 410 | charDeleted = charDeleted + Math.abs(key[0] - key[1]); //1 is already added in the charDeleted 411 | // selection = { 412 | // start: ((charDeleted+selection.start)-1), 413 | // end: ((charDeleted+selection.start)-1) 414 | // } 415 | this.mentionsMap.delete(key); 416 | } 417 | } else { 418 | //multi-char deleted 419 | const mentionKeys = EU.getSelectedMentionKeys( 420 | this.mentionsMap, 421 | totalSelection 422 | ); 423 | mentionKeys.forEach(key => { 424 | this.mentionsMap.delete(key); 425 | }); 426 | } 427 | /** 428 | * update indexes on charcters remove 429 | * no need to worry about totalSelection End. 430 | * We already removed deleted mentions from the actual string. 431 | * */ 432 | this.updateMentionsMap( 433 | { 434 | start: selection.end, 435 | end: prevText.length 436 | }, 437 | charDeleted, 438 | false 439 | ); 440 | } else { 441 | //update indexes on new charcter add 442 | 443 | let charAdded = Math.abs(text.length - prevText.length); 444 | this.updateMentionsMap( 445 | { 446 | start: selection.end, 447 | end: text.length 448 | }, 449 | charAdded, 450 | true 451 | ); 452 | /** 453 | * if user type anything on the mention 454 | * remove the mention from the mentions array 455 | * */ 456 | if (selection.start === selection.end) { 457 | const key = EU.findMentionKeyInMap( 458 | this.mentionsMap, 459 | selection.start - 1 460 | ); 461 | if (key && key.length) { 462 | this.mentionsMap.delete(key); 463 | } 464 | } 465 | } 466 | 467 | this.setState({ 468 | inputText: text, 469 | formattedText: this.formatText(text) 470 | // selection, 471 | }); 472 | this.checkForMention(text, selection); 473 | // const text = `${initialStr} @[${user.username}](id:${user.id}) ${remStr}`; //'@[__display__](__id__)' ///find this trigger parsing from react-mentions 474 | 475 | this.sendMessageToFooter(text); 476 | }; 477 | 478 | onContentSizeChange = evt => { 479 | /** 480 | * this function will dynamically 481 | * calculate editor height w.r.t 482 | * the size of text in the input. 483 | */ 484 | if (evt) { 485 | // const iosTextHeight = 20.5 486 | const androidTextHeight = 20.5; 487 | // const textHeight = Platform.OS === 'ios' ? iosTextHeight : androidTextHeight 488 | 489 | const height = 490 | Platform.OS === "ios" 491 | ? evt.nativeEvent.contentSize.height 492 | : evt.nativeEvent.contentSize.height - androidTextHeight; 493 | let editorHeight = 40; 494 | editorHeight = editorHeight + height; 495 | this.setState({ 496 | editorHeight 497 | }); 498 | } 499 | }; 500 | 501 | render() { 502 | const { props, state } = this; 503 | const { editorStyles = {} } = props; 504 | 505 | if (!props.showEditor) return null; 506 | 507 | const mentionListProps = { 508 | list: props.list, 509 | keyword: state.keyword, 510 | isTrackingStarted: state.isTrackingStarted, 511 | onSuggestionTap: this.onSuggestionTap.bind(this), 512 | editorStyles 513 | }; 514 | 515 | return ( 516 | 517 | {props.renderMentionList ? ( 518 | props.renderMentionList(mentionListProps) 519 | ) : ( 520 | 527 | )} 528 | 529 | { 531 | this.scroll = scroll; 532 | }} 533 | onContentSizeChange={() => { 534 | this.scroll.scrollToEnd({ animated: true }); 535 | }} 536 | style={[styles.editorContainer, editorStyles.editorContainer]} 537 | > 538 | 539 | 545 | {state.formattedText !== "" ? ( 546 | 549 | {state.formattedText} 550 | 551 | ) : ( 552 | 558 | {state.placeholder} 559 | 560 | )} 561 | 562 | props.onRef && props.onRef(input)} 564 | style={[styles.input, editorStyles.input]} 565 | multiline 566 | autoFocus 567 | numberOfLines={100} 568 | name={"message"} 569 | value={state.inputText} 570 | onBlur={props.toggleEditor} 571 | onChangeText={this.onChange} 572 | selection={this.state.selection} 573 | selectionColor={"#000"} 574 | onSelectionChange={this.handleSelectionChange} 575 | placeholder={state.placeholder} 576 | onContentSizeChange={this.onContentSizeChange} 577 | scrollEnabled={false} 578 | /> 579 | 580 | 581 | 582 | 583 | ); 584 | } 585 | } 586 | 587 | export default Editor; 588 | -------------------------------------------------------------------------------- /src/MentionList/MentionListStyles.js: -------------------------------------------------------------------------------- 1 | import { StyleSheet } from "react-native"; 2 | 3 | export default StyleSheet.create({ 4 | container: { 5 | // flex:1, 6 | maxHeight: 300 7 | }, 8 | suggestionsPanelStyle: {}, 9 | loaderContainer: {}, 10 | mentionsListContainer: { 11 | height: 100 12 | } 13 | }); 14 | -------------------------------------------------------------------------------- /src/MentionList/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | import { ActivityIndicator, FlatList, Animated, View } from "react-native"; 4 | 5 | import MentionListItem from "../MentionListItem"; 6 | // Styles 7 | import styles from "./MentionListStyles"; 8 | 9 | export class MentionList extends React.PureComponent { 10 | static propTypes = { 11 | list: PropTypes.array, 12 | editorStyles: PropTypes.object, 13 | isTrackingStarted: PropTypes.bool, 14 | suggestions: PropTypes.array, 15 | keyword: PropTypes.string, 16 | onSuggestionTap: PropTypes.func 17 | }; 18 | 19 | constructor() { 20 | super(); 21 | this.previousChar = " "; 22 | } 23 | 24 | renderSuggestionsRow = ({ item }) => { 25 | return ( 26 | 31 | ); 32 | }; 33 | render() { 34 | const { props } = this; 35 | 36 | const { keyword, isTrackingStarted } = props; 37 | const withoutAtKeyword = keyword.substr(1, keyword.length); 38 | const list = this.props.list; 39 | const suggestions = 40 | withoutAtKeyword !== "" 41 | ? list.filter(user => user.username.includes(withoutAtKeyword)) 42 | : list; 43 | if (!isTrackingStarted) { 44 | return null; 45 | } 46 | return ( 47 | 53 | 59 | 60 | 61 | } 62 | enableEmptySections={true} 63 | data={suggestions} 64 | keyExtractor={(item, index) => `${item.id}-${index}`} 65 | renderItem={rowData => { 66 | return this.renderSuggestionsRow(rowData); 67 | }} 68 | /> 69 | 70 | ); 71 | } 72 | } 73 | 74 | export default MentionList; 75 | -------------------------------------------------------------------------------- /src/MentionListItem/MentionListItemStyles.js: -------------------------------------------------------------------------------- 1 | import { StyleSheet } from "react-native"; 2 | 3 | export default StyleSheet.create({ 4 | suggestionItem: { 5 | flex: 1, 6 | flexDirection: "row", 7 | justifyContent: "flex-start", 8 | alignItems: "center", 9 | width: "100%", 10 | backgroundColor: "#fff", 11 | color: "rgba(0, 0, 0, 0.1)", 12 | height: 50, 13 | paddingHorizontal: 20, 14 | borderBottomWidth: 1, 15 | borderColor: "rgba(0, 0, 0, 0.05)" 16 | }, 17 | text: { 18 | alignSelf: "center", 19 | marginLeft: 12 20 | }, 21 | title: { 22 | fontSize: 16, 23 | color: "rgba(0, 0, 0, 0.8)" 24 | }, 25 | thumbnailWrapper: { 26 | width: 35, 27 | height: 35 28 | }, 29 | thumbnailChar: { 30 | fontSize: 16 31 | } 32 | }); 33 | -------------------------------------------------------------------------------- /src/MentionListItem/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | import { Text, View, TouchableOpacity } from "react-native"; 4 | 5 | // Styles 6 | import styles from "./MentionListItemStyles"; 7 | 8 | import Avatar from "../Avatar"; 9 | 10 | export class MentionListItem extends React.PureComponent { 11 | static propTypes = { 12 | item: PropTypes.object, 13 | onSuggestionTap: PropTypes.func, 14 | editorStyles: PropTypes.object 15 | }; 16 | 17 | onSuggestionTap = (user, hidePanel) => { 18 | this.props.onSuggestionTap(user); 19 | }; 20 | 21 | render() { 22 | const { item: user, index, editorStyles } = this.props; 23 | return ( 24 | 25 | this.onSuggestionTap(user)} 29 | > 30 | 35 | 36 | 37 | 38 | {user.name} 39 | 40 | 43 | @{user.username} 44 | 45 | 46 | 47 | 48 | ); 49 | } 50 | } 51 | 52 | export default MentionListItem; 53 | -------------------------------------------------------------------------------- /yarn.lock: -------------------------------------------------------------------------------- 1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | # yarn lockfile v1 3 | 4 | 5 | --------------------------------------------------------------------------------