import Key from '../utils/key' import Placeholder from '../components/placeholder' import React from 'react' import String from '../utils/string' import keycode from 'keycode' import { IS_WINDOWS, IS_MAC } from '../utils/environment' /** * The default plugin. * * @param {Object} options * @property {Element} placeholder * @property {String} placeholderClassName * @property {Object} placeholderStyle * @return {Object} */ function Plugin(options = {}) { const { placeholder, placeholderClassName, placeholderStyle } = options /** * Define a default block renderer. * * @type {Component} */ class DEFAULT_BLOCK extends React.Component { static propTypes = { attributes: React.PropTypes.object.isRequired, children: React.PropTypes.any.isRequired, node: React.PropTypes.object.isRequired, state: React.PropTypes.object.isRequired }; shouldComponentUpdate = (props, state) => { return ( props.node != this.props.node || props.state.selection.hasEdgeIn(props.node) ) } render = () => { const { attributes, children } = this.props return (
{this.renderPlaceholder()} {children}
) } renderPlaceholder = () => { if (!placeholder) return null const { node, state } = this.props return ( {placeholder} ) } } /** * Define a default inline renderer. * * @type {Component} */ class DEFAULT_INLINE extends React.Component { static propTypes = { attributes: React.PropTypes.object.isRequired, children: React.PropTypes.any.isRequired, node: React.PropTypes.object.isRequired, state: React.PropTypes.object.isRequired }; shouldComponentUpdate = (props, state) => { return ( props.node != this.props.node || props.state.selection.hasEdgeIn(props.node) ) } render = () => { const { attributes, children } = this.props return {children} } } /** * Return the plugin. */ return { /** * The core `onBeforeInput` handler. * * @param {Event} e * @param {State} state * @param {Editor} editor * @return {State or Null} */ onBeforeInput(e, state, editor) { const transform = state.transform().insertText(e.data) const synthetic = transform.apply() const resolved = editor.resolveState(synthetic) // We do not have to re-render if the current selection is collapsed, the // current node is not empty, and the new state has the same decorations // as the current one. const isNative = ( state.isCollapsed && state.startText.text != '' && resolved.equals(synthetic) ) state = isNative ? transform.apply({ isNative }) : synthetic if (!isNative) e.preventDefault() return state }, /** * The core `onDrop` handler. * * @param {Event} e * @param {Object} drop * @param {State} state * @param {Editor} editor * @return {State or Null} */ onDrop(e, drop, state, editor) { switch (drop.type) { case 'fragment': { return state .transform() .moveTo(drop.target) .insertFragment(drop.fragment) .apply() } case 'text': case 'html': { let transform = state .transform() .moveTo(drop.target) drop.text .split('\n') .forEach((line, i) => { if (i > 0) transform = transform.splitBlock() transform = transform.insertText(line) }) return transform.apply() } } }, /** * The core `onKeyDown` handler. * * @param {Event} e * @param {State} state * @param {Editor} editor * @return {State or Null} */ onKeyDown(e, state, editor) { const key = keycode(e.which) let transform = state.transform() switch (key) { case 'enter': { const { startBlock } = state if (startBlock && !startBlock.isVoid) return transform.splitBlock().apply() const { document, startKey } = state const text = document.getNextText(startKey) if (!text) return return transform.collapseToStartOf(text).apply() } case 'backspace': { if (state.isExpanded) return transform.delete().apply() const { startOffset, startBlock } = state const text = startBlock.text let n if (Key.isWord(e)) { n = String.getWordOffsetBackward(text, startOffset) } else if (Key.isLine(e)) { n = startOffset } else { n = String.getCharOffsetBackward(text, startOffset) } return transform.deleteBackward(n).apply() } case 'delete': { if (state.isExpanded) return transform.delete().apply() const { startOffset, startBlock } = state const text = startBlock.text let n if (Key.isWord(e)) { n = String.getWordOffsetForward(text, startOffset) } else if (Key.isLine(e)) { n = text.length - startOffset } else { n = String.getCharOffsetForward(text, startOffset) } return transform.deleteForward(n).apply() } case 'up': { if (state.isExpanded) return const first = state.blocks.first() if (!first || !first.isVoid) return e.preventDefault() return transform.collapseToEndOfPreviousBlock().apply() } case 'down': { if (state.isExpanded) return const first = state.blocks.first() if (!first || !first.isVoid) return e.preventDefault() return transform.collapseToStartOfNextBlock().apply() } case 'left': { if (state.isExpanded) return const node = state.blocks.first() || state.inlines.first() if (!node || !node.isVoid) return e.preventDefault() return transform.collapseToEndOfPreviousText().apply() } case 'right': { if (state.isExpanded) return const node = state.blocks.first() || state.inlines.first() if (!node || !node.isVoid) return e.preventDefault() return transform.collapseToStartOfNextText().apply() } case 'y': { if (!Key.isWindowsCommand(e)) return return transform.redo() } case 'z': { if (!Key.isCommand(e)) return return IS_MAC && Key.isShift(e) ? transform.redo() : transform.undo() } } }, /** * The core `onPaste` handler, which treats everything as plain text. * * @param {Event} e * @param {Object} paste * @param {State} state * @param {Editor} editor * @return {State or Null} */ onPaste(e, paste, state, editor) { switch (paste.type) { case 'text': case 'html': { let transform = state.transform() paste.text .split('\n') .forEach((line, i) => { if (i > 0) transform = transform.splitBlock() transform = transform.insertText(line) }) return transform.apply() } } }, /** * The core `node` renderer, which uses plain `
` or `` depending on * what kind of node it is. * * @param {Node} node * @return {Component} component */ renderNode(node) { return node.kind == 'block' ? DEFAULT_BLOCK : DEFAULT_INLINE } } } /** * Export. */ export default Plugin