diff --git a/lib/components/content.js b/lib/components/content.js index c8f89a370..ff2cfaf28 100644 --- a/lib/components/content.js +++ b/lib/components/content.js @@ -4,6 +4,7 @@ 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' /** @@ -19,8 +20,6 @@ class Content extends React.Component { static propTypes = { onBeforeInput: React.PropTypes.func, onChange: React.PropTypes.func, - onCopy: React.PropTypes.func, - onCut: React.PropTypes.func, onKeyDown: React.PropTypes.func, onPaste: React.PropTypes.func, onSelect: React.PropTypes.func, @@ -29,6 +28,17 @@ class Content extends React.Component { state: React.PropTypes.object.isRequired, }; + /** + * Constructor. + * + * @param {Object} props + */ + + constructor(props) { + super(props) + this.tmp = {} + } + /** * Should the component update? * @@ -44,6 +54,16 @@ class Content extends React.Component { return true } + /** + * On before input, bubble up. + * + * @param {Event} e + */ + + onBeforeInput(e) { + this.props.onBeforeInput(e) + } + /** * On change, bubble up. * @@ -55,14 +75,82 @@ class Content extends React.Component { } /** - * On certain events, bubble up. + * On copy, defer to `onCutCopy`, then bubble up. * - * @param {String} name * @param {Event} e */ - onEvent(name, e) { - this.props[name](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 + }) } /** @@ -109,7 +197,7 @@ class Content extends React.Component { } // Treat it as rich text if there is HTML content. - else if (types.includes('text/plain') && types.includes('text/html')) { + else if (types.includes('text/html')) { paste.type = 'html' paste.text = data.getData('text/plain') paste.html = data.getData('text/html') @@ -121,6 +209,27 @@ class Content extends React.Component { 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.onBeforeInput(e)} + onCopy={e => this.onCopy(e)} + onCut={e => this.onCut(e)} onKeyDown={e => this.onKeyDown(e)} - onSelect={e => this.onSelect(e)} onPaste={e => this.onPaste(e)} - onCopy={e => this.onEvent('onCopy', e)} - onCut={e => this.onEvent('onCut', e)} - onBeforeInput={e => this.onEvent('onBeforeInput', e)} + onSelect={e => this.onSelect(e)} > {children} diff --git a/lib/components/editor.js b/lib/components/editor.js index c6cdcc8c0..6dc0cd859 100644 --- a/lib/components/editor.js +++ b/lib/components/editor.js @@ -106,8 +106,6 @@ class Editor extends React.Component { onChange={state => this.onChange(state)} renderMark={mark => this.renderMark(mark)} renderNode={node => this.renderNode(node)} - onCopy={(e) => this.onEvent('onCopy', e)} - onCut={(e) => this.onEvent('onCut', e)} onPaste={(e, paste) => this.onEvent('onPaste', e, paste)} onBeforeInput={e => this.onEvent('onBeforeInput', e)} onKeyDown={e => this.onEvent('onKeyDown', e)} diff --git a/lib/plugins/core.js b/lib/plugins/core.js index 2b915d9e5..f3c26340b 100644 --- a/lib/plugins/core.js +++ b/lib/plugins/core.js @@ -45,19 +45,6 @@ export default { .apply({ isNative: true }) }, - /** - * The core `onCopy` handler. - * - * @param {Event} e - * @param {State} state - * @param {Editor} editor - * @return {State or Null} - */ - - onCopy(e, state, editor) { - editor.fragment = state.fragment - }, - /** * The core `onKeyDown` handler. * @@ -135,23 +122,6 @@ export default { onPaste(e, paste, state, editor) { if (paste.type == 'files') return - // If pasting html and the text matches the current fragment, use that. - if (paste.type == 'html') { - const { fragment } = editor - const text = fragment - .getBlocks() - .map(block => block.text) - .join('\n') - - if (paste.text == text) { - return state - .transform() - .insertFragment(fragment) - .apply() - } - } - - // Otherwise, just insert the plain text splitting at new lines. let transform = state.transform() paste.text