Last active
June 6, 2016 09:54
-
-
Save subblue/4b4939ac8760754af8eca779206afd3f to your computer and use it in GitHub Desktop.
RichTextEditor
This file contains 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
'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*( )+\s*/g, ' '); | |
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