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:
@@ -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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@@ -21,6 +21,8 @@ function noop() {}
|
|||||||
const EVENT_HANDLERS = [
|
const EVENT_HANDLERS = [
|
||||||
'onBeforeInput',
|
'onBeforeInput',
|
||||||
'onBlur',
|
'onBlur',
|
||||||
|
'onCopy',
|
||||||
|
'onCut',
|
||||||
'onDrop',
|
'onDrop',
|
||||||
'onKeyDown',
|
'onKeyDown',
|
||||||
'onPaste',
|
'onPaste',
|
||||||
|
@@ -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,
|
||||||
|
Reference in New Issue
Block a user