diff --git a/examples/app.js b/examples/app.js
index cbaf5a3e4..75c8762c8 100644
--- a/examples/app.js
+++ b/examples/app.js
@@ -31,6 +31,7 @@ import SearchHighlighting from './search-highlighting'
import InputTester from './input-tester'
import SyncingOperations from './syncing-operations'
import Tables from './tables'
+import Mentions from './mentions'
/**
* Examples.
@@ -62,6 +63,7 @@ const EXAMPLES = [
['History', History, '/history'],
['Versions', Versions, '/versions'],
['Input Tester', InputTester, '/input-tester'],
+ ['Mentions', Mentions, '/mentions'],
]
/**
diff --git a/examples/mentions/Suggestions.js b/examples/mentions/Suggestions.js
new file mode 100644
index 000000000..cfe8dea69
--- /dev/null
+++ b/examples/mentions/Suggestions.js
@@ -0,0 +1,99 @@
+import React from 'react'
+import ReactDOM from 'react-dom'
+
+import styled from 'react-emotion'
+
+const SuggestionList = styled('ul')`
+ background: #fff;
+ list-style: none;
+ margin: 0;
+ padding: 0;
+ position: absolute;
+`
+
+const Suggestion = styled('li')`
+ align-items: center;
+ border-left: 1px solid #ddd;
+ border-right: 1px solid #ddd;
+ border-top: 1px solid #ddd;
+
+ display: flex;
+ height: 32px;
+ padding: 4px 8px;
+
+ &:hover {
+ background: #87cefa;
+ }
+
+ &:last-of-type {
+ border-bottom: 1px solid #ddd;
+ }
+`
+
+const DEFAULT_POSITION = {
+ top: -10000,
+ left: -10000,
+}
+
+/**
+ * Suggestions is a PureComponent because we need to prevent updates when x/ y
+ * Are just going to be the same value. Otherwise we will update forever.
+ */
+
+class Suggestions extends React.PureComponent {
+ menuRef = React.createRef()
+
+ state = DEFAULT_POSITION
+
+ /**
+ * On update, update the menu.
+ */
+
+ componentDidMount = () => {
+ this.updateMenu()
+ }
+
+ componentDidUpdate = () => {
+ this.updateMenu()
+ }
+
+ render() {
+ const root = window.document.getElementById('root')
+
+ return ReactDOM.createPortal(
+
+ {this.props.users.map(user => {
+ return (
+ this.props.onSelect(user)}>
+ {user.username}
+
+ )
+ })}
+ ,
+ root
+ )
+ }
+
+ updateMenu() {
+ const anchor = window.document.querySelector(this.props.anchor)
+
+ if (!anchor) {
+ return this.setState(DEFAULT_POSITION)
+ }
+
+ const anchorRect = anchor.getBoundingClientRect()
+
+ this.setState({
+ top: anchorRect.bottom,
+ left: anchorRect.left,
+ })
+ }
+}
+
+export default Suggestions
diff --git a/examples/mentions/index.js b/examples/mentions/index.js
new file mode 100644
index 000000000..fc4b02de4
--- /dev/null
+++ b/examples/mentions/index.js
@@ -0,0 +1,306 @@
+/*
+This example is intended to be a super basic mentions implementation that
+people can work off of. What is show here is how to detect when a user starts
+typing a mention, making a search query, and then inserting a mention when
+the user selects an item. There are a few improvements that can be made in a
+production implementation:
+
+1. Serialization - in an actual implementation, you will probably want to
+ serialize the mentions out in a manner that your DB can parse, in order
+ to send notifications on the back end.
+2. Linkifying the mentions - There isn't really a good place to link to for
+ this example. But in most cases you would probably want to link to the
+ user's profile on click.
+3. Keyboard accessibility - it adds quite a bit of complexity to the
+ implementation to add this, as it involves capturing keyboard events like up
+ / down / enter and proxying them into the `Suggestions` component using a
+ `ref`. I've left this out because this is already a pretty confusing use
+ case.
+4. Plugin Mentions - in reality, you will probably want to put mentions into a
+ plugin, and make them configurable to support more than one kind of mention,
+ like users and hashtags. As you can see below it is a bit unweildy to bolt
+ all this directly to the editor.
+
+The list of characters was extracted from Wikipedia:
+https://en.wikipedia.org/wiki/List_of_Star_Wars_characters
+*/
+
+import { Editor } from 'slate-react'
+import { Value } from 'slate'
+import _ from 'lodash'
+import React from 'react'
+
+import initialValue from './value.json'
+import users from './users.json'
+import Suggestions from './Suggestions'
+
+/**
+ * @type {String}
+ */
+
+const USER_MENTION_NODE_TYPE = 'userMention'
+
+/**
+ * The decoration mark type that the menu will position itself against. The
+ * "context" is just the current text after the @ symbol.
+ * @type {String}
+ */
+
+const CONTEXT_MARK_TYPE = 'mentionContext'
+
+const schema = {
+ inlines: {
+ [USER_MENTION_NODE_TYPE]: {
+ // It's important that we mark the mentions as void nodes so that users
+ // can't edit the text of the mention.
+ isVoid: true,
+ },
+ },
+}
+
+/**
+ * The regex to use to find the searchQuery.
+ *
+ * @type {RegExp}
+ */
+
+const CAPTURE_REGEX = /@(\S*)$/
+
+/**
+ * Get get the potential mention input.
+ *
+ * @type {Value}
+ */
+
+function getInput(value) {
+ // In some cases, like if the node that was selected gets deleted,
+ // `startText` can be null.
+ if (!value.startText) {
+ return null
+ }
+
+ const startOffset = value.selection.start.offset
+ const textBefore = value.startText.text.slice(0, startOffset)
+ const result = CAPTURE_REGEX.exec(textBefore)
+
+ return result === null ? null : result[1]
+}
+
+/**
+ * @extends React.Component
+ */
+
+class MentionsExample extends React.Component {
+ /**
+ * Deserialize the initial editor value.
+ *
+ * @type {Object}
+ */
+
+ state = {
+ users: [],
+ value: Value.fromJSON(initialValue),
+ }
+
+ /**
+ * @type {React.RefObject}
+ */
+
+ editorRef = React.createRef()
+
+ render() {
+ return (
+
+
+
+
+ )
+ }
+
+ renderMark(props, next) {
+ if (props.mark.type === CONTEXT_MARK_TYPE) {
+ return (
+ // Adding the className here is important so taht the `Suggestions`
+ // component can find an anchor.
+
+ {props.children}
+
+ )
+ }
+
+ return next()
+ }
+
+ renderNode(props, next) {
+ const { attributes, node } = props
+
+ if (node.type === USER_MENTION_NODE_TYPE) {
+ // This is where you could turn the mention into a link to the user's
+ // profile or something.
+ return {props.node.text}
+ }
+
+ return next()
+ }
+
+ /**
+ * Replaces the current "context" with a user mention node corresponding to
+ * the given user.
+ * @param {Object} user
+ * @param {string} user.id
+ * @param {string} user.username
+ */
+
+ insertMention = user => {
+ const value = this.state.value
+ const inputValue = getInput(value)
+
+ // Delete the captured value, including the `@` symbol
+ this.editorRef.current.change(change => {
+ change = change.deleteBackward(inputValue.length + 1)
+
+ const selectedRange = change.value.selection
+
+ change
+ .insertText(' ')
+ .insertInlineAtRange(selectedRange, {
+ data: {
+ userId: user.id,
+ username: user.username,
+ },
+ nodes: [
+ {
+ object: 'text',
+ leaves: [
+ {
+ text: `@${user.username}`,
+ },
+ ],
+ },
+ ],
+ type: USER_MENTION_NODE_TYPE,
+ })
+ .focus()
+
+ this.setState({
+ value: change.value,
+ })
+ })
+ }
+
+ /**
+ * On change, save the new `value`.
+ *
+ * @param {Change} change
+ */
+
+ onChange = change => {
+ const inputValue = getInput(change.value)
+
+ if (inputValue !== this.lastInputValue) {
+ this.lastInputValue = inputValue
+
+ if (hasValidAncestors(change.value)) {
+ this.search(inputValue)
+ }
+
+ const { selection } = change.value
+
+ let decorations = change.value.decorations.filter(
+ value => value.mark.type !== CONTEXT_MARK_TYPE
+ )
+
+ if (inputValue && hasValidAncestors(change.value)) {
+ decorations = decorations.push({
+ anchor: {
+ key: selection.start.key,
+ offset: selection.start.offset - inputValue.length,
+ },
+ focus: {
+ key: selection.start.key,
+ offset: selection.start.offset,
+ },
+ mark: {
+ type: CONTEXT_MARK_TYPE,
+ },
+ })
+ }
+
+ return change.withoutSaving(() => change.setValue({ decorations }))
+ }
+
+ this.setState({ value: change.value })
+ }
+
+ /**
+ * Get an array of users that match the given search query
+ *
+ * @type {String}
+ */
+
+ search(searchQuery) {
+ // We don't want to show the wrong users for the current search query, so
+ // wipe them out.
+ this.setState({
+ users: [],
+ })
+
+ if (!searchQuery) return
+
+ // In order to make this seem like an API call, add a set timeout for some
+ // async.
+ setTimeout(() => {
+ // WARNING: In a production environment you should escape the search query.
+ const regex = RegExp(`^${searchQuery}`, 'gi')
+
+ // If you want to get fancy here, you can add some emphasis to the part
+ // of the string that matches.
+ const result = _.filter(users, user => {
+ return user.username.match(regex)
+ })
+
+ this.setState({
+ // Only return the first 5 results
+ users: result.slice(0, 5),
+ })
+ }, 50)
+ }
+}
+
+/**
+ * Determine if the current selection has valid ancestors for a context. In our
+ * case, we want to make sure that the mention is only a direct child of a
+ * paragraph. In this simple example it isn't that important, but in a complex
+ * editor you wouldn't want it to be a child of another inline like a link.
+ *
+ * @param {Value} value
+ */
+
+function hasValidAncestors(value) {
+ const { document, selection } = value
+
+ const invalidParent = document.getClosest(
+ selection.start.key,
+ // In this simple case, we only want mentions to live inside a paragraph.
+ // This check can be adjusted for more complex rich text implementations.
+ node => node.type !== 'paragraph'
+ )
+
+ return !invalidParent
+}
+
+export default MentionsExample
diff --git a/examples/mentions/users.json b/examples/mentions/users.json
new file mode 100644
index 000000000..4a73d993c
--- /dev/null
+++ b/examples/mentions/users.json
@@ -0,0 +1,417 @@
+[
+ { "username": "2-1B", "id": "1" },
+ { "username": "4-LOM", "id": "2" },
+ { "username": "8D8", "id": "3" },
+ { "username": "99", "id": "4" },
+ { "username": "0-0-0", "id": "5" },
+ { "username": "A'Koba", "id": "6" },
+ { "username": "Admiral Gial Ackbar", "id": "7" },
+ { "username": "Sim Aloo", "id": "8" },
+ { "username": "Almec", "id": "9" },
+ { "username": "Mas Amedda", "id": "10" },
+ { "username": "Amee", "id": "11" },
+ { "username": "Padmé Amidala", "id": "12" },
+ { "username": "Cassian Andor", "id": "13" },
+ { "username": "Fodesinbeed Annodue", "id": "14" },
+ { "username": "Raymus Antilles", "id": "15" },
+ { "username": "Wedge Antilles", "id": "16" },
+ { "username": "AP-5", "id": "17" },
+ { "username": "Queen Apailana", "id": "18" },
+ { "username": "Doctor Aphra", "id": "19" },
+ { "username": "Faro Argyus", "id": "20" },
+ { "username": "Aiolin and Morit Astarte", "id": "21" },
+ { "username": "Ello Asty", "id": "22" },
+ { "username": "AZI-3", "id": "23" },
+ { "username": "Walrus Man", "id": "24" },
+ { "username": "Kitster Banai", "id": "25" },
+ { "username": "Cad Bane", "id": "26" },
+ { "username": "Darth Bane", "id": "27" },
+ { "username": "Barada", "id": "28" },
+ { "username": "Jom Barell", "id": "29" },
+ { "username": "Moradmin Bast", "id": "30" },
+ { "username": "BB-8", "id": "31" },
+ { "username": "BB-9E", "id": "32" },
+ { "username": "Tobias Beckett", "id": "33" },
+ { "username": "Val Beckett", "id": "34" },
+ { "username": "The Bendu", "id": "35" },
+ { "username": "Shara Bey", "id": "36" },
+ { "username": "Sio Bibble", "id": "37" },
+ { "username": "Depa Billaba", "id": "38" },
+ { "username": "Jar Jar Binks", "id": "39" },
+ { "username": "Temiri Blagg", "id": "40" },
+ { "username": "Commander Bly", "id": "41" },
+ { "username": "Bobbajo", "id": "42" },
+ { "username": "Dud Bolt", "id": "43" },
+ { "username": "Mister Bones", "id": "44" },
+ { "username": "Lux Bonteri", "id": "45" },
+ { "username": "Mina Bonteri", "id": "46" },
+ { "username": "Borvo the Hutt", "id": "47" },
+ { "username": "Bossk", "id": "48" },
+ { "username": "Ezra Bridger", "id": "49" },
+ { "username": "BT-1", "id": "50" },
+ { "username": "Sora Bulq", "id": "51" },
+ { "username": "C1-10P", "id": "52" },
+ { "username": "C-3PO", "id": "53" },
+ { "username": "Lando Calrissian", "id": "54" },
+ { "username": "Moden Canady", "id": "55" },
+ { "username": "Ransolm Casterfo", "id": "56" },
+ { "username": "Chewbacca", "id": "57" },
+ { "username": "Chief Chirpa", "id": "58" },
+ { "username": "Rush Clovis", "id": "59" },
+ { "username": "Commander Cody (CC-2224)", "id": "60" },
+ { "username": "Lieutenant Kaydel Ko Connix", "id": "61" },
+ { "username": "Jeremoch Colton", "id": "62" },
+ { "username": "Cordé", "id": "63" },
+ { "username": "Salacious B. Crumb", "id": "64" },
+ { "username": "Arvel Crynyd", "id": "65" },
+ { "username": "Dr. Cylo", "id": "66" },
+ { "username": "Larma D'Acy", "id": "67" },
+ { "username": "Figrin D'an", "id": "68" },
+ { "username": "Kes Dameron", "id": "69" },
+ { "username": "Poe Dameron", "id": "70" },
+ { "username": "Vober Dand", "id": "71" },
+ { "username": "Joclad Danva", "id": "72" },
+ { "username": "Dapp", "id": "73" },
+ { "username": "Biggs Darklighter", "id": "74" },
+ { "username": "Oro Dassyne", "id": "75" },
+ { "username": "Gizor Dellso", "id": "76" },
+ { "username": "Dengar", "id": "77" },
+ { "username": "Bren Derlin", "id": "78" },
+ { "username": "Ima-Gun Di", "id": "79" },
+ { "username": "Rinnriyin Di", "id": "80" },
+ { "username": "DJ", "id": "81" },
+ { "username": "Lott Dod", "id": "82" },
+ { "username": "Jan Dodonna", "id": "83" },
+ { "username": "Daultay Dofine", "id": "84" },
+ { "username": "Dogma", "id": "85" },
+ { "username": "Darth Tyranus", "id": "86" },
+ { "username": "Dormé", "id": "87" },
+ { "username": "Cin Drallig", "id": "88" },
+ { "username": "Garven Dreis", "id": "89" },
+ { "username": "Droidbait", "id": "90" },
+ { "username": "Rio Durant", "id": "91" },
+ { "username": "Lok Durd", "id": "92" },
+ { "username": "Eirtaé", "id": "93" },
+ { "username": "Dineé Ellberger", "id": "94" },
+ { "username": "Ellé", "id": "95" },
+ { "username": "Caluan Ematt", "id": "96" },
+ { "username": "Embo", "id": "97" },
+ { "username": "Emperor's Royal Guard", "id": "98" },
+ { "username": "Jas Emari", "id": "99" },
+ { "username": "Ebe E. Endocott", "id": "100" },
+ { "username": "Galen Erso", "id": "101" },
+ { "username": "Jyn Erso", "id": "102" },
+ { "username": "Lyra Erso", "id": "103" },
+ { "username": "EV-9D9", "id": "104" },
+ { "username": "Moralo Eval", "id": "105" },
+ { "username": "Doctor Evazan", "id": "106" },
+ { "username": "Onaconda Farr", "id": "107" },
+ { "username": "Boba Fett", "id": "108" },
+ { "username": "Jango Fett", "id": "109" },
+ { "username": "Feral", "id": "110" },
+ { "username": "Commander Fil (CC-3714)", "id": "111" },
+ { "username": "Finn", "id": "112" },
+ { "username": "Kit Fisto", "id": "113" },
+ { "username": "Fives", "id": "114" },
+ { "username": "FN-1824", "id": "115" },
+ { "username": "FN-2003", "id": "116" },
+ { "username": "Nines", "id": "117" },
+ { "username": "Bib Fortuna", "id": "118" },
+ { "username": "Commander Fox", "id": "119" },
+ { "username": "FX-7", "id": "120" },
+ { "username": "GA-97", "id": "121" },
+ { "username": "Adi Gallia", "id": "122" },
+ { "username": "Gardulla the Hutt", "id": "123" },
+ { "username": "Yarna d'al' Gargan", "id": "124" },
+ { "username": "Gonk droid", "id": "125" },
+ { "username": "Commander Gree", "id": "126" },
+ { "username": "Greedo", "id": "127" },
+ { "username": "Janus Greejatus", "id": "128" },
+ { "username": "Captain Gregor", "id": "129" },
+ { "username": "Grievous", "id": "130" },
+ { "username": "Grummgar", "id": "131" },
+ { "username": "Gungi", "id": "132" },
+ { "username": "Nute Gunray", "id": "133" },
+ { "username": "Mars Guo", "id": "134" },
+ { "username": "Rune Haako", "id": "135" },
+ { "username": "Rako Hardeen", "id": "136" },
+ { "username": "Gideon Hask", "id": "137" },
+ { "username": "Hevy", "id": "138" },
+ { "username": "San Hill", "id": "139" },
+ { "username": "Clegg Holdfast", "id": "140" },
+ { "username": "Vice Admiral Amilyn Holdo", "id": "141" },
+ { "username": "Tey How", "id": "142" },
+ { "username": "Huyang", "id": "143" },
+ { "username": "Armitage Hux", "id": "144" },
+ { "username": "Brendol Hux", "id": "145" },
+ { "username": "IG-88", "id": "146" },
+ { "username": "Chirrut Îmwe", "id": "147" },
+ { "username": "Inquisitors", "id": "148" },
+ { "username": "Grand Inquisitor", "id": "149" },
+ { "username": "Fifth Brother", "id": "150" },
+ { "username": "Sixth Brother", "id": "151" },
+ { "username": "Seventh Sister", "id": "152" },
+ { "username": "Eighth Brother", "id": "153" },
+ { "username": "Sidon Ithano", "id": "154" },
+ { "username": "Jabba", "id": "155" },
+ { "username": "Queen Jamillia", "id": "156" },
+ { "username": "Wes Janson", "id": "157" },
+ { "username": "Kanan Jarrus", "id": "158" },
+ { "username": "Jaxxon", "id": "159" },
+ { "username": "Greeata Jendowanian", "id": "160" },
+ { "username": "Tiaan Jerjerrod", "id": "161" },
+ { "username": "Commander Jet", "id": "162" },
+ { "username": "Dexter Jettster", "id": "163" },
+ { "username": "Qui-Gon Jinn", "id": "164" },
+ { "username": "Jira", "id": "165" },
+ { "username": "Jubnuk", "id": "166" },
+ { "username": "K-2SO", "id": "167" },
+ { "username": "Tee Watt Kaa", "id": "168" },
+ { "username": "Agent Kallus", "id": "169" },
+ { "username": "Harter Kalonia", "id": "170" },
+ { "username": "Maz Kanata", "id": "171" },
+ { "username": "Colonel Kaplan", "id": "172" },
+ { "username": "Karbin", "id": "173" },
+ { "username": "Karina the Great", "id": "174" },
+ { "username": "Alton Kastle", "id": "175" },
+ { "username": "King Katuunko", "id": "176" },
+ { "username": "Coleman Kcaj", "id": "177" },
+ { "username": "Obi-Wan Kenobi", "id": "178" },
+ { "username": "Ki-Adi-Mundi", "id": "179" },
+ { "username": "Klaatu", "id": "180" },
+ { "username": "Klik-Klak", "id": "181" },
+ { "username": "Derek Klivian", "id": "182" },
+ { "username": "Agen Kolar", "id": "183" },
+ { "username": "Plo Koon", "id": "184" },
+ { "username": "Eeth Koth", "id": "185" },
+ { "username": "Sergeant Kreel", "id": "186" },
+ { "username": "Pong Krell", "id": "187" },
+ { "username": "Black Krrsantan", "id": "188" },
+ { "username": "Bo-Katan Kryze", "id": "189" },
+ { "username": "Satine Kryze", "id": "190" },
+ { "username": "Conder Kyl", "id": "191" },
+ { "username": "Thane Kyrell", "id": "192" },
+ { "username": "L3-37", "id": "193" },
+ { "username": "L'ulo", "id": "194" },
+ { "username": "Beru Lars", "id": "195" },
+ { "username": "Cliegg Lars", "id": "196" },
+ { "username": "Owen Lars", "id": "197" },
+ { "username": "Cut Lawquane", "id": "198" },
+ { "username": "Tasu Leech", "id": "199" },
+ { "username": "Xamuel Lennox", "id": "200" },
+ { "username": "Tallissan Lintra", "id": "201" },
+ { "username": "Slowen Lo", "id": "202" },
+ { "username": "Lobot", "id": "203" },
+ { "username": "Logray", "id": "204" },
+ { "username": "Lumat", "id": "205" },
+ { "username": "Crix Madine", "id": "206" },
+ { "username": "Shu Mai", "id": "207" },
+ { "username": "Malakili", "id": "208" },
+ { "username": "Baze Malbus", "id": "209" },
+ { "username": "Mama the Hutt", "id": "210" },
+ { "username": "Ody Mandrell", "id": "211" },
+ { "username": "Darth Maul", "id": "212" },
+ { "username": "Saelt-Marae", "id": "213" },
+ { "username": "Mawhonic", "id": "214" },
+ { "username": "Droopy McCool", "id": "215" },
+ { "username": "Pharl McQuarrie", "id": "216" },
+ { "username": "ME-8D9", "id": "217" },
+ { "username": "Lyn Me", "id": "218" },
+ { "username": "Tion Medon", "id": "219" },
+ { "username": "Del Meeko", "id": "220" },
+ { "username": "Aks Moe", "id": "221" },
+ { "username": "Sly Moore", "id": "222" },
+ { "username": "Morley", "id": "223" },
+ { "username": "Delian Mors", "id": "224" },
+ { "username": "Mon Mothma", "id": "225" },
+ { "username": "Conan Antonio Motti", "id": "226" },
+ { "username": "Jobal Naberrie", "id": "227" },
+ { "username": "Pooja Naberrie", "id": "228" },
+ { "username": "Ruwee Naberrie", "id": "229" },
+ { "username": "Ryoo Naberrie", "id": "230" },
+ { "username": "Sola Naberrie", "id": "231" },
+ { "username": "Hammerhead", "id": "232" },
+ { "username": "Boss Nass", "id": "233" },
+ { "username": "Lorth Needa", "id": "234" },
+ { "username": "Queen Neeyutnee", "id": "235" },
+ { "username": "Enfys Nest", "id": "236" },
+ { "username": "Bazine Netal", "id": "237" },
+ { "username": "Niima the Hutt", "id": "238" },
+ { "username": "Jocasta Nu", "id": "239" },
+ { "username": "Po Nudo", "id": "240" },
+ { "username": "Nien Nunb", "id": "241" },
+ { "username": "Has Obbit", "id": "242" },
+ { "username": "Barriss Offee", "id": "243" },
+ { "username": "Hondo Ohnaka", "id": "244" },
+ { "username": "Ric Olié", "id": "245" },
+ { "username": "Omi", "id": "246" },
+ { "username": "Ketsu Onyo", "id": "247" },
+ { "username": "Oola", "id": "248" },
+ { "username": "OOM-9", "id": "249" },
+ { "username": "Savage Opress", "id": "250" },
+ { "username": "Senator Organa", "id": "251" },
+ { "username": "Breha Antilles-Organa", "id": "252" },
+ { "username": "Leia Organa", "id": "253" },
+ { "username": "Garazeb \"Zeb\" Orrelios", "id": "254" },
+ { "username": "Orrimarko", "id": "255" },
+ { "username": "Admiral Ozzel", "id": "256" },
+ { "username": "Odd Ball", "id": "257" },
+ { "username": "Pablo-Jill", "id": "258" },
+ { "username": "Teemto Pagalies", "id": "259" },
+ { "username": "Captain Quarsh Panaka", "id": "260" },
+ { "username": "Casca Panzoro", "id": "261" },
+ { "username": "Reeve Panzoro", "id": "262" },
+ { "username": "Baron Papanoida", "id": "263" },
+ { "username": "Che Amanwe Papanoida", "id": "264" },
+ { "username": "Chi Eekway Papanoida", "id": "265" },
+ { "username": "Paploo", "id": "266" },
+ { "username": "Captain Phasma", "id": "267" },
+ { "username": "Even Piell", "id": "268" },
+ { "username": "Admiral Firmus Piett", "id": "269" },
+ { "username": "Sarco Plank", "id": "270" },
+ { "username": "Unkar Plutt", "id": "271" },
+ { "username": "Poggle the Lesser", "id": "272" },
+ { "username": "Yarael Poof", "id": "273" },
+ { "username": "Jek Tono Porkins", "id": "274" },
+ { "username": "Nahdonnis Praji", "id": "275" },
+ { "username": "PZ-4CO", "id": "276" },
+ { "username": "Ben Quadinaros", "id": "277" },
+ { "username": "Qi'ra", "id": "278" },
+ { "username": "Quarrie", "id": "279" },
+ { "username": "Quiggold", "id": "280" },
+ { "username": "Artoo", "id": "281" },
+ { "username": "R2-KT", "id": "282" },
+ { "username": "R3-S6", "id": "283" },
+ { "username": "R4-P17", "id": "284" },
+ { "username": "R5-D4", "id": "285" },
+ { "username": "RA-7", "id": "286" },
+ { "username": "Rabé", "id": "287" },
+ { "username": "Admiral Raddus", "id": "288" },
+ { "username": "Dak Ralter", "id": "289" },
+ { "username": "Oppo Rancisis", "id": "290" },
+ { "username": "Admiral Dodd Rancit", "id": "291" },
+ { "username": "Rappertunie", "id": "292" },
+ { "username": "Siniir Rath Velus", "id": "293" },
+ { "username": "Gallius Rax", "id": "294" },
+ { "username": "Eneb Ray", "id": "295" },
+ { "username": "Max Rebo", "id": "296" },
+ { "username": "Ciena Ree", "id": "297" },
+ { "username": "Ree-Yees", "id": "298" },
+ { "username": "Kylo Ren", "id": "299" },
+ { "username": "Captain Rex", "id": "300" },
+ { "username": "Rey", "id": "301" },
+ { "username": "Carlist Rieekan", "id": "302" },
+ { "username": "Riley", "id": "303" },
+ { "username": "Rogue Squadron", "id": "304" },
+ { "username": "Romba", "id": "305" },
+ { "username": "Bodhi Rook", "id": "306" },
+ { "username": "Pagetti Rook", "id": "307" },
+ { "username": "Rotta the Hutt", "id": "308" },
+ { "username": "Rukh", "id": "309" },
+ { "username": "Sabé", "id": "310" },
+ { "username": "Saché", "id": "311" },
+ { "username": "Sarkli", "id": "312" },
+ { "username": "Admiral U.O. Statura", "id": "313" },
+ { "username": "Joph Seastriker", "id": "314" },
+ { "username": "Miraj Scintel", "id": "315" },
+ { "username": "Admiral Terrinald Screed", "id": "316" },
+ { "username": "Sebulba", "id": "317" },
+ { "username": "Aayla Secura", "id": "318" },
+ { "username": "Korr Sella", "id": "319" },
+ { "username": "Zev Senesca", "id": "320" },
+ { "username": "Echuu Shen-Jon", "id": "321" },
+ { "username": "Sifo-Dyas", "id": "322" },
+ { "username": "Aurra Sing", "id": "323" },
+ { "username": "Luke Skywalker", "id": "324" },
+ { "username": "Shmi Skywalker", "id": "325" },
+ { "username": "The Smuggler", "id": "326" },
+ { "username": "Snaggletooth", "id": "327" },
+ { "username": "Snoke", "id": "328" },
+ { "username": "Sy Snootles", "id": "329" },
+ { "username": "Osi Sobeck", "id": "330" },
+ { "username": "Han Solo", "id": "331" },
+ { "username": "Greer Sonnel", "id": "332" },
+ { "username": "Sana Starros", "id": "333" },
+ { "username": "Lama Su", "id": "334" },
+ { "username": "Mercurial Swift", "id": "335" },
+ { "username": "Gavyn Sykes", "id": "336" },
+ { "username": "Cham Syndulla", "id": "337" },
+ { "username": "Hera Syndulla", "id": "338" },
+ { "username": "Jacen Syndulla", "id": "339" },
+ { "username": "Orn Free Taa", "id": "340" },
+ { "username": "Cassio Tagge", "id": "341" },
+ { "username": "Mother Talzin", "id": "342" },
+ { "username": "Wat Tambor", "id": "343" },
+ { "username": "Riff Tamson", "id": "344" },
+ { "username": "Fulcrum", "id": "345" },
+ { "username": "Tarfful", "id": "346" },
+ { "username": "Jova Tarkin", "id": "347" },
+ { "username": "Wilhuff Tarkin", "id": "348" },
+ { "username": "Roos Tarpals", "id": "349" },
+ { "username": "TC-14", "id": "350" },
+ { "username": "Berch Teller", "id": "351" },
+ { "username": "Teebo", "id": "352" },
+ { "username": "Teedo", "id": "353" },
+ { "username": "Mod Terrik", "id": "354" },
+ { "username": "Tessek", "id": "355" },
+ { "username": "Lor San Tekka", "id": "356" },
+ { "username": "Petty Officer Thanisson", "id": "357" },
+ { "username": "Inspector Thanoth", "id": "358" },
+ { "username": "Lieutenant Thire", "id": "359" },
+ { "username": "Thrawn", "id": "360" },
+ { "username": "C'ai Threnalli", "id": "361" },
+ { "username": "Shaak Ti", "id": "362" },
+ { "username": "Paige Tico", "id": "363" },
+ { "username": "Rose Tico", "id": "364" },
+ { "username": "Saesee Tiin", "id": "365" },
+ { "username": "Bala-Tik", "id": "366" },
+ { "username": "Meena Tills", "id": "367" },
+ { "username": "Quay Tolsite", "id": "368" },
+ { "username": "Bargwill Tomder", "id": "369" },
+ { "username": "Wag Too", "id": "370" },
+ { "username": "Coleman Trebor", "id": "371" },
+ { "username": "Admiral Trench", "id": "372" },
+ { "username": "Strono Tuggs", "id": "373" },
+ { "username": "Tup", "id": "374" },
+ { "username": "Letta Turmond", "id": "375" },
+ { "username": "Longo Two-Guns", "id": "376" },
+ { "username": "Cpatain Typho", "id": "377" },
+ { "username": "Ratts Tyerell", "id": "378" },
+ { "username": "U9-C4", "id": "379" },
+ { "username": "Luminara Unduli", "id": "380" },
+ { "username": "Finis Valorum", "id": "381" },
+ { "username": "Eli Vanto", "id": "382" },
+ { "username": "Nahdar Vebb", "id": "383" },
+ { "username": "Maximilian Veers", "id": "384" },
+ { "username": "Asajj Ventress", "id": "385" },
+ { "username": "Evaan Verlaine", "id": "386" },
+ { "username": "Garrick Versio", "id": "387" },
+ { "username": "Iden Versio", "id": "388" },
+ { "username": "Lanever Villecham", "id": "389" },
+ { "username": "Nuvo Vindi", "id": "390" },
+ { "username": "Tulon Voidgazer", "id": "391" },
+ { "username": "Dryden Vos", "id": "392" },
+ { "username": "Quinlan Vos", "id": "393" },
+ { "username": "WAC-47", "id": "394" },
+ { "username": "Wald", "id": "395" },
+ { "username": "Warok", "id": "396" },
+ { "username": "Wicket W. Warrick", "id": "397" },
+ { "username": "Watto", "id": "398" },
+ { "username": "Taun We", "id": "399" },
+ { "username": "Zam Wesell", "id": "400" },
+ { "username": "Norra Wexley", "id": "401" },
+ { "username": "Snap Wexley", "id": "402" },
+ { "username": "Vanden Willard", "id": "403" },
+ { "username": "Mace Windu", "id": "404" },
+ { "username": "Commander Wolffe", "id": "405" },
+ { "username": "Wollivan", "id": "406" },
+ { "username": "Sabine Wren", "id": "407" },
+ { "username": "Wuher", "id": "408" },
+ { "username": "Yaddle", "id": "409" },
+ { "username": "Yoda", "id": "410" },
+ { "username": "Joh Yowza", "id": "411" },
+ { "username": "Wullf Yularen", "id": "412" },
+ { "username": "Ziro the Hutt", "id": "413" },
+ { "username": "Zuckuss", "id": "414" },
+ { "username": "Constable Zuvio", "id": "415" }
+]
diff --git a/examples/mentions/value.json b/examples/mentions/value.json
new file mode 100644
index 000000000..9b2e3d6eb
--- /dev/null
+++ b/examples/mentions/value.json
@@ -0,0 +1,20 @@
+{
+ "document": {
+ "nodes": [
+ {
+ "object": "block",
+ "type": "paragraph",
+ "nodes": [
+ {
+ "object": "text",
+ "leaves": [
+ {
+ "text": "Try mentioning some users, like Luke or Leia."
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+}