mirror of
https://github.com/ianstormtaylor/slate.git
synced 2025-08-22 06:53:25 +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)
|
||||
- [Custom Nodes](./reference/slate-react/custom-nodes.md)
|
||||
- [Core Plugins](./reference/slate-react/core-plugins.md)
|
||||
- [cloneFragment](./reference/slate-react/utils.md)
|
||||
- [findDOMNode](./reference/slate-react/utils.md)
|
||||
- [findDOMRange](./reference/slate-react/utils.md)
|
||||
- [findNode](./reference/slate-react/utils.md)
|
||||
|
@@ -3,6 +3,7 @@
|
||||
|
||||
```js
|
||||
import {
|
||||
cloneFragment,
|
||||
findDOMNode,
|
||||
findDOMRange,
|
||||
findNode,
|
||||
@@ -18,6 +19,40 @@ React-specific utility functions for Slate that may be useful in certain use cas
|
||||
|
||||
## 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(node: Node) => DOMElement`
|
||||
|
||||
|
@@ -1,5 +1,6 @@
|
||||
|
||||
import Editor from './components/editor'
|
||||
import cloneFragment from './utils/clone-fragment'
|
||||
import findDOMNode from './utils/find-dom-node'
|
||||
import findDOMRange from './utils/find-dom-range'
|
||||
import findNode from './utils/find-node'
|
||||
@@ -16,6 +17,7 @@ import setEventTransfer from './utils/set-event-transfer'
|
||||
|
||||
export {
|
||||
Editor,
|
||||
cloneFragment,
|
||||
findDOMNode,
|
||||
findDOMRange,
|
||||
findNode,
|
||||
@@ -27,6 +29,7 @@ export {
|
||||
|
||||
export default {
|
||||
Editor,
|
||||
cloneFragment,
|
||||
findDOMNode,
|
||||
findDOMRange,
|
||||
findNode,
|
||||
|
@@ -9,6 +9,7 @@ import { Block, Inline, Text } from 'slate'
|
||||
import EVENT_HANDLERS from '../constants/event-handlers'
|
||||
import HOTKEYS from '../constants/hotkeys'
|
||||
import Content from '../components/content'
|
||||
import cloneFragment from '../utils/clone-fragment'
|
||||
import findDOMNode from '../utils/find-dom-node'
|
||||
import findNode from '../utils/find-node'
|
||||
import findPoint from '../utils/find-point'
|
||||
@@ -16,7 +17,6 @@ import findRange from '../utils/find-range'
|
||||
import getEventRange from '../utils/get-event-range'
|
||||
import getEventTransfer from '../utils/get-event-transfer'
|
||||
import setEventTransfer from '../utils/set-event-transfer'
|
||||
import { IS_CHROME, IS_SAFARI } from '../constants/environment'
|
||||
|
||||
/**
|
||||
* Debug.
|
||||
@@ -102,7 +102,7 @@ function AfterPlugin() {
|
||||
function onCopy(event, change, editor) {
|
||||
debug('onCopy', { event })
|
||||
|
||||
onCutOrCopy(event, change)
|
||||
cloneFragment(event, change.value)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -116,7 +116,7 @@ function AfterPlugin() {
|
||||
function onCut(event, change, editor) {
|
||||
debug('onCut', { event })
|
||||
|
||||
onCutOrCopy(event, change)
|
||||
cloneFragment(event, change.value)
|
||||
const window = getWindow(event.target)
|
||||
|
||||
// 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.
|
||||
*
|
||||
|
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