1
0
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:
Justin Weiss
2017-12-11 06:56:38 -08:00
committed by Ian Storm Taylor
parent b2e0612149
commit 082fb53633
5 changed files with 169 additions and 124 deletions

View File

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

View File

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

View File

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

View File

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

View 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