1
0
mirror of https://github.com/ianstormtaylor/slate.git synced 2025-04-21 13:51:59 +02:00

handle copy pasting from other editor instances

This commit is contained in:
Ian Storm Taylor 2016-06-27 19:15:20 -07:00
parent 0c22e6172b
commit f34e4b4a04
3 changed files with 122 additions and 43 deletions

View File

@ -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('<span data-fragment="')) {
const regexp = /data-fragment="([^\s]+)"/
const matches = regexp.exec(paste.html)
const [ full, encoded ] = matches
const string = window.atob(encoded)
const json = JSON.parse(string)
const fragment = Raw.deserialize(json)
let { state } = this.props
state = state
.transform()
.insertFragment(fragment.document)
.apply()
this.onChange(state)
return
}
paste.data = data
this.props.onPaste(e, paste)
}
@ -132,6 +241,8 @@ class Content extends React.Component {
*/
onSelect(e) {
if (this.tmp.isCopying) return
let { state } = this.props
let { document, selection } = state
const native = window.getSelection()
@ -184,12 +295,12 @@ class Content extends React.Component {
<div
contentEditable suppressContentEditableWarning
style={style}
onBeforeInput={e => 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}
</div>

View File

@ -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)}

View File

@ -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