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." + } + ] + } + ] + } + ] + } +}