Emoji in the editor! 🍾 🙌 🎉 – Medium Engineering


Last week I was blocked on product development, so I added an :emoji: typeahead to the Medium editor, because I was tired of ^⌘[space]ing. 🙅🕒

Try it out! Type a colon, followed by the name of your favorite emoji into any Medium post. (Here’s a cheat sheet.)

Context

We already implicitly allow emoji in the editor. We even gracefully handle multi-character emoji like 👨‍👩‍👦‍👦 and 🇦🇺 when you backspace and arrow around. Which…

git blameNick Santos, obviously. 🙏

The typeahead

The Medium editor has a bunch of plugins, which translate key and mouse input into editor commands. They also sometimes draw other editor UX. (Like typeaheads!)

A few typeaheads already exist in the editor, including the ones for @mentions and post tags. Each one is its own editor plugin, but they all extend the root TypeaheadPlugin. So I created a new class called TypeaheadEmojiPlugin, and overrode the following methods:

  • linkifyCommand and unlinkifyCommand, which add and remove the temporary HTML markup we put on :query strings. (That’s what turns the text green as you type.)
  • shouldLookup, which determines whether we do a query, and shouldLinkify, which determines if we try to add the query markup. Both are checked on most keypresses, and both look for a colon preceding the cursor, with no spaces or punctuation in between. shouldLinkify additionally checks if the query markup is already there, so we don’t add it twice.
  • requestData, which happens when shouldLookup is true. For user mentions, we send a request to our backend. For emoji, we just wrap a call to emoji.getMatchingEmoji in a Deferred.
  • extractData, which by default tries to unpack a response with a value key. We don’t need this, because we didn’t send a request. So we just take the raw data straight from requestData and return it.
  • tokenCommand, which does the actual keyword → emoji replacement when a typeahead item is selected. We find the preceding colon, remove it and everything between it and the cursor, and then insert the selected emoji.
tokenCommand = function (keyword, emoji) {
return function (paragraph, offset) {
let text = paragraph.getText()
let start = emoji.getEmojiQueryStartIndex(text, offset)
if (start == -1) return 0
    paragraph.removeText(start, offset)
paragraph.insertText(emoji, start)
    return start + emoji.length - offset
}
}

Smart text replacement

In addition to being able to :start_typing to call up a list of emoji, we also want fully formed emoji strings like :100: to be replaced with 💯.

Like all of our other smart text replacements, that means listening for the last character in the sequence, :, and then checking the preceding characters for an emoji keyword.

We perform a replacement when:

  1. There’s another colon preceding the one that triggered this check, and only non-space and non-punctuation characters in between (so hello :no_mouth: should trigger, but :YOLO. WOOO: should not).
  2. The string between the colons matches an emoji keyword.
  3. The starting colon has a space before it.
insertions.insertColon = function (paragraph, offset) {
let text = paragraph.getText()
let start = emoji.getEmojiQueryStartIndex(text, offset)
let keyword = text.substring(start + 1, offset)
let emoji = emojiKeywords.KEYWORD_TO_EMOJI
let prevChar = paragraph.getCharAt(start - 1)
if (start > -1 && emoji && (prevChar == ' ' || !prevChar)) {
paragraph.removeText(start, offset)
paragraph.insertText(emoji, start)
return start + emoji.length - offset
} else {
paragraph.insertText(':', offset)
return 1
}
}

Keywords

In a testament to the cleanliness of Medium’s editor code, everything I described above took me about five hours to do, unit tests and all. I then proceeded to spend two full days looking through different sets of emoji keywords, and trying to figure out which emoji are supported by which macOS versions.

I initially tried using canonical unicode names, but there are some really bizarre ones. :smiling_face_with_open_mouth_and_tightly_closed_eyes:, for instance, which I and most other emoji-capable humans know as :laughing: 😆

I ended up cannibalizing the keywords available on Github, found here. For ease of searching (and smaller file size), I massaged that list into an object of aliasemoji pairs.

I kept an eye out for macOS emoji support documentation, but didn’t find much. Going off a hodgepodge of release notes, iOS rumors, and old Stack Overflow answers, I cobbled together the following timeline:

  • At some point in the distant past (~OS X Lion), emoji support is introduced. 😬 👻 🔮
  • There are two OS X updates that just add more flags. 🇮🇲 🇯🇲 🇳🇿
  • There is an emoji update with El Capitan (10.11.1), which corresponds to iOS 9.1 and adds support for Unicode 7 and 8. 🦄 🏍🍿
  • Sierra (10.12) comes out at the same time as iOS 10, with a bunch of new emoji that don’t really map to any Unicode version, and are largely differently-gendered versions of existing emoji. 🕺🕵️‍♀️ 👩‍👩‍👦
  • After a bit, Sierra (10.12.2), along with iOS 10.2, adds support for Unicode 9. 🥑 🦊 🥃

So, those are the version cut-offs I’m going with. For non-🍏 machines, we’ll provide the base set (everything pre–El Capitan).

Fun facts

  • My first push of emojiKeywords.js failed lint checks, because you can’t have duplicate keys in an object, and there were two turkeys 🦃 🇹🇷 (now :turkey: and :turkey_flag:).
  • There are 1462 emoji keywords total on Medium. Some are repeats, like :tangerine::orange:, and :mandarin: 🍊
  • I added a vanity alias for my favorite emoji, because I can never remember what it’s called. 😐 ← This is now :pokerface:



Source link