
Emoji in the editor! ๐พ ๐ ๐
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
andunlinkifyCommand
, 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, andshouldLinkify
, 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 whenshouldLookup
is true. For user mentions, we send a request to our backend. For emoji, we just wrap a call toemoji.getMatchingEmoji
in a Deferred.extractData
, which by default tries to unpack a response with avalue
key. We donโt need this, because we didnโt send a request. So we just take the raw data straight fromrequestData
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:
- 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). - The string between the colons matches an emoji keyword.
- 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[keyword]
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: