1
0
mirror of https://github.com/ianstormtaylor/slate.git synced 2025-02-24 17:23:07 +01:00
slate/lib/components/editor.js
Ian Storm Taylor ccac6102a5 fixes
2016-08-14 13:21:46 -07:00

334 lines
6.7 KiB
JavaScript

import Content from './content'
import CorePlugin from '../plugins/core'
import Debug from 'debug'
import React from 'react'
import Schema from '../models/schema'
import State from '../models/state'
import isReactComponent from '../utils/is-react-component'
import typeOf from 'type-of'
/**
* Debug.
*/
const debug = Debug('slate:editor')
/**
* Noop.
*
* @type {Function}
*/
function noop() {}
/**
* Event handlers to mix in to the editor.
*
* @type {Array}
*/
const EVENT_HANDLERS = [
'onBeforeInput',
'onBlur',
'onCopy',
'onCut',
'onDrop',
'onKeyDown',
'onPaste',
'onSelect'
]
/**
* Editor.
*
* @type {Component}
*/
class Editor extends React.Component {
/**
* Properties.
*/
static propTypes = {
className: React.PropTypes.string,
onBeforeChange: React.PropTypes.func,
onChange: React.PropTypes.func,
onDocumentChange: React.PropTypes.func,
onSelectionChange: React.PropTypes.func,
placeholder: React.PropTypes.any,
placeholderClassName: React.PropTypes.string,
placeholderStyle: React.PropTypes.object,
plugins: React.PropTypes.array,
readOnly: React.PropTypes.bool,
schema: React.PropTypes.object,
spellCheck: React.PropTypes.bool,
state: React.PropTypes.object.isRequired,
style: React.PropTypes.object
};
/**
* Default properties.
*/
static defaultProps = {
onChange: noop,
onDocumentChange: noop,
onSelectionChange: noop,
plugins: [],
readOnly: false,
schema: {},
spellCheck: true
};
/**
* When created, compute the plugins from `props`.
*
* @param {Object} props
*/
constructor(props) {
super(props)
this.tmp = {}
this.state = {}
this.state.plugins = this.resolvePlugins(props)
this.state.schema = this.resolveSchema(this.state.plugins)
this.state.state = this.onBeforeChange(props.state)
// Mix in the event handlers.
for (const method of EVENT_HANDLERS) {
this[method] = (...args) => {
this.onEvent(method, ...args)
}
}
}
/**
* When the `props` are updated, recompute the state and plugins.
*
* @param {Object} props
*/
componentWillReceiveProps = (props) => {
if (props.plugins != this.props.plugins) {
const plugins = this.resolvePlugins(props)
const schema = this.resolveSchema(plugins)
this.setState({ plugins, schema })
}
else if (props.schema != this.props.schema) {
const schema = this.resolveSchema(this.state.plugins)
this.setState({ schema })
}
this.setState({
state: this.onBeforeChange(props.state)
})
}
/**
* Programmatically blur the editor.
*/
blur = () => {
const state = this.state.state
.transform()
.blur()
.apply()
this.onChange(state)
}
/**
* Programmatically focus the editor.
*/
focus = () => {
const state = this.state.state
.transform()
.focus()
.apply()
this.onChange(state)
}
/**
* Get the editor's current `schema`.
*
* @return {Schema}
*/
getSchema = () => {
return this.state.schema
}
/**
* Get the editor's current `state`.
*
* @return {State}
*/
getState = () => {
return this.state.state
}
/**
* When the editor receives a new 'state'
*
* @param {State} state
* @return {State} newState
*/
onBeforeChange = (state) => {
if (state == this.state.state) return state
for (const plugin of this.state.plugins) {
if (!plugin.onBeforeChange) continue
const newState = plugin.onBeforeChange(state, this)
if (newState == null) continue
state = newState
}
return state
}
/**
* When the `state` changes, pass through plugins, then bubble up.
*
* @param {State} state
*/
onChange = (state) => {
if (state == this.state.state) return
state = this.onBeforeChange(state)
for (const plugin of this.state.plugins) {
if (!plugin.onChange) continue
const newState = plugin.onChange(state, this)
if (newState == null) continue
state = newState
}
this.props.onChange(state)
if (state.document != this.tmp.document) {
this.props.onDocumentChange(state.document, state)
this.tmp.document = state.document
}
if (state.selection != this.tmp.selection) {
this.props.onSelectionChange(state.selection, state)
this.tmp.selection = state.selection
}
}
/**
* When an event by `name` fires, pass it through the plugins, and update the
* state if one of them chooses to.
*
* @param {String} name
* @param {Mixed} ...args
*/
onEvent = (name, ...args) => {
for (const plugin of this.state.plugins) {
if (!plugin[name]) continue
const newState = plugin[name](...args, this.state.state, this)
if (!newState) continue
this.onChange(newState)
break
}
}
/**
* Render the editor.
*
* @return {Element}
*/
render = () => {
debug('render')
const handlers = {}
for (const property of EVENT_HANDLERS) {
handlers[property] = this[property]
}
return (
<Content
{...handlers}
className={this.props.className}
editor={this}
onChange={this.onChange}
readOnly={this.props.readOnly}
schema={this.state.schema}
spellCheck={this.props.spellCheck}
state={this.state.state}
style={this.props.style}
/>
)
}
/**
* Resolve the editor's current plugins from `props` when they change.
*
* Add a plugin made from the editor's own `props` at the beginning of the
* stack. That way, you can add a `onKeyDown` handler to the editor itself,
* and it will override all of the existing plugins.
*
* Also add the "core" functionality plugin that handles the most basic events
* for the editor, like delete characters and such.
*
* @param {Object} props
* @return {Array}
*/
resolvePlugins = (props) => {
const { onChange, plugins, ...editorPlugin } = props
const corePlugin = CorePlugin(props)
return [
editorPlugin,
...plugins,
corePlugin
]
}
/**
* Resolve the editor's schema from a set of `plugins`.
*
* @param {Array} plugins
* @return {Schema}
*/
resolveSchema = (plugins) => {
let rules = []
for (const plugin of plugins) {
if (!plugin.schema) continue
const schema = Schema.create(plugin.schema)
rules = rules.concat(schema.rules)
}
const schema = Schema.create({ rules })
return schema
}
}
/**
* Mix in the property types for the event handlers.
*/
for (const property of EVENT_HANDLERS) {
Editor.propTypes[property] = React.PropTypes.func
}
/**
* Export.
*/
export default Editor