import OffsetKey from '../utils/offset-key' import React from 'react' import ReactDOM from 'react-dom' import Text from './text' import keycode from 'keycode' import { Raw } from '..' import { isCommand, isWindowsCommand } from '../utils/event' /** * Content. */ class Content extends React.Component { /** * Props. */ static propTypes = { onBeforeInput: React.PropTypes.func, onChange: React.PropTypes.func, onKeyDown: React.PropTypes.func, onPaste: React.PropTypes.func, onSelect: React.PropTypes.func, renderMark: React.PropTypes.func.isRequired, renderNode: React.PropTypes.func.isRequired, state: React.PropTypes.object.isRequired, }; /** * Constructor. * * @param {Object} props */ constructor(props) { super(props) this.tmp = {} } /** * Should the component update? * * @param {Object} props * @param {Object} state * @return {Boolean} shouldUpdate */ shouldComponentUpdate(props, state) { if (props.state == this.props.state) return false if (props.state.document == this.props.state.document) return false if (props.state.isNative) return false return true } /** * On before input, bubble up. * * @param {Event} e */ onBeforeInput(e) { this.props.onBeforeInput(e) } /** * On change, bubble up. * * @param {State} state */ onChange(state) { this.props.onChange(state) } /** * On copy, defer to `onCutCopy`, then bubble up. * * @param {Event} e */ onCopy(e) { this.onCutCopy(e) } /** * On cut, defer to `onCutCopy`, then bubble up. * * @param {Event} e */ onCut(e) { this.onCutCopy(e) // Once the cut has successfully executed, delete the current selection. window.requestAnimationFrame(() => { const state = this.props.state.transform().delete().apply() this.onChange(state) }) } /** * On cut and copy, add the currently selected fragment to the currently * selected DOM, so that it will show up when pasted. * * @param {Event} e */ onCutCopy(e) { const native = window.getSelection() if (!native.rangeCount) return const { state } = this.props const { fragment } = state const raw = Raw.serializeNode(fragment) const string = JSON.stringify(raw) const encoded = window.btoa(string) // Wrap the first character of the selection in a span that has the encoded // fragment attached as an attribute, so it will show up in the copied HTML. const range = native.getRangeAt(0) const contents = range.cloneContents() const wrapper = window.document.createElement('span') const text = contents.childNodes[0] const char = text.textContent.slice(0, 1) const first = window.document.createTextNode(char) const rest = text.textContent.slice(1) text.textContent = rest wrapper.appendChild(first) wrapper.setAttribute('data-fragment', encoded) contents.insertBefore(wrapper, text) // Add the phony content to the DOM, and select it, so it will be copied. const body = window.document.querySelector('body') const div = window.document.createElement('div') div.setAttribute('contenteditable', true) div.style.position = 'absolute' div.style.left = '-9999px' div.appendChild(contents) body.appendChild(div) // Set the `isCopying` flag, so our `onSelect` logic doesn't fire. this.tmp.isCopying = true native.selectAllChildren(div) // Revert to the previous selection right after copying. window.requestAnimationFrame(() => { body.removeChild(div) native.removeAllRanges() native.addRange(range) this.tmp.isCopying = false }) } /** * On key down, prevent the default behavior of certain commands that will * leave the editor in an out-of-sync state, then bubble up. * * @param {Event} e */ onKeyDown(e) { const key = keycode(e.which) if ( (key == 'enter') || (key == 'backspace') || (key == 'delete') || (key == 'b' && isCommand(e)) || (key == 'i' && isCommand(e)) || (key == 'y' && isWindowsCommand(e)) || (key == 'z' && isCommand(e)) ) { e.preventDefault() } this.props.onKeyDown(e) } /** * On paste, determine the type and bubble up. * * @param {Event} e */ onPaste(e) { e.preventDefault() const data = e.clipboardData const { types } = data const paste = {} // Handle files. if (data.files.length != 0) { paste.type = 'files' paste.files = data.files } // Treat it as rich text if there is HTML content. else if (types.includes('text/html')) { paste.type = 'html' paste.text = data.getData('text/plain') paste.html = data.getData('text/html') } // Treat everything else as plain text. else { paste.type = 'text' paste.text = data.getData('text/plain') } // If html, and the html includes a `data-fragment` attribute, it's actually // a raw-serialized JSON fragment from a previous cut/copy, so deserialize // it and insert it normally. if (paste.type == 'html' && ~paste.html.indexOf(' this.renderNode(node)) .toArray() const style = { outline: 'none', // prevent the default outline styles whiteSpace: 'pre-wrap', // preserve adjacent whitespace and new lines wordWrap: 'break-word' // allow words to break if they are too long } return (
this.onBeforeInput(e)} onCopy={e => this.onCopy(e)} onCut={e => this.onCut(e)} onKeyDown={e => this.onKeyDown(e)} onPaste={e => this.onPaste(e)} onSelect={e => this.onSelect(e)} > {children}
) } /** * Render a `node`. * * @param {Node} node * @return {Component} component */ renderNode(node) { switch (node.kind) { case 'text': return this.renderText(node) case 'block': case 'inline': return this.renderElement(node) } } /** * Render a text `node`. * * @param {Node} node * @return {Component} component */ renderText(node) { const { renderMark, renderNode, state } = this.props return ( ) } /** * Render an element `node`. * * @param {Node} node * @return {Component} component */ renderElement(node) { const { renderNode, state } = this.props const Component = renderNode(node) const children = node.nodes .map(child => this.renderNode(child)) .toArray() return ( {children} ) } } /** * Export. */ export default Content