mirror of
https://github.com/ianstormtaylor/slate.git
synced 2025-01-17 13:38:37 +01:00
722 lines
15 KiB
TypeScript
722 lines
15 KiB
TypeScript
import React, {
|
|
useMemo,
|
|
useCallback,
|
|
useRef,
|
|
useEffect,
|
|
useState,
|
|
Fragment,
|
|
} from 'react'
|
|
import { Editor, Transforms, Range, createEditor, Descendant } from 'slate'
|
|
import { withHistory } from 'slate-history'
|
|
import {
|
|
Slate,
|
|
Editable,
|
|
ReactEditor,
|
|
withReact,
|
|
useSelected,
|
|
useFocused,
|
|
} from 'slate-react'
|
|
|
|
import { Portal } from './components'
|
|
import { MentionElement } from './custom-types.d'
|
|
import { IS_MAC } from './utils/environment'
|
|
|
|
const MentionExample = () => {
|
|
const ref = useRef<HTMLDivElement | null>()
|
|
const [target, setTarget] = useState<Range | undefined>()
|
|
const [index, setIndex] = useState(0)
|
|
const [search, setSearch] = useState('')
|
|
const renderElement = useCallback(props => <Element {...props} />, [])
|
|
const renderLeaf = useCallback(props => <Leaf {...props} />, [])
|
|
const editor = useMemo(
|
|
() => withMentions(withReact(withHistory(createEditor()))),
|
|
[]
|
|
)
|
|
|
|
const chars = CHARACTERS.filter(c =>
|
|
c.toLowerCase().startsWith(search.toLowerCase())
|
|
).slice(0, 10)
|
|
|
|
const onKeyDown = useCallback(
|
|
event => {
|
|
if (target && chars.length > 0) {
|
|
switch (event.key) {
|
|
case 'ArrowDown':
|
|
event.preventDefault()
|
|
const prevIndex = index >= chars.length - 1 ? 0 : index + 1
|
|
setIndex(prevIndex)
|
|
break
|
|
case 'ArrowUp':
|
|
event.preventDefault()
|
|
const nextIndex = index <= 0 ? chars.length - 1 : index - 1
|
|
setIndex(nextIndex)
|
|
break
|
|
case 'Tab':
|
|
case 'Enter':
|
|
event.preventDefault()
|
|
Transforms.select(editor, target)
|
|
insertMention(editor, chars[index])
|
|
setTarget(null)
|
|
break
|
|
case 'Escape':
|
|
event.preventDefault()
|
|
setTarget(null)
|
|
break
|
|
}
|
|
}
|
|
},
|
|
[chars, editor, index, target]
|
|
)
|
|
|
|
useEffect(() => {
|
|
if (target && chars.length > 0) {
|
|
const el = ref.current
|
|
const domRange = ReactEditor.toDOMRange(editor, target)
|
|
const rect = domRange.getBoundingClientRect()
|
|
el.style.top = `${rect.top + window.pageYOffset + 24}px`
|
|
el.style.left = `${rect.left + window.pageXOffset}px`
|
|
}
|
|
}, [chars.length, editor, index, search, target])
|
|
|
|
return (
|
|
<Slate
|
|
editor={editor}
|
|
initialValue={initialValue}
|
|
onChange={() => {
|
|
const { selection } = editor
|
|
|
|
if (selection && Range.isCollapsed(selection)) {
|
|
const [start] = Range.edges(selection)
|
|
const wordBefore = Editor.before(editor, start, { unit: 'word' })
|
|
const before = wordBefore && Editor.before(editor, wordBefore)
|
|
const beforeRange = before && Editor.range(editor, before, start)
|
|
const beforeText = beforeRange && Editor.string(editor, beforeRange)
|
|
const beforeMatch = beforeText && beforeText.match(/^@(\w+)$/)
|
|
const after = Editor.after(editor, start)
|
|
const afterRange = Editor.range(editor, start, after)
|
|
const afterText = Editor.string(editor, afterRange)
|
|
const afterMatch = afterText.match(/^(\s|$)/)
|
|
|
|
if (beforeMatch && afterMatch) {
|
|
setTarget(beforeRange)
|
|
setSearch(beforeMatch[1])
|
|
setIndex(0)
|
|
return
|
|
}
|
|
}
|
|
|
|
setTarget(null)
|
|
}}
|
|
>
|
|
<Editable
|
|
renderElement={renderElement}
|
|
renderLeaf={renderLeaf}
|
|
onKeyDown={onKeyDown}
|
|
placeholder="Enter some text..."
|
|
/>
|
|
{target && chars.length > 0 && (
|
|
<Portal>
|
|
<div
|
|
ref={ref}
|
|
style={{
|
|
top: '-9999px',
|
|
left: '-9999px',
|
|
position: 'absolute',
|
|
zIndex: 1,
|
|
padding: '3px',
|
|
background: 'white',
|
|
borderRadius: '4px',
|
|
boxShadow: '0 1px 5px rgba(0,0,0,.2)',
|
|
}}
|
|
data-cy="mentions-portal"
|
|
>
|
|
{chars.map((char, i) => (
|
|
<div
|
|
key={char}
|
|
onClick={() => {
|
|
Transforms.select(editor, target)
|
|
insertMention(editor, char)
|
|
setTarget(null)
|
|
}}
|
|
style={{
|
|
padding: '1px 3px',
|
|
borderRadius: '3px',
|
|
cursor: 'pointer',
|
|
background: i === index ? '#B4D5FF' : 'transparent',
|
|
}}
|
|
>
|
|
{char}
|
|
</div>
|
|
))}
|
|
</div>
|
|
</Portal>
|
|
)}
|
|
</Slate>
|
|
)
|
|
}
|
|
|
|
const withMentions = editor => {
|
|
const { isInline, isVoid, markableVoid } = editor
|
|
|
|
editor.isInline = element => {
|
|
return element.type === 'mention' ? true : isInline(element)
|
|
}
|
|
|
|
editor.isVoid = element => {
|
|
return element.type === 'mention' ? true : isVoid(element)
|
|
}
|
|
|
|
editor.markableVoid = element => {
|
|
return element.type === 'mention' || markableVoid(element)
|
|
}
|
|
|
|
return editor
|
|
}
|
|
|
|
const insertMention = (editor, character) => {
|
|
const mention: MentionElement = {
|
|
type: 'mention',
|
|
character,
|
|
children: [{ text: '' }],
|
|
}
|
|
Transforms.insertNodes(editor, mention)
|
|
Transforms.move(editor)
|
|
}
|
|
|
|
// Borrow Leaf renderer from the Rich Text example.
|
|
// In a real project you would get this via `withRichText(editor)` or similar.
|
|
const Leaf = ({ attributes, children, leaf }) => {
|
|
if (leaf.bold) {
|
|
children = <strong>{children}</strong>
|
|
}
|
|
|
|
if (leaf.code) {
|
|
children = <code>{children}</code>
|
|
}
|
|
|
|
if (leaf.italic) {
|
|
children = <em>{children}</em>
|
|
}
|
|
|
|
if (leaf.underline) {
|
|
children = <u>{children}</u>
|
|
}
|
|
|
|
return <span {...attributes}>{children}</span>
|
|
}
|
|
|
|
const Element = props => {
|
|
const { attributes, children, element } = props
|
|
switch (element.type) {
|
|
case 'mention':
|
|
return <Mention {...props} />
|
|
default:
|
|
return <p {...attributes}>{children}</p>
|
|
}
|
|
}
|
|
|
|
const Mention = ({ attributes, children, element }) => {
|
|
const selected = useSelected()
|
|
const focused = useFocused()
|
|
const style: React.CSSProperties = {
|
|
padding: '3px 3px 2px',
|
|
margin: '0 1px',
|
|
verticalAlign: 'baseline',
|
|
display: 'inline-block',
|
|
borderRadius: '4px',
|
|
backgroundColor: '#eee',
|
|
fontSize: '0.9em',
|
|
boxShadow: selected && focused ? '0 0 0 2px #B4D5FF' : 'none',
|
|
}
|
|
// See if our empty text child has any styling marks applied and apply those
|
|
if (element.children[0].bold) {
|
|
style.fontWeight = 'bold'
|
|
}
|
|
if (element.children[0].italic) {
|
|
style.fontStyle = 'italic'
|
|
}
|
|
return (
|
|
<span
|
|
{...attributes}
|
|
contentEditable={false}
|
|
data-cy={`mention-${element.character.replace(' ', '-')}`}
|
|
style={style}
|
|
>
|
|
{/* Prevent Chromium from interrupting IME when moving the cursor */}
|
|
{/* 1. span + inline-block 2. div + contenteditable=false */}
|
|
<div contentEditable={false}>
|
|
{IS_MAC ? (
|
|
// Mac OS IME https://github.com/ianstormtaylor/slate/issues/3490
|
|
<Fragment>
|
|
{children}@{element.character}
|
|
</Fragment>
|
|
) : (
|
|
// Others like Android https://github.com/ianstormtaylor/slate/pull/5360
|
|
<Fragment>
|
|
@{element.character}
|
|
{children}
|
|
</Fragment>
|
|
)}
|
|
</div>
|
|
</span>
|
|
)
|
|
}
|
|
|
|
const initialValue: Descendant[] = [
|
|
{
|
|
type: 'paragraph',
|
|
children: [
|
|
{
|
|
text: 'This example shows how you might implement a simple ',
|
|
},
|
|
{
|
|
text: '@-mentions',
|
|
bold: true,
|
|
},
|
|
{
|
|
text: ' feature that lets users autocomplete mentioning a user by their username. Which, in this case means Star Wars characters. The ',
|
|
},
|
|
{
|
|
text: 'mentions',
|
|
bold: true,
|
|
},
|
|
{
|
|
text: ' are rendered as ',
|
|
},
|
|
{
|
|
text: 'void inline elements',
|
|
code: true,
|
|
},
|
|
{
|
|
text: ' inside the document.',
|
|
},
|
|
],
|
|
},
|
|
{
|
|
type: 'paragraph',
|
|
children: [
|
|
{ text: 'Try mentioning characters, like ' },
|
|
{
|
|
type: 'mention',
|
|
character: 'R2-D2',
|
|
children: [{ text: '', bold: true }],
|
|
},
|
|
{ text: ' or ' },
|
|
{
|
|
type: 'mention',
|
|
character: 'Mace Windu',
|
|
children: [{ text: '' }],
|
|
},
|
|
{ text: '!' },
|
|
],
|
|
},
|
|
]
|
|
|
|
const CHARACTERS = [
|
|
'Aayla Secura',
|
|
'Adi Gallia',
|
|
'Admiral Dodd Rancit',
|
|
'Admiral Firmus Piett',
|
|
'Admiral Gial Ackbar',
|
|
'Admiral Ozzel',
|
|
'Admiral Raddus',
|
|
'Admiral Terrinald Screed',
|
|
'Admiral Trench',
|
|
'Admiral U.O. Statura',
|
|
'Agen Kolar',
|
|
'Agent Kallus',
|
|
'Aiolin and Morit Astarte',
|
|
'Aks Moe',
|
|
'Almec',
|
|
'Alton Kastle',
|
|
'Amee',
|
|
'AP-5',
|
|
'Armitage Hux',
|
|
'Artoo',
|
|
'Arvel Crynyd',
|
|
'Asajj Ventress',
|
|
'Aurra Sing',
|
|
'AZI-3',
|
|
'Bala-Tik',
|
|
'Barada',
|
|
'Bargwill Tomder',
|
|
'Baron Papanoida',
|
|
'Barriss Offee',
|
|
'Baze Malbus',
|
|
'Bazine Netal',
|
|
'BB-8',
|
|
'BB-9E',
|
|
'Ben Quadinaros',
|
|
'Berch Teller',
|
|
'Beru Lars',
|
|
'Bib Fortuna',
|
|
'Biggs Darklighter',
|
|
'Black Krrsantan',
|
|
'Bo-Katan Kryze',
|
|
'Boba Fett',
|
|
'Bobbajo',
|
|
'Bodhi Rook',
|
|
'Borvo the Hutt',
|
|
'Boss Nass',
|
|
'Bossk',
|
|
'Breha Antilles-Organa',
|
|
'Bren Derlin',
|
|
'Brendol Hux',
|
|
'BT-1',
|
|
'C-3PO',
|
|
'C1-10P',
|
|
'Cad Bane',
|
|
'Caluan Ematt',
|
|
'Captain Gregor',
|
|
'Captain Phasma',
|
|
'Captain Quarsh Panaka',
|
|
'Captain Rex',
|
|
'Carlist Rieekan',
|
|
'Casca Panzoro',
|
|
'Cassian Andor',
|
|
'Cassio Tagge',
|
|
'Cham Syndulla',
|
|
'Che Amanwe Papanoida',
|
|
'Chewbacca',
|
|
'Chi Eekway Papanoida',
|
|
'Chief Chirpa',
|
|
'Chirrut Îmwe',
|
|
'Ciena Ree',
|
|
'Cin Drallig',
|
|
'Clegg Holdfast',
|
|
'Cliegg Lars',
|
|
'Coleman Kcaj',
|
|
'Coleman Trebor',
|
|
'Colonel Kaplan',
|
|
'Commander Bly',
|
|
'Commander Cody (CC-2224)',
|
|
'Commander Fil (CC-3714)',
|
|
'Commander Fox',
|
|
'Commander Gree',
|
|
'Commander Jet',
|
|
'Commander Wolffe',
|
|
'Conan Antonio Motti',
|
|
'Conder Kyl',
|
|
'Constable Zuvio',
|
|
'Cordé',
|
|
'Cpatain Typho',
|
|
'Crix Madine',
|
|
'Cut Lawquane',
|
|
'Dak Ralter',
|
|
'Dapp',
|
|
'Darth Bane',
|
|
'Darth Maul',
|
|
'Darth Tyranus',
|
|
'Daultay Dofine',
|
|
'Del Meeko',
|
|
'Delian Mors',
|
|
'Dengar',
|
|
'Depa Billaba',
|
|
'Derek Klivian',
|
|
'Dexter Jettster',
|
|
'Dineé Ellberger',
|
|
'DJ',
|
|
'Doctor Aphra',
|
|
'Doctor Evazan',
|
|
'Dogma',
|
|
'Dormé',
|
|
'Dr. Cylo',
|
|
'Droidbait',
|
|
'Droopy McCool',
|
|
'Dryden Vos',
|
|
'Dud Bolt',
|
|
'Ebe E. Endocott',
|
|
'Echuu Shen-Jon',
|
|
'Eeth Koth',
|
|
'Eighth Brother',
|
|
'Eirtaé',
|
|
'Eli Vanto',
|
|
'Ellé',
|
|
'Ello Asty',
|
|
'Embo',
|
|
'Eneb Ray',
|
|
'Enfys Nest',
|
|
'EV-9D9',
|
|
'Evaan Verlaine',
|
|
'Even Piell',
|
|
'Ezra Bridger',
|
|
'Faro Argyus',
|
|
'Feral',
|
|
'Fifth Brother',
|
|
'Finis Valorum',
|
|
'Finn',
|
|
'Fives',
|
|
'FN-1824',
|
|
'FN-2003',
|
|
'Fodesinbeed Annodue',
|
|
'Fulcrum',
|
|
'FX-7',
|
|
'GA-97',
|
|
'Galen Erso',
|
|
'Gallius Rax',
|
|
'Garazeb "Zeb" Orrelios',
|
|
'Gardulla the Hutt',
|
|
'Garrick Versio',
|
|
'Garven Dreis',
|
|
'Gavyn Sykes',
|
|
'Gideon Hask',
|
|
'Gizor Dellso',
|
|
'Gonk droid',
|
|
'Grand Inquisitor',
|
|
'Greeata Jendowanian',
|
|
'Greedo',
|
|
'Greer Sonnel',
|
|
'Grievous',
|
|
'Grummgar',
|
|
'Gungi',
|
|
'Hammerhead',
|
|
'Han Solo',
|
|
'Harter Kalonia',
|
|
'Has Obbit',
|
|
'Hera Syndulla',
|
|
'Hevy',
|
|
'Hondo Ohnaka',
|
|
'Huyang',
|
|
'Iden Versio',
|
|
'IG-88',
|
|
'Ima-Gun Di',
|
|
'Inquisitors',
|
|
'Inspector Thanoth',
|
|
'Jabba',
|
|
'Jacen Syndulla',
|
|
'Jan Dodonna',
|
|
'Jango Fett',
|
|
'Janus Greejatus',
|
|
'Jar Jar Binks',
|
|
'Jas Emari',
|
|
'Jaxxon',
|
|
'Jek Tono Porkins',
|
|
'Jeremoch Colton',
|
|
'Jira',
|
|
'Jobal Naberrie',
|
|
'Jocasta Nu',
|
|
'Joclad Danva',
|
|
'Joh Yowza',
|
|
'Jom Barell',
|
|
'Joph Seastriker',
|
|
'Jova Tarkin',
|
|
'Jubnuk',
|
|
'Jyn Erso',
|
|
'K-2SO',
|
|
'Kanan Jarrus',
|
|
'Karbin',
|
|
'Karina the Great',
|
|
'Kes Dameron',
|
|
'Ketsu Onyo',
|
|
'Ki-Adi-Mundi',
|
|
'King Katuunko',
|
|
'Kit Fisto',
|
|
'Kitster Banai',
|
|
'Klaatu',
|
|
'Klik-Klak',
|
|
'Korr Sella',
|
|
'Kylo Ren',
|
|
'L3-37',
|
|
'Lama Su',
|
|
'Lando Calrissian',
|
|
'Lanever Villecham',
|
|
'Leia Organa',
|
|
'Letta Turmond',
|
|
'Lieutenant Kaydel Ko Connix',
|
|
'Lieutenant Thire',
|
|
'Lobot',
|
|
'Logray',
|
|
'Lok Durd',
|
|
'Longo Two-Guns',
|
|
'Lor San Tekka',
|
|
'Lorth Needa',
|
|
'Lott Dod',
|
|
'Luke Skywalker',
|
|
'Lumat',
|
|
'Luminara Unduli',
|
|
'Lux Bonteri',
|
|
'Lyn Me',
|
|
'Lyra Erso',
|
|
'Mace Windu',
|
|
'Malakili',
|
|
'Mama the Hutt',
|
|
'Mars Guo',
|
|
'Mas Amedda',
|
|
'Mawhonic',
|
|
'Max Rebo',
|
|
'Maximilian Veers',
|
|
'Maz Kanata',
|
|
'ME-8D9',
|
|
'Meena Tills',
|
|
'Mercurial Swift',
|
|
'Mina Bonteri',
|
|
'Miraj Scintel',
|
|
'Mister Bones',
|
|
'Mod Terrik',
|
|
'Moden Canady',
|
|
'Mon Mothma',
|
|
'Moradmin Bast',
|
|
'Moralo Eval',
|
|
'Morley',
|
|
'Mother Talzin',
|
|
'Nahdar Vebb',
|
|
'Nahdonnis Praji',
|
|
'Nien Nunb',
|
|
'Niima the Hutt',
|
|
'Nines',
|
|
'Norra Wexley',
|
|
'Nute Gunray',
|
|
'Nuvo Vindi',
|
|
'Obi-Wan Kenobi',
|
|
'Odd Ball',
|
|
'Ody Mandrell',
|
|
'Omi',
|
|
'Onaconda Farr',
|
|
'Oola',
|
|
'OOM-9',
|
|
'Oppo Rancisis',
|
|
'Orn Free Taa',
|
|
'Oro Dassyne',
|
|
'Orrimarko',
|
|
'Osi Sobeck',
|
|
'Owen Lars',
|
|
'Pablo-Jill',
|
|
'Padmé Amidala',
|
|
'Pagetti Rook',
|
|
'Paige Tico',
|
|
'Paploo',
|
|
'Petty Officer Thanisson',
|
|
'Pharl McQuarrie',
|
|
'Plo Koon',
|
|
'Po Nudo',
|
|
'Poe Dameron',
|
|
'Poggle the Lesser',
|
|
'Pong Krell',
|
|
'Pooja Naberrie',
|
|
'PZ-4CO',
|
|
'Quarrie',
|
|
'Quay Tolsite',
|
|
'Queen Apailana',
|
|
'Queen Jamillia',
|
|
'Queen Neeyutnee',
|
|
'Qui-Gon Jinn',
|
|
'Quiggold',
|
|
'Quinlan Vos',
|
|
'R2-D2',
|
|
'R2-KT',
|
|
'R3-S6',
|
|
'R4-P17',
|
|
'R5-D4',
|
|
'RA-7',
|
|
'Rabé',
|
|
'Rako Hardeen',
|
|
'Ransolm Casterfo',
|
|
'Rappertunie',
|
|
'Ratts Tyerell',
|
|
'Raymus Antilles',
|
|
'Ree-Yees',
|
|
'Reeve Panzoro',
|
|
'Rey',
|
|
'Ric Olié',
|
|
'Riff Tamson',
|
|
'Riley',
|
|
'Rinnriyin Di',
|
|
'Rio Durant',
|
|
'Rogue Squadron',
|
|
'Romba',
|
|
'Roos Tarpals',
|
|
'Rose Tico',
|
|
'Rotta the Hutt',
|
|
'Rukh',
|
|
'Rune Haako',
|
|
'Rush Clovis',
|
|
'Ruwee Naberrie',
|
|
'Ryoo Naberrie',
|
|
'Sabé',
|
|
'Sabine Wren',
|
|
'Saché',
|
|
'Saelt-Marae',
|
|
'Saesee Tiin',
|
|
'Salacious B. Crumb',
|
|
'San Hill',
|
|
'Sana Starros',
|
|
'Sarco Plank',
|
|
'Sarkli',
|
|
'Satine Kryze',
|
|
'Savage Opress',
|
|
'Sebulba',
|
|
'Senator Organa',
|
|
'Sergeant Kreel',
|
|
'Seventh Sister',
|
|
'Shaak Ti',
|
|
'Shara Bey',
|
|
'Shmi Skywalker',
|
|
'Shu Mai',
|
|
'Sidon Ithano',
|
|
'Sifo-Dyas',
|
|
'Sim Aloo',
|
|
'Siniir Rath Velus',
|
|
'Sio Bibble',
|
|
'Sixth Brother',
|
|
'Slowen Lo',
|
|
'Sly Moore',
|
|
'Snaggletooth',
|
|
'Snap Wexley',
|
|
'Snoke',
|
|
'Sola Naberrie',
|
|
'Sora Bulq',
|
|
'Strono Tuggs',
|
|
'Sy Snootles',
|
|
'Tallissan Lintra',
|
|
'Tarfful',
|
|
'Tasu Leech',
|
|
'Taun We',
|
|
'TC-14',
|
|
'Tee Watt Kaa',
|
|
'Teebo',
|
|
'Teedo',
|
|
'Teemto Pagalies',
|
|
'Temiri Blagg',
|
|
'Tessek',
|
|
'Tey How',
|
|
'Thane Kyrell',
|
|
'The Bendu',
|
|
'The Smuggler',
|
|
'Thrawn',
|
|
'Tiaan Jerjerrod',
|
|
'Tion Medon',
|
|
'Tobias Beckett',
|
|
'Tulon Voidgazer',
|
|
'Tup',
|
|
'U9-C4',
|
|
'Unkar Plutt',
|
|
'Val Beckett',
|
|
'Vanden Willard',
|
|
'Vice Admiral Amilyn Holdo',
|
|
'Vober Dand',
|
|
'WAC-47',
|
|
'Wag Too',
|
|
'Wald',
|
|
'Walrus Man',
|
|
'Warok',
|
|
'Wat Tambor',
|
|
'Watto',
|
|
'Wedge Antilles',
|
|
'Wes Janson',
|
|
'Wicket W. Warrick',
|
|
'Wilhuff Tarkin',
|
|
'Wollivan',
|
|
'Wuher',
|
|
'Wullf Yularen',
|
|
'Xamuel Lennox',
|
|
'Yaddle',
|
|
'Yarael Poof',
|
|
'Yoda',
|
|
'Zam Wesell',
|
|
'Zev Senesca',
|
|
'Ziro the Hutt',
|
|
'Zuckuss',
|
|
]
|
|
|
|
export default MentionExample
|