Skip to content

Instantly share code, notes, and snippets.

@subblue
Last active June 6, 2016 09:54
Show Gist options
  • Save subblue/4b4939ac8760754af8eca779206afd3f to your computer and use it in GitHub Desktop.
Save subblue/4b4939ac8760754af8eca779206afd3f to your computer and use it in GitHub Desktop.
RichTextEditor
'use strict';
/* eslint "react/prop-types": 0 */
// import Immutable from 'immutable';
import React, { PropTypes, Component } from 'react';
import { Editor, EditorState, ContentState, RichUtils, convertFromHTML, convertFromRaw } from 'draft-js';
import { stateToHTML } from 'draft-js-export-html';
// based on this example:
// https://github.com/facebook/draft-js/tree/master/examples/rich
export default class RichTextEditor extends Component {
static propTypes = {
dispatch: PropTypes.func,
id: PropTypes.string,
structure: PropTypes.object,
dataKey: PropTypes.string,
value: PropTypes.oneOfType([PropTypes.string, PropTypes.object]),
inline: PropTypes.bool,
updateHandler: PropTypes.func,
postRender: PropTypes.func
};
constructor(props) {
super(props);
this.focus = this.focus.bind(this);
this.onChange = this.onChange.bind(this);
this.handleKeyCommand = this.handleKeyCommand.bind(this);
this.handleReturn = this.handleReturn.bind(this);
this.toggleBlockType = this.toggleBlockType.bind(this);
this.toggleInlineStyle = this.toggleInlineStyle.bind(this);
this._timeout = 0;
this.state = {
editorState: this.setEditorState(props.value)
};
}
componentWillMount() {
// console.log('structure:', this.props.id, this.props.dataKey, this.props.structure.toJS());
this.setState({editorState: this.setEditorState(this.props.value)});
}
componentWillUnmount() {
this.updateContent(this.state.editorState);
}
componentWillReceiveProps(nextProps) {
if (nextProps.id !== this.props.id) {
console.log('update editor state');
this.setState({editorState: this.setEditorState(nextProps.value)});
}
}
setEditorState(value) {
if (!value) {
return EditorState.createEmpty();
}
if (typeof value === 'string') {
// legacy HTML content, which needs to be wrapped into a single parent div
// and we need to strip multiple   that can creep in
let val = `<div>${value}</div>`;
val = val.replace(/\s*(&nbsp;)+\s*/g, '&nbsp;');
val = val.replace(/\n[ ]+/g, '\n');
const raw = convertFromHTML(val);
const contentState = ContentState.createFromBlockArray(raw);
return EditorState.createWithContent(contentState);
}
const blocks = convertFromRaw(value);
return EditorState.createWithContent(blocks);
}
focus(event) {
event.preventDefault();
event.stopPropagation();
this.refs.editor.focus();
}
onChange(editorState) {
const { inline } = this.props;
this.setState({editorState});
// console.log('editor state:', stateToHTML(editorState.getCurrentContent()));
if (!inline) {
// debounce the update
clearTimeout(this._timeout);
this._timeout = window.setTimeout(() => {
this.updateContent(editorState);
}, 300);
}
}
updateContent(editorState) {
const { updateHandler, dataKey, value } = this.props;
const html = stateToHTML(editorState.getCurrentContent());
if (html !== value) {
updateHandler(dataKey, html);
}
}
handleKeyCommand(command) {
const { editorState } = this.state;
const newState = RichUtils.handleKeyCommand(editorState, command);
// console.log('command:', command);
if (newState) {
this.onChange(newState);
return true;
}
return false;
}
handleReturn(event) {
// console.log('soft return', event.shiftKey);
if (!event.shiftKey) {
// normal return
return this.handleKeyCommand('split-block');
}
// soft return
const { editorState } = this.state;
const newState = RichUtils.insertSoftNewline(editorState);
if (newState) {
this.onChange(newState);
return true;
}
return false;
}
toggleBlockType(blockType) {
this.onChange(
RichUtils.toggleBlockType(
this.state.editorState,
blockType
)
);
}
toggleInlineStyle(inlineStyle) {
this.onChange(
RichUtils.toggleInlineStyle(
this.state.editorState,
inlineStyle
)
);
}
render() {
const { structure, dataKey, inline } = this.props;
const { editorState } = this.state;
// console.log('Render RichTextEditor:', id, inline);
const _id = ('' + dataKey).replace('.', '-');
let containerClass = 'ui-field richtext';
if (inline) {
containerClass += ' inline';
}
let className = `richtext-editor field-${_id}`;
// If the user changes block type before entering any text, we can
// either style the placeholder or hide it. Let's just hide it now.
const contentState = editorState.getCurrentContent();
if (!contentState.hasText() && contentState.getBlockMap().first().getType() !== 'unstyled') {
className += ' richtext-hide-placeholder';
}
let label;
if (structure.get('label') && !inline) {
label = <label className="ui-label full-width">{structure.get('label')}</label>;
}
return (
<div className={containerClass}>
{label}
<nav className="richtext-controls">
<BlockStyleControls
editorState={editorState}
onToggle={this.toggleBlockType}
/>
<InlineStyleControls
editorState={editorState}
onToggle={this.toggleInlineStyle}
/>
</nav>
<div className={className} onClick={this.focus}>
<Editor
blockStyleFn={getBlockStyle}
customStyleMap={styleMap}
editorState={editorState}
handleKeyCommand={this.handleKeyCommand}
handleReturn={this.handleReturn}
onChange={this.onChange}
placeholder="Content..."
ref="editor"
spellCheck={true}
/>
</div>
</div>
);
}
}
// // ref: https://github.com/facebook/draft-js/issues/395
// const blockRenderMap = Immutable.Map({
// paragraph: {
// element: 'p'
// },
// unstyled: {
// element: 'p'
// }
// });
//
// // Include 'paragraph' as a valid block and updated the unstyled element but
// // keep support for other draft default block types
// const extendedBlockRenderMap = DefaultDraftBlockRenderMap.merge(blockRenderMap);
// Custom overrides for "code" style.
const styleMap = {
CODE: {
backgroundColor: 'rgba(0, 0, 0, 0.05)',
fontFamily: '"Inconsolata", "Menlo", "Consolas", monospace',
fontSize: 16,
padding: 2
}
};
function getBlockStyle(block) {
switch (block.getType()) {
case 'blockquote':
return 'RichEditor-blockquote';
default:
return null;
}
}
const BLOCK_TYPES = [
{label: 'H1', style: 'header-one', className: 'richtext-btn richtext-header-btn'},
{label: 'H2', style: 'header-two', className: 'richtext-btn richtext-header-btn'},
// {label: 'H3', style: 'header-three'},
// {label: 'H4', style: 'header-four'},
// {label: 'H5', style: 'header-five'},
// {label: 'H6', style: 'header-six'},
{label: 'format_quote', style: 'blockquote'},
{label: 'format_list_bulleted', style: 'unordered-list-item'},
{label: 'format_list_numbered', style: 'ordered-list-item'}
// {label: 'Code Block', style: 'code-block'}
];
const BlockStyleControls = ({editorState, onToggle}) => {
const selection = editorState.getSelection();
const blockType = editorState
.getCurrentContent()
.getBlockForKey(selection.getStartKey())
.getType();
return (
<section className="richtext-control-group">
{BLOCK_TYPES.map((type) =>
<StyleButton
key={type.label}
active={type.style === blockType}
className={type.className || 'richtext-btn icon-btn'}
label={type.label}
onToggle={onToggle}
style={type.style}
/>
)}
</section>
);
};
var INLINE_STYLES = [
{label: 'format_bold', style: 'BOLD'},
{label: 'format_italic', style: 'ITALIC'},
{label: 'format_underlined', style: 'UNDERLINE'}
// {label: 'Monospace', style: 'CODE'}
];
const InlineStyleControls = ({editorState, onToggle}) => {
var currentStyle = editorState.getCurrentInlineStyle();
return (
<section className="richtext-control-group">
{INLINE_STYLES.map(type =>
<StyleButton
key={type.label}
active={currentStyle.has(type.style)}
className="richtext-btn icon-btn"
label={type.label}
onToggle={onToggle}
style={type.style}
/>
)}
</section>
);
};
class StyleButton extends Component {
constructor() {
super();
this.onToggle = this.onToggle.bind(this);
}
onToggle(event) {
event.preventDefault();
this.props.onToggle(this.props.style);
}
render() {
let className = this.props.className;
if (this.props.active) {
className += ' active';
}
return (
<span className={className} onMouseDown={this.onToggle}>
{this.props.label}
</span>
);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment