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 blame โ†’ Nick 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 alias โ†’ emoji 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