mirror of
https://github.com/ianstormtaylor/slate.git
synced 2025-08-10 17:24:02 +02:00
handle copy pasting from other editor instances
This commit is contained in:
@@ -4,6 +4,7 @@ import React from 'react'
|
|||||||
import ReactDOM from 'react-dom'
|
import ReactDOM from 'react-dom'
|
||||||
import Text from './text'
|
import Text from './text'
|
||||||
import keycode from 'keycode'
|
import keycode from 'keycode'
|
||||||
|
import { Raw } from '..'
|
||||||
import { isCommand, isWindowsCommand } from '../utils/event'
|
import { isCommand, isWindowsCommand } from '../utils/event'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -19,8 +20,6 @@ class Content extends React.Component {
|
|||||||
static propTypes = {
|
static propTypes = {
|
||||||
onBeforeInput: React.PropTypes.func,
|
onBeforeInput: React.PropTypes.func,
|
||||||
onChange: React.PropTypes.func,
|
onChange: React.PropTypes.func,
|
||||||
onCopy: React.PropTypes.func,
|
|
||||||
onCut: React.PropTypes.func,
|
|
||||||
onKeyDown: React.PropTypes.func,
|
onKeyDown: React.PropTypes.func,
|
||||||
onPaste: React.PropTypes.func,
|
onPaste: React.PropTypes.func,
|
||||||
onSelect: React.PropTypes.func,
|
onSelect: React.PropTypes.func,
|
||||||
@@ -29,6 +28,17 @@ class Content extends React.Component {
|
|||||||
state: React.PropTypes.object.isRequired,
|
state: React.PropTypes.object.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructor.
|
||||||
|
*
|
||||||
|
* @param {Object} props
|
||||||
|
*/
|
||||||
|
|
||||||
|
constructor(props) {
|
||||||
|
super(props)
|
||||||
|
this.tmp = {}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Should the component update?
|
* Should the component update?
|
||||||
*
|
*
|
||||||
@@ -44,6 +54,16 @@ class Content extends React.Component {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* On before input, bubble up.
|
||||||
|
*
|
||||||
|
* @param {Event} e
|
||||||
|
*/
|
||||||
|
|
||||||
|
onBeforeInput(e) {
|
||||||
|
this.props.onBeforeInput(e)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* On change, bubble up.
|
* 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
|
* @param {Event} e
|
||||||
*/
|
*/
|
||||||
|
|
||||||
onEvent(name, e) {
|
onCopy(e) {
|
||||||
this.props[name](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.
|
// 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.type = 'html'
|
||||||
paste.text = data.getData('text/plain')
|
paste.text = data.getData('text/plain')
|
||||||
paste.html = data.getData('text/html')
|
paste.html = data.getData('text/html')
|
||||||
@@ -121,6 +209,27 @@ class Content extends React.Component {
|
|||||||
paste.text = data.getData('text/plain')
|
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
|
paste.data = data
|
||||||
this.props.onPaste(e, paste)
|
this.props.onPaste(e, paste)
|
||||||
}
|
}
|
||||||
@@ -132,6 +241,8 @@ class Content extends React.Component {
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
onSelect(e) {
|
onSelect(e) {
|
||||||
|
if (this.tmp.isCopying) return
|
||||||
|
|
||||||
let { state } = this.props
|
let { state } = this.props
|
||||||
let { document, selection } = state
|
let { document, selection } = state
|
||||||
const native = window.getSelection()
|
const native = window.getSelection()
|
||||||
@@ -184,12 +295,12 @@ class Content extends React.Component {
|
|||||||
<div
|
<div
|
||||||
contentEditable suppressContentEditableWarning
|
contentEditable suppressContentEditableWarning
|
||||||
style={style}
|
style={style}
|
||||||
|
onBeforeInput={e => this.onBeforeInput(e)}
|
||||||
|
onCopy={e => this.onCopy(e)}
|
||||||
|
onCut={e => this.onCut(e)}
|
||||||
onKeyDown={e => this.onKeyDown(e)}
|
onKeyDown={e => this.onKeyDown(e)}
|
||||||
onSelect={e => this.onSelect(e)}
|
|
||||||
onPaste={e => this.onPaste(e)}
|
onPaste={e => this.onPaste(e)}
|
||||||
onCopy={e => this.onEvent('onCopy', e)}
|
onSelect={e => this.onSelect(e)}
|
||||||
onCut={e => this.onEvent('onCut', e)}
|
|
||||||
onBeforeInput={e => this.onEvent('onBeforeInput', e)}
|
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
|
@@ -106,8 +106,6 @@ class Editor extends React.Component {
|
|||||||
onChange={state => this.onChange(state)}
|
onChange={state => this.onChange(state)}
|
||||||
renderMark={mark => this.renderMark(mark)}
|
renderMark={mark => this.renderMark(mark)}
|
||||||
renderNode={node => this.renderNode(node)}
|
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)}
|
onPaste={(e, paste) => this.onEvent('onPaste', e, paste)}
|
||||||
onBeforeInput={e => this.onEvent('onBeforeInput', e)}
|
onBeforeInput={e => this.onEvent('onBeforeInput', e)}
|
||||||
onKeyDown={e => this.onEvent('onKeyDown', e)}
|
onKeyDown={e => this.onEvent('onKeyDown', e)}
|
||||||
|
@@ -45,19 +45,6 @@ export default {
|
|||||||
.apply({ isNative: true })
|
.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.
|
* The core `onKeyDown` handler.
|
||||||
*
|
*
|
||||||
@@ -135,23 +122,6 @@ export default {
|
|||||||
onPaste(e, paste, state, editor) {
|
onPaste(e, paste, state, editor) {
|
||||||
if (paste.type == 'files') return
|
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()
|
let transform = state.transform()
|
||||||
|
|
||||||
paste.text
|
paste.text
|
||||||
|
Reference in New Issue
Block a user