react-draft-wysiwyg
Version:
A wysiwyg on top of DraftJS.
262 lines (241 loc) • 8.02 kB
JavaScript
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import addMention from '../addMention';
import KeyDownHandler from '../../../event-handler/keyDown';
import SuggestionHandler from '../../../event-handler/suggestions';
import './styles.css';
class Suggestion {
constructor(config) {
const {
separator,
trigger,
getSuggestions,
onChange,
getEditorState,
getWrapperRef,
caseSensitive,
dropdownClassName,
optionClassName,
modalHandler,
} = config;
this.config = {
separator,
trigger,
getSuggestions,
onChange,
getEditorState,
getWrapperRef,
caseSensitive,
dropdownClassName,
optionClassName,
modalHandler,
};
}
findSuggestionEntities = (contentBlock, callback) => {
if (this.config.getEditorState()) {
const { separator, trigger, getSuggestions, getEditorState } = this.config;
const selection = getEditorState().getSelection();
if (selection.get('anchorKey') === contentBlock.get('key') &&
selection.get('anchorKey') === selection.get('focusKey')) {
let text = contentBlock.getText();
text = text.substr(
0,
selection.get('focusOffset') === text.length - 1
? text.length
: selection.get('focusOffset') + 1,
);
let index = text.lastIndexOf(separator + trigger);
let preText = separator + trigger;
if ((index === undefined || index < 0) && text[0] === trigger) {
index = 0;
preText = trigger;
}
if (index >= 0) {
const mentionText = text.substr(index + preText.length, text.length);
const suggestionPresent =
getSuggestions().some((suggestion) => {
if (suggestion.value) {
if (this.config.caseSensitive) {
return suggestion.value.indexOf(mentionText) >= 0;
}
return suggestion.value.toLowerCase()
.indexOf(mentionText && mentionText.toLowerCase()) >= 0;
}
return false;
});
if (suggestionPresent) {
callback(index === 0 ? 0 : index + 1, text.length);
}
}
}
}
}
getSuggestionComponent = getSuggestionComponent.bind(this);
getSuggestionDecorator = () => ({
strategy: this.findSuggestionEntities,
component: this.getSuggestionComponent(),
});
}
function getSuggestionComponent() {
const { config } = this;
return class SuggestionComponent extends Component {
static propTypes = {
children: PropTypes.array,
};
state: Object = {
style: { left: 15 },
activeOption: -1,
showSuggestions: true,
};
componentDidMount() {
const editorRect = config.getWrapperRef().getBoundingClientRect();
const suggestionRect = this.suggestion.getBoundingClientRect();
const dropdownRect = this.dropdown.getBoundingClientRect();
let left;
let right;
let bottom;
if (editorRect.width < (suggestionRect.left - editorRect.left) + dropdownRect.width) {
right = 15;
} else {
left = 15;
}
if (editorRect.bottom < dropdownRect.bottom) {
bottom = 0;
}
this.setState({ // eslint-disable-line react/no-did-mount-set-state
style: { left, right, bottom },
});
KeyDownHandler.registerCallBack(this.onEditorKeyDown);
SuggestionHandler.open();
config.modalHandler.setSuggestionCallback(this.closeSuggestionDropdown);
this.filterSuggestions(this.props);
}
componentWillReceiveProps(props) {
if (this.props.children !== props.children) {
this.filterSuggestions(props);
this.setState({
showSuggestions: true,
});
}
}
componentWillUnmount() {
KeyDownHandler.deregisterCallBack(this.onEditorKeyDown);
SuggestionHandler.close();
config.modalHandler.removeSuggestionCallback();
}
onEditorKeyDown = (event) => {
const { activeOption } = this.state;
const newState = {};
if (event.key === 'ArrowDown') {
event.preventDefault();
if (activeOption === this.filteredSuggestions.length - 1) {
newState.activeOption = 0;
} else {
newState.activeOption = activeOption + 1;
}
} else if (event.key === 'ArrowUp') {
if (activeOption <= 0) {
newState.activeOption = this.filteredSuggestions.length - 1;
} else {
newState.activeOption = activeOption - 1;
}
} else if (event.key === 'Escape') {
newState.showSuggestions = false;
SuggestionHandler.close();
} else if (event.key === 'Enter') {
this.addMention();
}
this.setState(newState);
}
onOptionMouseEnter = (event) => {
const index = event.target.getAttribute('data-index');
this.setState({
activeOption: index,
});
}
onOptionMouseLeave = () => {
this.setState({
activeOption: -1,
});
}
setSuggestionReference: Function = (ref: Object): void => {
this.suggestion = ref;
};
setDropdownReference: Function = (ref: Object): void => {
this.dropdown = ref;
};
closeSuggestionDropdown: Function = (): void => {
this.setState({
showSuggestions: false,
});
}
filteredSuggestions = [];
filterSuggestions = (props) => {
const mentionText = props.children[0].props.text.substr(1);
const suggestions = config.getSuggestions();
this.filteredSuggestions =
suggestions && suggestions.filter((suggestion) => {
if (!mentionText || mentionText.length === 0) {
return true;
}
if (config.caseSensitive) {
return suggestion.value.indexOf(mentionText) >= 0;
}
return suggestion.value.toLowerCase()
.indexOf(mentionText && mentionText.toLowerCase()) >= 0;
});
}
addMention = () => {
const { activeOption } = this.state;
const editorState = config.getEditorState();
const { onChange, separator, trigger } = config;
const selectedMention = this.filteredSuggestions[activeOption];
if (selectedMention) {
addMention(editorState, onChange, separator, trigger, selectedMention);
}
}
render() {
const { children } = this.props;
const { activeOption, showSuggestions } = this.state;
const { dropdownClassName, optionClassName } = config;
return (
<span
className="rdw-suggestion-wrapper"
ref={this.setSuggestionReference}
onClick={config.modalHandler.onSuggestionClick}
aria-haspopup="true"
aria-label="rdw-suggestion-popup"
>
<span>{children}</span>
{showSuggestions &&
<span
className={classNames('rdw-suggestion-dropdown', dropdownClassName)}
contentEditable="false"
suppressContentEditableWarning
style={this.state.style}
ref={this.setDropdownReference}
>
{this.filteredSuggestions.map((suggestion, index) =>
(<span
key={index}
spellCheck={false}
onClick={this.addMention}
data-index={index}
onMouseEnter={this.onOptionMouseEnter}
onMouseLeave={this.onOptionMouseLeave}
className={classNames(
'rdw-suggestion-option',
optionClassName,
{ 'rdw-suggestion-option-active': (index === activeOption) },
)}
>
{suggestion.text}
</span>))}
</span>}
</span>
);
}
};
}
module.exports = Suggestion;