mirror of
https://github.com/ianstormtaylor/slate.git
synced 2025-08-22 15:02:51 +02:00
Add a copyFragment
helper for plugin onCut/onCopy functions (#1429)
* Add a `copyFragment` helper for plugin onCut/onCopy functions * Rename `copyFragment` to `cloneFragment` * Fix a missing clone-fragment reference
This commit is contained in:
committed by
Ian Storm Taylor
parent
b2e0612149
commit
082fb53633
@@ -55,6 +55,7 @@
|
|||||||
- [Plugins](./reference/slate-react/plugins.md)
|
- [Plugins](./reference/slate-react/plugins.md)
|
||||||
- [Custom Nodes](./reference/slate-react/custom-nodes.md)
|
- [Custom Nodes](./reference/slate-react/custom-nodes.md)
|
||||||
- [Core Plugins](./reference/slate-react/core-plugins.md)
|
- [Core Plugins](./reference/slate-react/core-plugins.md)
|
||||||
|
- [cloneFragment](./reference/slate-react/utils.md)
|
||||||
- [findDOMNode](./reference/slate-react/utils.md)
|
- [findDOMNode](./reference/slate-react/utils.md)
|
||||||
- [findDOMRange](./reference/slate-react/utils.md)
|
- [findDOMRange](./reference/slate-react/utils.md)
|
||||||
- [findNode](./reference/slate-react/utils.md)
|
- [findNode](./reference/slate-react/utils.md)
|
||||||
|
@@ -3,6 +3,7 @@
|
|||||||
|
|
||||||
```js
|
```js
|
||||||
import {
|
import {
|
||||||
|
cloneFragment,
|
||||||
findDOMNode,
|
findDOMNode,
|
||||||
findDOMRange,
|
findDOMRange,
|
||||||
findNode,
|
findNode,
|
||||||
@@ -18,6 +19,40 @@ React-specific utility functions for Slate that may be useful in certain use cas
|
|||||||
|
|
||||||
## Functions
|
## Functions
|
||||||
|
|
||||||
|
### `cloneFragment`
|
||||||
|
`cloneFragment(event: DOMEvent|ReactEvent, value: Value, fragment: Document)`
|
||||||
|
|
||||||
|
During a cut or copy event, sets `fragment` as the Slate document fragment to be copied.
|
||||||
|
|
||||||
|
```js
|
||||||
|
function onCopy(event, change, editor) {
|
||||||
|
const { value } = change
|
||||||
|
const fragment = // ... create a fragment from a set of nodes ...
|
||||||
|
|
||||||
|
if (fragment) {
|
||||||
|
cloneFragment(event, value, fragment)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Note that calling `cloneFragment` should be the last thing you do in your event handler. If you change the window selection after calling `cloneFragment`, the browser may copy the wrong content. If you need to perform an action after calling `cloneFragment`, wrap it in `requestAnimationFrame`:
|
||||||
|
|
||||||
|
```js
|
||||||
|
function onCut(event, change, editor) {
|
||||||
|
const { value } = change
|
||||||
|
const fragment = // ... create a fragment from a set of nodes ...
|
||||||
|
|
||||||
|
if (fragment) {
|
||||||
|
cloneFragment(event, value, fragment)
|
||||||
|
window.requestAnimationFrame(() => {
|
||||||
|
editor.change(change => change.delete())
|
||||||
|
})
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
### `findDOMNode`
|
### `findDOMNode`
|
||||||
`findDOMNode(node: Node) => DOMElement`
|
`findDOMNode(node: Node) => DOMElement`
|
||||||
|
|
||||||
|
@@ -1,5 +1,6 @@
|
|||||||
|
|
||||||
import Editor from './components/editor'
|
import Editor from './components/editor'
|
||||||
|
import cloneFragment from './utils/clone-fragment'
|
||||||
import findDOMNode from './utils/find-dom-node'
|
import findDOMNode from './utils/find-dom-node'
|
||||||
import findDOMRange from './utils/find-dom-range'
|
import findDOMRange from './utils/find-dom-range'
|
||||||
import findNode from './utils/find-node'
|
import findNode from './utils/find-node'
|
||||||
@@ -16,6 +17,7 @@ import setEventTransfer from './utils/set-event-transfer'
|
|||||||
|
|
||||||
export {
|
export {
|
||||||
Editor,
|
Editor,
|
||||||
|
cloneFragment,
|
||||||
findDOMNode,
|
findDOMNode,
|
||||||
findDOMRange,
|
findDOMRange,
|
||||||
findNode,
|
findNode,
|
||||||
@@ -27,6 +29,7 @@ export {
|
|||||||
|
|
||||||
export default {
|
export default {
|
||||||
Editor,
|
Editor,
|
||||||
|
cloneFragment,
|
||||||
findDOMNode,
|
findDOMNode,
|
||||||
findDOMRange,
|
findDOMRange,
|
||||||
findNode,
|
findNode,
|
||||||
|
@@ -9,6 +9,7 @@ import { Block, Inline, Text } from 'slate'
|
|||||||
import EVENT_HANDLERS from '../constants/event-handlers'
|
import EVENT_HANDLERS from '../constants/event-handlers'
|
||||||
import HOTKEYS from '../constants/hotkeys'
|
import HOTKEYS from '../constants/hotkeys'
|
||||||
import Content from '../components/content'
|
import Content from '../components/content'
|
||||||
|
import cloneFragment from '../utils/clone-fragment'
|
||||||
import findDOMNode from '../utils/find-dom-node'
|
import findDOMNode from '../utils/find-dom-node'
|
||||||
import findNode from '../utils/find-node'
|
import findNode from '../utils/find-node'
|
||||||
import findPoint from '../utils/find-point'
|
import findPoint from '../utils/find-point'
|
||||||
@@ -16,7 +17,6 @@ import findRange from '../utils/find-range'
|
|||||||
import getEventRange from '../utils/get-event-range'
|
import getEventRange from '../utils/get-event-range'
|
||||||
import getEventTransfer from '../utils/get-event-transfer'
|
import getEventTransfer from '../utils/get-event-transfer'
|
||||||
import setEventTransfer from '../utils/set-event-transfer'
|
import setEventTransfer from '../utils/set-event-transfer'
|
||||||
import { IS_CHROME, IS_SAFARI } from '../constants/environment'
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Debug.
|
* Debug.
|
||||||
@@ -102,7 +102,7 @@ function AfterPlugin() {
|
|||||||
function onCopy(event, change, editor) {
|
function onCopy(event, change, editor) {
|
||||||
debug('onCopy', { event })
|
debug('onCopy', { event })
|
||||||
|
|
||||||
onCutOrCopy(event, change)
|
cloneFragment(event, change.value)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -116,7 +116,7 @@ function AfterPlugin() {
|
|||||||
function onCut(event, change, editor) {
|
function onCut(event, change, editor) {
|
||||||
debug('onCut', { event })
|
debug('onCut', { event })
|
||||||
|
|
||||||
onCutOrCopy(event, change)
|
cloneFragment(event, change.value)
|
||||||
const window = getWindow(event.target)
|
const window = getWindow(event.target)
|
||||||
|
|
||||||
// Once the fake cut content has successfully been added to the clipboard,
|
// Once the fake cut content has successfully been added to the clipboard,
|
||||||
@@ -139,127 +139,6 @@ function AfterPlugin() {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* On cut or copy.
|
|
||||||
*
|
|
||||||
* @param {Event} event
|
|
||||||
* @param {Change} change
|
|
||||||
* @param {Editor} editor
|
|
||||||
*/
|
|
||||||
|
|
||||||
function onCutOrCopy(event, change, editor) {
|
|
||||||
const window = getWindow(event.target)
|
|
||||||
const native = window.getSelection()
|
|
||||||
const { value } = change
|
|
||||||
const { startKey, endKey, startText, endBlock, endInline } = value
|
|
||||||
const isVoidBlock = endBlock && endBlock.isVoid
|
|
||||||
const isVoidInline = endInline && endInline.isVoid
|
|
||||||
const isVoid = isVoidBlock || isVoidInline
|
|
||||||
|
|
||||||
// If the selection is collapsed, and it isn't inside a void node, abort.
|
|
||||||
if (native.isCollapsed && !isVoid) return
|
|
||||||
|
|
||||||
// Create a fake selection so that we can add a Base64-encoded copy of the
|
|
||||||
// fragment to the HTML, to decode on future pastes.
|
|
||||||
const { fragment } = value
|
|
||||||
const encoded = Base64.serializeNode(fragment)
|
|
||||||
const range = native.getRangeAt(0)
|
|
||||||
let contents = range.cloneContents()
|
|
||||||
let attach = contents.childNodes[0]
|
|
||||||
|
|
||||||
// If the end node is a void node, we need to move the end of the range from
|
|
||||||
// the void node's spacer span, to the end of the void node's content.
|
|
||||||
if (isVoid) {
|
|
||||||
const r = range.cloneRange()
|
|
||||||
const n = isVoidBlock ? endBlock : endInline
|
|
||||||
const node = findDOMNode(n, window)
|
|
||||||
r.setEndAfter(node)
|
|
||||||
contents = r.cloneContents()
|
|
||||||
attach = contents.childNodes[contents.childNodes.length - 1].firstChild
|
|
||||||
}
|
|
||||||
|
|
||||||
// COMPAT: in Safari and Chrome when selecting a single marked word,
|
|
||||||
// marks are not preserved when copying.
|
|
||||||
// If the attatched is not void, and the startKey and endKey is the same,
|
|
||||||
// check if there is marks involved. If so, set the range start just before the
|
|
||||||
// startText node
|
|
||||||
if ((IS_CHROME || IS_SAFARI) && !isVoid && startKey === endKey) {
|
|
||||||
const hasMarks = startText.characters
|
|
||||||
.slice(value.selection.anchorOffset, value.selection.focusOffset)
|
|
||||||
.filter(char => char.marks.size !== 0)
|
|
||||||
.size !== 0
|
|
||||||
if (hasMarks) {
|
|
||||||
const r = range.cloneRange()
|
|
||||||
const node = findDOMNode(startText, window)
|
|
||||||
r.setStartBefore(node)
|
|
||||||
contents = r.cloneContents()
|
|
||||||
attach = contents.childNodes[contents.childNodes.length - 1].firstChild
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove any zero-width space spans from the cloned DOM so that they don't
|
|
||||||
// show up elsewhere when pasted.
|
|
||||||
const zws = [].slice.call(contents.querySelectorAll('[data-slate-zero-width]'))
|
|
||||||
zws.forEach(zw => zw.parentNode.removeChild(zw))
|
|
||||||
|
|
||||||
// COMPAT: In Chrome and Safari, if the last element in the selection to
|
|
||||||
// copy has `contenteditable="false"` the copy will fail, and nothing will
|
|
||||||
// be put in the clipboard. So we remove them all. (2017/05/04)
|
|
||||||
if (IS_CHROME || IS_SAFARI) {
|
|
||||||
const els = [].slice.call(contents.querySelectorAll('[contenteditable="false"]'))
|
|
||||||
els.forEach(el => el.removeAttribute('contenteditable'))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set a `data-slate-fragment` attribute on a non-empty node, so it shows up
|
|
||||||
// in the HTML, and can be used for intra-Slate pasting. If it's a text
|
|
||||||
// node, wrap it in a `<span>` so we have something to set an attribute on.
|
|
||||||
if (attach.nodeType == 3) {
|
|
||||||
const span = window.document.createElement('span')
|
|
||||||
|
|
||||||
// COMPAT: In Chrome and Safari, if we don't add the `white-space` style
|
|
||||||
// then leading and trailing spaces will be ignored. (2017/09/21)
|
|
||||||
span.style.whiteSpace = 'pre'
|
|
||||||
|
|
||||||
span.appendChild(attach)
|
|
||||||
contents.appendChild(span)
|
|
||||||
attach = span
|
|
||||||
}
|
|
||||||
|
|
||||||
attach.setAttribute('data-slate-fragment', encoded)
|
|
||||||
|
|
||||||
// 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'
|
|
||||||
|
|
||||||
// COMPAT: In Firefox, the viewport jumps to find the phony div, so it
|
|
||||||
// should be created at the current scroll offset with `style.top`.
|
|
||||||
// The box model attributes which can interact with 'top' are also reset.
|
|
||||||
div.style.border = '0px'
|
|
||||||
div.style.padding = '0px'
|
|
||||||
div.style.margin = '0px'
|
|
||||||
div.style.top = `${window.pageYOffset || window.document.documentElement.scrollTop}px`
|
|
||||||
|
|
||||||
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 drag end.
|
* On drag end.
|
||||||
*
|
*
|
||||||
|
127
packages/slate-react/src/utils/clone-fragment.js
Normal file
127
packages/slate-react/src/utils/clone-fragment.js
Normal file
@@ -0,0 +1,127 @@
|
|||||||
|
|
||||||
|
import Base64 from 'slate-base64-serializer'
|
||||||
|
|
||||||
|
import findDOMNode from './find-dom-node'
|
||||||
|
import getWindow from 'get-window'
|
||||||
|
import { IS_CHROME, IS_SAFARI } from '../constants/environment'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prepares a Slate document fragment to be copied to the clipboard.
|
||||||
|
*
|
||||||
|
* @param {Event} event
|
||||||
|
* @param {Value} value
|
||||||
|
* @param {Document} [fragment]
|
||||||
|
*/
|
||||||
|
|
||||||
|
function cloneFragment(event, value, fragment = value.fragment) {
|
||||||
|
const window = getWindow(event.target)
|
||||||
|
const native = window.getSelection()
|
||||||
|
const { startKey, endKey, startText, endBlock, endInline } = value
|
||||||
|
const isVoidBlock = endBlock && endBlock.isVoid
|
||||||
|
const isVoidInline = endInline && endInline.isVoid
|
||||||
|
const isVoid = isVoidBlock || isVoidInline
|
||||||
|
|
||||||
|
// If the selection is collapsed, and it isn't inside a void node, abort.
|
||||||
|
if (native.isCollapsed && !isVoid) return
|
||||||
|
|
||||||
|
// Create a fake selection so that we can add a Base64-encoded copy of the
|
||||||
|
// fragment to the HTML, to decode on future pastes.
|
||||||
|
const encoded = Base64.serializeNode(fragment)
|
||||||
|
const range = native.getRangeAt(0)
|
||||||
|
let contents = range.cloneContents()
|
||||||
|
let attach = contents.childNodes[0]
|
||||||
|
|
||||||
|
// If the end node is a void node, we need to move the end of the range from
|
||||||
|
// the void node's spacer span, to the end of the void node's content.
|
||||||
|
if (isVoid) {
|
||||||
|
const r = range.cloneRange()
|
||||||
|
const n = isVoidBlock ? endBlock : endInline
|
||||||
|
const node = findDOMNode(n, window)
|
||||||
|
r.setEndAfter(node)
|
||||||
|
contents = r.cloneContents()
|
||||||
|
attach = contents.childNodes[contents.childNodes.length - 1].firstChild
|
||||||
|
}
|
||||||
|
|
||||||
|
// COMPAT: in Safari and Chrome when selecting a single marked word,
|
||||||
|
// marks are not preserved when copying.
|
||||||
|
// If the attatched is not void, and the startKey and endKey is the same,
|
||||||
|
// check if there is marks involved. If so, set the range start just before the
|
||||||
|
// startText node
|
||||||
|
if ((IS_CHROME || IS_SAFARI) && !isVoid && startKey === endKey) {
|
||||||
|
const hasMarks = startText.characters
|
||||||
|
.slice(value.selection.anchorOffset, value.selection.focusOffset)
|
||||||
|
.filter(char => char.marks.size !== 0)
|
||||||
|
.size !== 0
|
||||||
|
if (hasMarks) {
|
||||||
|
const r = range.cloneRange()
|
||||||
|
const node = findDOMNode(startText, window)
|
||||||
|
r.setStartBefore(node)
|
||||||
|
contents = r.cloneContents()
|
||||||
|
attach = contents.childNodes[contents.childNodes.length - 1].firstChild
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove any zero-width space spans from the cloned DOM so that they don't
|
||||||
|
// show up elsewhere when pasted.
|
||||||
|
const zws = [].slice.call(contents.querySelectorAll('[data-slate-zero-width]'))
|
||||||
|
zws.forEach(zw => zw.parentNode.removeChild(zw))
|
||||||
|
|
||||||
|
// COMPAT: In Chrome and Safari, if the last element in the selection to
|
||||||
|
// copy has `contenteditable="false"` the copy will fail, and nothing will
|
||||||
|
// be put in the clipboard. So we remove them all. (2017/05/04)
|
||||||
|
if (IS_CHROME || IS_SAFARI) {
|
||||||
|
const els = [].slice.call(contents.querySelectorAll('[contenteditable="false"]'))
|
||||||
|
els.forEach(el => el.removeAttribute('contenteditable'))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set a `data-slate-fragment` attribute on a non-empty node, so it shows up
|
||||||
|
// in the HTML, and can be used for intra-Slate pasting. If it's a text
|
||||||
|
// node, wrap it in a `<span>` so we have something to set an attribute on.
|
||||||
|
if (attach.nodeType == 3) {
|
||||||
|
const span = window.document.createElement('span')
|
||||||
|
|
||||||
|
// COMPAT: In Chrome and Safari, if we don't add the `white-space` style
|
||||||
|
// then leading and trailing spaces will be ignored. (2017/09/21)
|
||||||
|
span.style.whiteSpace = 'pre'
|
||||||
|
|
||||||
|
span.appendChild(attach)
|
||||||
|
contents.appendChild(span)
|
||||||
|
attach = span
|
||||||
|
}
|
||||||
|
|
||||||
|
attach.setAttribute('data-slate-fragment', encoded)
|
||||||
|
|
||||||
|
// 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'
|
||||||
|
|
||||||
|
// COMPAT: In Firefox, the viewport jumps to find the phony div, so it
|
||||||
|
// should be created at the current scroll offset with `style.top`.
|
||||||
|
// The box model attributes which can interact with 'top' are also reset.
|
||||||
|
div.style.border = '0px'
|
||||||
|
div.style.padding = '0px'
|
||||||
|
div.style.margin = '0px'
|
||||||
|
div.style.top = `${window.pageYOffset || window.document.documentElement.scrollTop}px`
|
||||||
|
|
||||||
|
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)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export default cloneFragment
|
Reference in New Issue
Block a user