1
0
mirror of https://github.com/ianstormtaylor/slate.git synced 2025-09-02 19:52:32 +02:00

move onCopy and onCut logic to core plugin, refactor onDrop

This commit is contained in:
Ian Storm Taylor
2016-07-27 13:33:36 -07:00
parent 6503ee2d80
commit fba3fe7a13
3 changed files with 138 additions and 83 deletions

View File

@@ -222,7 +222,16 @@ class Content extends React.Component {
*/ */
onCopy = (e) => { onCopy = (e) => {
this.onCutCopy(e) this.tmp.isCopying = true
window.requestAnimationFrame(() => {
this.tmp.isCopying = false
})
const { state } = this.props
const data = {}
data.type = 'fragment'
data.fragment = state.fragment
this.props.onCopy(e, data)
} }
/** /**
@@ -233,69 +242,17 @@ class Content extends React.Component {
onCut = (e) => { onCut = (e) => {
if (this.props.readOnly) return if (this.props.readOnly) return
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 encoded = Base64.serializeNode(fragment)
// 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 this.tmp.isCopying = true
const r = window.document.createRange()
// COMPAT: In Firefox, trying to use the terser `native.selectAllChildren`
// throws an error, so we use the older `range` equivalent. (2016/06/21)
r.selectNodeContents(div)
native.removeAllRanges()
native.addRange(r)
// Revert to the previous selection right after copying.
window.requestAnimationFrame(() => { window.requestAnimationFrame(() => {
body.removeChild(div)
native.removeAllRanges()
native.addRange(range)
this.tmp.isCopying = false this.tmp.isCopying = false
}) })
const { state } = this.props
const data = {}
data.type = 'fragment'
data.fragment = state.fragment
this.props.onCut(e, data)
} }
/** /**
@@ -364,14 +321,13 @@ class Content extends React.Component {
const { state, renderDecorations } = this.props const { state, renderDecorations } = this.props
const { selection } = state const { selection } = state
const data = e.nativeEvent.dataTransfer const { dataTransfer, x, y } = e.nativeEvent
const drop = {} const data = {}
// COMPAT: In Firefox, `types` is array-like. (2016/06/21) // COMPAT: In Firefox, `types` is array-like. (2016/06/21)
const types = Array.from(data.types) const types = Array.from(dataTransfer.types)
// Resolve the point where the drop occured. // Resolve the point where the drop occured.
const { x, y } = e.nativeEvent
let range let range
// COMPAT: In Firefox, `caretRangeFromPoint` doesn't exist. (2016/07/25) // COMPAT: In Firefox, `caretRangeFromPoint` doesn't exist. (2016/07/25)
@@ -398,45 +354,44 @@ class Content extends React.Component {
// Handle Slate fragments. // Handle Slate fragments.
if (includes(types, TYPES.FRAGMENT)) { if (includes(types, TYPES.FRAGMENT)) {
const encoded = data.getData(TYPES.FRAGMENT) const encoded = dataTransfer.getData(TYPES.FRAGMENT)
const fragment = Base64.deserializeNode(encoded) const fragment = Base64.deserializeNode(encoded)
drop.type = 'fragment' data.type = 'fragment'
drop.fragment = fragment data.fragment = fragment
drop.isInternal = this.tmp.isInternalDrag data.isInternal = this.tmp.isInternalDrag
} }
// Handle Slate nodes. // Handle Slate nodes.
else if (includes(types, TYPES.NODE)) { else if (includes(types, TYPES.NODE)) {
const encoded = data.getData(TYPES.NODE) const encoded = dataTransfer.getData(TYPES.NODE)
const node = Base64.deserializeNode(encoded) const node = Base64.deserializeNode(encoded)
drop.type = 'node' data.type = 'node'
drop.node = node data.node = node
drop.isInternal = this.tmp.isInternalDrag data.isInternal = this.tmp.isInternalDrag
} }
// Handle files. // Handle files.
else if (data.files.length) { else if (dataTransfer.files.length) {
drop.type = 'files' data.type = 'files'
drop.files = data.files data.files = dataTransfer.files
} }
// Handle HTML. // Handle HTML.
else if (includes(types, TYPES.HTML)) { else if (includes(types, TYPES.HTML)) {
drop.type = 'html' data.type = 'html'
drop.text = data.getData(TYPES.TEXT) data.text = dataTransfer.getData(TYPES.TEXT)
drop.html = data.getData(TYPES.HTML) data.html = dataTransfer.getData(TYPES.HTML)
} }
// Handle plain text. // Handle plain text.
else { else {
drop.type = 'text' data.type = 'text'
drop.text = data.getData(TYPES.TEXT) data.text = dataTransfer.getData(TYPES.TEXT)
} }
drop.data = data data.target = target
drop.target = target data.effect = dataTransfer.dropEffect
drop.effect = data.dropEffect this.props.onDrop(e, data)
this.props.onDrop(e, drop)
} }
/** /**

View File

@@ -21,6 +21,8 @@ function noop() {}
const EVENT_HANDLERS = [ const EVENT_HANDLERS = [
'onBeforeInput', 'onBeforeInput',
'onBlur', 'onBlur',
'onCopy',
'onCut',
'onDrop', 'onDrop',
'onKeyDown', 'onKeyDown',
'onPaste', 'onPaste',

View File

@@ -1,4 +1,5 @@
import Base64 from '../serializers/base-64'
import Character from '../models/character' import Character from '../models/character'
import Key from '../utils/key' import Key from '../utils/key'
import Placeholder from '../components/placeholder' import Placeholder from '../components/placeholder'
@@ -136,6 +137,100 @@ function Plugin(options = {}) {
.apply({ isNative: true }) .apply({ isNative: true })
} }
/**
* On copy.
*
* @param {Event} e
* @param {Object} data
* @param {State} state
* @return {State}
*/
function onCopy(e, data, state) {
onCutOrCopy(e, data, state)
}
/**
* On cut.
*
* @param {Event} e
* @param {Object} data
* @param {State} state
* @param {Editor} editor
* @return {State}
*/
function onCut(e, data, state, editor) {
onCutOrCopy(e, data, state)
// Once the fake cut content has successfully been added to the clipboard,
// delete the content in the current selection.
window.requestAnimationFrame(() => {
const next = editor
.getState()
.transform()
.delete()
.apply()
editor.onChange(next)
})
}
/**
* On cut or copy, create a fake selection so that we can add a Base 64
* encoded copy of the fragment to the HTML, to decode on future pastes.
*
* @param {Event} e
* @param {Object} data
* @param {State} state
* @return {State}
*/
function onCutOrCopy(e, data, state) {
const native = window.getSelection()
if (!native.rangeCount) return
const { fragment } = data
const encoded = Base64.serializeNode(fragment)
// 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)
// COMPAT: In Firefox, trying to use the terser `native.selectAllChildren`
// throws an error, so we use the older `range` equivalent. (2016/06/21)
const r = window.document.createRange()
r.selectNodeContents(div)
native.removeAllRanges()
native.addRange(r)
// Revert to the previous selection right after copying.
window.requestAnimationFrame(() => {
body.removeChild(div)
native.removeAllRanges()
native.addRange(range)
})
}
/** /**
* On drop. * On drop.
* *
@@ -440,6 +535,9 @@ function Plugin(options = {}) {
return { return {
onBeforeInput, onBeforeInput,
onBlur,
onCopy,
onCut,
onDrop, onDrop,
onKeyDown, onKeyDown,
onPaste, onPaste,