Created
September 20, 2017 12:47
-
-
Save steveliles/038af7c3bc22eca79e60d5b584dfdce4 to your computer and use it in GitHub Desktop.
Implementing @Mention's in Draft.js
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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