Last active
April 1, 2016 16:35
-
-
Save ianstormtaylor/f28bdb94b0bf6593e45c to your computer and use it in GitHub Desktop.
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
import { getDefaultKeyBinding } from 'draft-js' | |
/** | |
* Handle default key bindings. | |
* | |
* @param {Event} event | |
* @return {String} defaultCommand | |
*/ | |
export default function defaultKeyBindingPlugin() { | |
return { | |
keyBindingFn(event) { | |
return getDefaultKeyBinding(event) | |
} | |
} | |
} |
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
import React from 'react' | |
import defaultKeyBindingPlugin from './default-key-binding-plugin' | |
import { CompositeDecorator, Editor, EditorState } from 'draft-js' | |
/** | |
* Proxy method properties that will call into the original editor's "ref". | |
*/ | |
const PROXY_PROPS = [ | |
'blur', | |
'exitCurrentMode', | |
'focus', | |
'getClipboard', | |
'getEditorKey', | |
'onDragEnter', | |
'onDragLeave', | |
'removeRenderGuard', | |
'restoreEditorDOM', | |
'setClipboard', | |
'setMode', | |
'setRenderGuard', | |
'update', | |
] | |
/** | |
* Handler properties that will be wrapped with a middleware stack, to be | |
* evaluated by each plugin. | |
*/ | |
const HANDLER_PROPS = [ | |
'blockRendererFn', | |
'blockStyleFn', | |
'handleBeforeInput', | |
'handleDrop', | |
'handleDroppedFiles', | |
'handleKeyCommand', | |
'handlePastedFiles', | |
'handlePastedText', | |
'handleReturn', | |
'keyBindingFn', | |
'onDownArrow', | |
'onEscape', | |
'onTab', | |
'onUpArrow', | |
] | |
/** | |
* All properties that are plugin-controlled, to create a single plugin for all | |
* of the top-level editor props, to override any plugin behavior. | |
*/ | |
const PLUGIN_PROPS = [ | |
...HANDLER_PROPS, | |
'customStyleMap', | |
] | |
/** | |
* Properties that are specific to the pluggable version of the editor, and that | |
* should not be passed into the original editor. | |
*/ | |
const PLUGGABLE_PROPS = [ | |
'defaultKeyBindings', | |
'editorState', | |
'onChange', | |
'plugins', | |
] | |
/** | |
* Component. | |
*/ | |
class PluggableEditor extends React.Component { | |
/** | |
* Types. | |
*/ | |
static propTypes = { | |
defaultKeyBindings: React.PropTypes.bool, | |
editorState: React.PropTypes.object.isRequired, | |
onChange: React.PropTypes.func.isRequired, | |
plugins: React.PropTypes.arrayOf(React.PropTypes.object), | |
}; | |
/** | |
* Defaults. | |
*/ | |
static defaultProps = { | |
defaultKeyBindings: true, | |
plugins: [], | |
}; | |
/** | |
* Create a proxy that calls into the original editor "ref" for each of the | |
* proxy methods. | |
* | |
* @param {Object} props | |
*/ | |
constructor(props) { | |
super(props) | |
for (const method of PROXY_PROPS) { | |
this[method] = (...args) => { | |
return this.refs.editor[method](...args) | |
} | |
} | |
} | |
/** | |
* On mount, set the decorator based on all of the plugins, and update the | |
* `editorState` since setting the decorator will have caused it to change. | |
*/ | |
componentWillMount() { | |
const decorator = this.resolveDecorator() | |
const editorState = EditorState.set(this.props.editorState, { decorator }) | |
this.onChange(editorState) | |
} | |
/** | |
* Get the editor's state. | |
* | |
* @return {EditorState} editorState | |
* @public | |
*/ | |
getEditorState = () => { | |
return this.props.editorState | |
} | |
/** | |
* When the <Editor> changes, pass the value through the plugins as middleware | |
* so they can update with any extra changes, and then call `onChange`. | |
* | |
* @param {EditorState} editorState | |
* @public | |
*/ | |
onChange = (editorState) => { | |
const plugins = this.resolvePlugins() | |
for (const plugin of plugins) { | |
if (!plugin.onChange) continue | |
editorState = plugin.onChange(editorState, this) || editorState | |
} | |
this.props.onChange(editorState) | |
} | |
/** | |
* Render the editor. | |
* | |
* @return {Object} | |
*/ | |
render() { | |
const { editorState } = this.props | |
const editorProps = this.resolveEditorProps() | |
const pluginProps = this.resolvePluginProps() | |
const handlerProps = this.resolveHandlerProps() | |
const customStyleMap = this.resolveCustomStyleMap() | |
return ( | |
<Editor | |
{...pluginProps} | |
{...handlerProps} | |
{...editorProps} | |
ref='editor' | |
onChange={this.onChange} | |
editorState={editorState} | |
customStyleMap={customStyleMap} | |
/> | |
) | |
} | |
/** | |
* Resolve the `customStyleMap` property by iterating all of the plugins. | |
* | |
* @return {Object} styles | |
*/ | |
resolveCustomStyleMap() { | |
const plugins = this.resolvePlugins() | |
let styles = {} | |
for (const plugin of plugins) { | |
if (!plugin.customStyleMap) continue | |
styles = { | |
...styles, | |
...plugin.customStyleMap, | |
} | |
} | |
return styles | |
} | |
/** | |
* Resolve the composite `decorator` function by joining any decorators provided | |
* by all of the plugins. | |
* | |
* @return {CompositeDecorator} decorator | |
*/ | |
resolveDecorator() { | |
const plugins = this.resolvePlugins() | |
let decorators = [] | |
for (const plugin of plugins) { | |
if (!plugin.decorators) continue | |
decorators = [ | |
...decorators, | |
...plugin.decorators, | |
] | |
} | |
return new CompositeDecorator(decorators) | |
} | |
/** | |
* Resolve all of the top-level editor properties that should be passed into | |
* the editor, omitting properties that are specific to the pluggable version | |
* of the editor, or properties that are turned into the editor-level plugin. | |
* | |
* @return {Object} props | |
*/ | |
resolveEditorProps() { | |
const props = {} | |
for (const key in this.props) { | |
if (PLUGIN_PROPS.includes(key)) continue | |
if (PLUGGABLE_PROPS.includes(key)) continue | |
props[key] = this.props[key] | |
} | |
return props | |
} | |
/** | |
* Resolve the handler method properties from all of the plugins. | |
* | |
* @return {Object} props | |
*/ | |
resolveHandlerProps() { | |
let props = {} | |
for (const method of HANDLER_PROPS) { | |
props[method] = (...args) => { | |
args.push(this) | |
const plugins = this.resolvePlugins() | |
for (const plugin of plugins) { | |
if (!plugin[method]) continue | |
const ret = plugin[method](...args) | |
if (ret !== undefined) return ret | |
} | |
} | |
} | |
return props | |
} | |
/** | |
* Resolve the current plugins active for the editor. | |
* | |
* @return {Array} plugins | |
*/ | |
resolvePlugins() { | |
const plugins = this.props.plugins.slice() | |
plugins.unshift(this.resolveEditorPropsPlugin()) | |
if (this.props.defaultKeyBindings) plugins.push(defaultKeyBindingPlugin()) | |
return plugins | |
} | |
/** | |
* Resolve any extra properties defined by all of the plugins. | |
* | |
* @return {Object} props | |
*/ | |
resolvePluginProps() { | |
const plugins = this.resolvePlugins() | |
let props = {} | |
for (const plugin of plugins) { | |
if (!plugin.getEditorProps) continue | |
props = { | |
...props, | |
...plugin.getEditorProps(this) | |
} | |
} | |
return props | |
} | |
/** | |
* Resolve a single plugin from the top-level properties passed into the | |
* editor, that will be added first in the chain, so that it can override any | |
* of the other plugins's logic that it wants. | |
* | |
* @return {Object} plugin | |
*/ | |
resolveEditorPropsPlugin() { | |
const plugin = {} | |
for (const prop of PLUGIN_PROPS) { | |
if (prop in this.props) plugin[prop] = this.props[prop] | |
} | |
return plugin | |
} | |
} | |
/** | |
* Export. | |
*/ | |
export default PluggableEditor |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment