If you'd like to follow along, or take a closer look at the complete code, you can check out the github repo here
This is a fun one! I've been busy with work-related stuff for the past week, so I wanted to ease myself back into this project.
No discord clone would be complete without emoji support, and I wanted to try and recreate the elegant autocomplete feature that discord has. If you're not familiar with it, it can be activated by typing a colon ':' followed by an emoji name in the discord textbox. This then pops up an emoji suggestion list, and hitting tab adds the selected emoji to your message.
What I've come up with isn't a complete or perfect implementation, but it's a good jumping off point. So let's get started.
We're going to need to handle the state of two things - our text input value, and our autocomplete value. So let's set up two variables for that, along with a couple HTML elements to display the data.
// the input state const [msg, setMsg] = useState(""); // the autocomplete state const [autoComplete, setAutoComplete] = useState(""); return( <> <div id="autoComplete">{autoComplete}</div> <textarea name="content" id="chatInput" value={msg} onChange={registerEmoji} onKeyDown={autocompleteEmoji} /> </> )
The autocomplete div is where we can display the emoji match, and the textarea will be the message box that we'll be manipulating.
You can see I've already set up a few event listeners. onChange will fire whenever we type into the textarea. This will be where we check for matches, and set our autoComplete value.
onKeyDown will listen for a 'tab' keypress, and can be used to autocomplete suggestions before typing out the full emoji name.
Of course, we'll also need a way to correlate these shortcode names to an actual emoji. That's where the emojis array comes in.
const emojis = [ { name: ":smirk:", content: "😏", }, { name: ":hot-face:", content: "🥵", }, { name: ":sunglasses:", content: "😎", }, ];
So just a basic array of objects, each with name and content.
So here is how we'll find matches. OnChange will run the registerEmoji function whenever you type into the textarea. Inside this function, we can run a forEach loop on the emojis array.
Inside this loop, we first check if the message contains a colon ':'. If so, split the string, and take the 1st index, which will be contain the text after the colon. From there, run an includes function on the emoji.name value, passing the match string.
If the condition passes, we set our autocomplete state to the corresponding emoji content (the emoji itself) with a call to the setAutocomplete function. If it doesn't match, we do the same, but set an empty value.
This portion of the code will handle the autocomplete suggestion, but we also need to control what happens if the user enters the complete emoji name. In this case, we have a separate conditional that checks if the msgvariable contains the entirety of an emoji name. If so, it will replace the shortcode with the emoji.
function registerEmoji(e) { var msg = e.target.value; //loop through our emojis array emojis.forEach((emoji) => { //check for colon if (msg.includes(":")) { //split the text value, check only the content after the colon var match = msg.split(":")[1]; //set autocomplete based on results match = emoji.name.includes(match) ? setAutoComplete(emoji.content) : ""; } else { //if no match, clear the autocomplete setAutoComplete(""); } //if exact match, replace with emoji if (msg.includes(emoji.name)) { msg = msg.replace(emoji.name, emoji.content); } }); setMsg(msg); }
Pretty cool! So how do we actually utilize the autocomplete value? Well, going back to the example discord gives us - when a user starts writing out an emoji shortcode, it provides suggestions based on that input. Then, the user can hit tab to select the suggested emoji, and populate their message with it.
This is where the onKeyDown listener comes into play. This function will fire when a user hits any key while focused on the textarea, including the tab key.
In this function, we will check if the autocomplete variable is populated. If so, we'll replace the partially entered shortcode with the matched emoji.
function autocompleteEmoji(e) { let msg = e.target.value; //check that autocomplete is empty, and that the keypress was a tab if (autoComplete != "" && e.keyCode == 9) { //preventDefault allows us to maintain focus inside the textarea e.preventDefault(); //split the message, and replace anything after the colon with the autoComplete value msg = msg.split(":"); try { msg[1] = autoComplete; msg = msg.join(""); setMsg(msg); //clear the autocomplete setAutoComplete(""); } catch (er) { console.log("oofy", er); } } }
The use of an includes function to check for matches isn't perfect. It will autopopulate with the first emoji in the array right after typing in ':'. It can also return inaccurate matches based on the first 1-2 characters you enter. This could be partially resolved by setting a minimum character count.
Another issue is how to handle it if a user goes back into the middle of a message to try an add an emoji. Currently the autocomplete function would remove the entirety of the message after the shortcode begins. This might be partially resolved by checking for a space ' ' or some other character to limit the amount of content that gets replaced.
So that's that! Pretty fun feature if you ask me. Like I said above, there is some cleaning up to do. It would also be cool to return an entire array of potential matches, the same way discord does. But for now, I'm pretty pleased with this.