Skip to content

Instantly share code, notes, and snippets.

@steveliles
Created September 20, 2017 12:47
Show Gist options
  • Save steveliles/038af7c3bc22eca79e60d5b584dfdce4 to your computer and use it in GitHub Desktop.
Save steveliles/038af7c3bc22eca79e60d5b584dfdce4 to your computer and use it in GitHub Desktop.
Implementing @Mention's in Draft.js
const { Editor, EditorState, CompositeDecorator, Modifier, SelectionState } = Draft;
const getMentionPosition = () => {
const range = window.getSelection().getRangeAt(0).cloneRange();
const rect = range.getBoundingClientRect();
return { top: rect.bottom, left: rect.left }
}
const getCaretPosition = (editorState) => {
return editorState.getSelection().getAnchorOffset()
}
const getText = (editorState, start, end) => {
const block = getCurrentBlock(editorState)
const blockText = block.getText()
return blockText.substring(start, end)
}
const getCurrentBlock = editorState => {
if (editorState.getSelection) {
const selectionState = editorState.getSelection()
const contentState = editorState.getCurrentContent()
const block = contentState.getBlockForKey(selectionState.getStartKey())
return block
}
}
const people = [{
id: 'annette1',
name: 'Annette Cartwright'
}, {
id:'steve123',
name: 'Steve Liles'
}, {
id: 'jojo96',
name: 'Jo Orange'
}, {
id: 'markbame2000',
name: 'Mark Martirez'
}, {
id: 'aliassam',
name: 'Ali Assam'
}, {
id: 'justjane',
name: 'Jane Justin'
}]
class Person extends React.Component {
constructor(props) {
super(props)
this.handleClick = this.handleClick.bind(this)
}
handleClick(ev){
ev.preventDefault()
ev.stopPropagation()
const { person, onClick } = this.props
onClick(person)
}
render(){
const { person: { name }, selected } = this.props
return (
<li className={selected ? 'selected' : ''} onClick={this.handleClick}>
<span>{name}</span>
</li>
)
}
}
const People = ({top,left,people,selectedIndex=0,onClick}) =>
<ol className="people" style={{top,left}}>
{
people.map((person,idx) =>
<Person key={person.id} person={person} selected={idx === selectedIndex} onClick={onClick}/>
)
}
</ol>
const Mention = ({children, contentState, entityKey}) =>
<span className="mention" title={contentState.getEntity(entityKey).getData().id}>{children}</span>
const newEntityLocationStrategy = type => {
const findEntitiesOfType = (contentBlock, callback, contentState) => {
contentBlock.findEntityRanges(character => {
const entityKey = character.getEntity()
return (
entityKey !== null &&
contentState.getEntity(entityKey).getType() === type
)
}, callback)
}
return findEntitiesOfType
}
const decorator = new CompositeDecorator([
{
strategy: newEntityLocationStrategy('MENTION'),
component: Mention
}
])
class Container extends React.Component {
constructor(props) {
super(props);
this.state = {
editorState: EditorState.createEmpty(decorator)
};
this.handleChange = this.handleChange.bind(this)
this.handleBeforeInput = this.handleBeforeInput.bind(this)
this.acceptSelectedPerson = this.acceptSelectedPerson.bind(this)
this.handleReturn = this.handleReturn.bind(this)
this.handleTab = this.handleTab.bind(this)
this.handleEscape = this.handleEscape.bind(this)
this.handleUpArrow = this.handleUpArrow.bind(this)
this.handleDownArrow = this.handleDownArrow.bind(this)
this.confirmMention = this.confirmMention.bind(this)
this.handleClick = this.handleClick.bind(this)
}
handleChange(editorState) {
const { mention } = this.state
if (mention) {
const caret = getCaretPosition(editorState)
if (caret > mention.offset) {
const mentionText = getText(editorState, mention.offset+1, caret).toLowerCase()
const candidates = people.filter(person => person.name.toLowerCase().startsWith(mentionText))
this.setState({
editorState,
mention: {
...mention,
selectedIndex: 0,
people: candidates
}
})
} else {
// last change deleted the @ character, so exit mention mode
this.setState({editorState, mention: undefined})
}
} else {
this.setState({editorState})
}
}
handleBeforeInput(ch, editorState) {
const { mention } = this.state
if (mention) {
// ???
} else {
if (ch === '@') {
// enter "mention mode"
this.setState({
mention: {
people: [],
selectedIndex: 0,
offset: getCaretPosition(editorState),
position: getMentionPosition()
}
})
}
}
return false
}
handleEscape(ev) {
if (this.state.mention) {
this.setState({ mention: undefined })
ev.preventDefault()
}
}
acceptSelectedPerson(ev) {
const { mention } = this.state
if (mention) {
if (mention.people && mention.people.length > mention.selectedIndex) {
let person = mention.people[mention.selectedIndex]
this.confirmMention(person)
} else {
this.setState({mention:undefined})
}
ev.preventDefault()
return true
}
return false
}
handleTab(ev) {
this.acceptSelectedPerson(ev)
}
handleReturn(ev, editorState) {
return this.acceptSelectedPerson(ev)
}
handleUpArrow(ev){
if (this.state.mention) {
this.setState(({mention}) => ({
mention:{
...mention,
selectedIndex: Math.max(0, mention.selectedIndex-1)
}}))
ev.preventDefault()
}
}
handleDownArrow(ev){
if (this.state.mention) {
this.setState(({mention}) => ({
mention:{
...mention,
selectedIndex: Math.min(mention.selectedIndex+1, people.length-1)
}
}))
ev.preventDefault()
}
}
handleClick(ev) {
if (this.state.mention) {
setTimeout(()=>this.setState({ mention: undefined }))
}
this._editor.focus()
}
confirmMention(person){
const { editorState, mention } = this.state
const contentState = editorState.getCurrentContent()
const contentStateWithEntity = contentState.createEntity(
'MENTION',
'IMMUTABLE',
person
)
const entityKey = contentStateWithEntity.getLastCreatedEntityKey()
const block = getCurrentBlock(editorState)
const blockKey = block.getKey()
const mentionText = '@' + person.name
const contentStateWithReplacedText = Modifier.replaceText(
contentStateWithEntity,
new SelectionState({
anchorKey: blockKey,
anchorOffset: mention.offset,
focusKey: blockKey,
focusOffset: getCaretPosition(editorState),
isBackward: false,
hasFocus: true
}),
mentionText,
null,
entityKey
)
const newEditorState = EditorState.set(editorState, {
currentContent: contentStateWithReplacedText,
selection: new SelectionState({
anchorKey: blockKey,
anchorOffset: mention.offset + mentionText.length,
focusKey: blockKey,
focusOffset: mention.offset + mentionText.length,
isBackward: false,
hasFocus: true
})
})
setTimeout(() => {
this.setState({
mention: undefined,
editorState: newEditorState
})
}, 0)
}
render() {
const { mention, editorState } = this.state
return (
<div className="container-root" onClick={this.handleClick}>
<Editor
ref={ref => this._editor = ref}
placeholder="Try typing @mention's"
editorState={editorState}
onChange={this.handleChange}
handleBeforeInput={this.handleBeforeInput}
handleReturn={this.handleReturn}
onTab={this.handleTab}
onEscape={this.handleEscape}
onUpArrow={this.handleUpArrow}
onDownArrow={this.handleDownArrow}
/>
{ mention ?
<People
{...(mention.position)}
people={mention.people}
selectedIndex={mention.selectedIndex}
onClick={this.confirmMention}/>
:
null
}
</div>
);
}
}
ReactDOM.render(<Container />, document.getElementById('react-root'))
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment