Skip to content

Instantly share code, notes, and snippets.

@ianstormtaylor
Last active April 1, 2016 16:35
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save ianstormtaylor/f28bdb94b0bf6593e45c to your computer and use it in GitHub Desktop.
Save ianstormtaylor/f28bdb94b0bf6593e45c to your computer and use it in GitHub Desktop.
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)
}
}
}
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