so that we can get its inner HTML.
- const div = document.createElement('div')
- div.appendChild(contents)
- div.setAttribute('hidden', 'true')
- document.body.appendChild(div)
- dataTransfer.setData('text/html', div.innerHTML)
- dataTransfer.setData('text/plain', getPlainText(div))
- document.body.removeChild(div)
-}
-
-/**
- * Get a plaintext representation of the content of a node, accounting for block
- * elements which get a newline appended.
- *
- * The domNode must be attached to the DOM.
- */
-
-const getPlainText = (domNode: DOMNode) => {
- let text = ''
-
- if (isDOMText(domNode) && domNode.nodeValue) {
- return domNode.nodeValue
- }
-
- if (isDOMElement(domNode)) {
- for (const childNode of Array.from(domNode.childNodes)) {
- text += getPlainText(childNode)
- }
-
- const display = getComputedStyle(domNode).getPropertyValue('display')
-
- if (display === 'block' || display === 'list' || domNode.tagName === 'BR') {
- text += '\n'
- }
- }
-
- return text
-}
diff --git a/packages/slate-react/src/plugin/react-editor.ts b/packages/slate-react/src/plugin/react-editor.ts
index f0e47cb7b..250839857 100644
--- a/packages/slate-react/src/plugin/react-editor.ts
+++ b/packages/slate-react/src/plugin/react-editor.ts
@@ -1,4 +1,4 @@
-import { Editor, Node, Path, Point, Range, Transforms } from 'slate'
+import { Editor, Node, Path, Point, Range, Transforms, Descendant } from 'slate'
import { Key } from '../utils/key'
import {
@@ -28,6 +28,7 @@ import {
export interface ReactEditor extends Editor {
insertData: (data: DataTransfer) => void
+ setFragmentData: (data: DataTransfer) => void
}
export const ReactEditor = {
@@ -188,6 +189,14 @@ export const ReactEditor = {
editor.insertData(data)
},
+ /**
+ * Sets data from the currently selected fragment on a `DataTransfer`.
+ */
+
+ setFragmentData(editor: ReactEditor, data: DataTransfer): void {
+ editor.setFragmentData(data)
+ },
+
/**
* Find the native DOM element from a Slate node.
*/
diff --git a/packages/slate-react/src/plugin/with-react.ts b/packages/slate-react/src/plugin/with-react.ts
index d42cf4b77..6b9204077 100644
--- a/packages/slate-react/src/plugin/with-react.ts
+++ b/packages/slate-react/src/plugin/with-react.ts
@@ -1,9 +1,10 @@
import ReactDOM from 'react-dom'
-import { Editor, Node, Path, Operation, Transforms } from 'slate'
+import { Editor, Node, Path, Operation, Transforms, Range } from 'slate'
import { ReactEditor } from './react-editor'
import { Key } from '../utils/key'
import { EDITOR_TO_ON_CHANGE, NODE_TO_KEY } from '../utils/weak-maps'
+import { isDOMText, getPlainText } from '../utils/dom'
/**
* `withReact` adds React and DOM specific behaviors to the editor.
@@ -56,6 +57,91 @@ export const withReact =
(editor: T) => {
}
}
+ e.setFragmentData = (data: DataTransfer) => {
+ const { selection } = e
+
+ if (!selection) {
+ return
+ }
+
+ const [start, end] = Range.edges(selection)
+ const startVoid = Editor.void(e, { at: start.path })
+ const endVoid = Editor.void(e, { at: end.path })
+
+ if (Range.isCollapsed(selection) && !startVoid) {
+ 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 domRange = ReactEditor.toDOMRange(e, selection)
+ let contents = domRange.cloneContents()
+ let attach = contents.childNodes[0] as HTMLElement
+
+ // Make sure attach is non-empty, since empty nodes will not get copied.
+ contents.childNodes.forEach(node => {
+ if (node.textContent && node.textContent.trim() !== '') {
+ attach = node as HTMLElement
+ }
+ })
+
+ // COMPAT: 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, since the spacer is before void's content in the DOM.
+ if (endVoid) {
+ const [voidNode] = endVoid
+ const r = domRange.cloneRange()
+ const domNode = ReactEditor.toDOMNode(e, voidNode)
+ r.setEndAfter(domNode)
+ contents = r.cloneContents()
+ }
+
+ // COMPAT: If the start node is a void node, we need to attach the encoded
+ // fragment to the void node's content node instead of the spacer, because
+ // attaching it to empty `/
` nodes will end up having it erased by
+ // most browsers. (2018/04/27)
+ if (startVoid) {
+ attach = contents.querySelector('[data-slate-spacer]')! as HTMLElement
+ }
+
+ // Remove any zero-width space spans from the cloned DOM so that they don't
+ // show up elsewhere when pasted.
+ Array.from(contents.querySelectorAll('[data-slate-zero-width]')).forEach(
+ zw => {
+ const isNewline = zw.getAttribute('data-slate-zero-width') === 'n'
+ zw.textContent = isNewline ? '\n' : ''
+ }
+ )
+
+ // 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 `` so we have something to set an attribute on.
+ if (isDOMText(attach)) {
+ const span = 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
+ }
+
+ const fragment = e.getFragment()
+ const string = JSON.stringify(fragment)
+ const encoded = window.btoa(encodeURIComponent(string))
+ attach.setAttribute('data-slate-fragment', encoded)
+ data.setData('application/x-slate-fragment', encoded)
+
+ // Add the content to a so that we can get its inner HTML.
+ const div = document.createElement('div')
+ div.appendChild(contents)
+ div.setAttribute('hidden', 'true')
+ document.body.appendChild(div)
+ data.setData('text/html', div.innerHTML)
+ data.setData('text/plain', getPlainText(div))
+ document.body.removeChild(div)
+ }
+
e.insertData = (data: DataTransfer) => {
const fragment = data.getData('application/x-slate-fragment')
diff --git a/packages/slate-react/src/utils/dom.ts b/packages/slate-react/src/utils/dom.ts
index e4b0490e4..0736bd4d1 100644
--- a/packages/slate-react/src/utils/dom.ts
+++ b/packages/slate-react/src/utils/dom.ts
@@ -145,3 +145,32 @@ export const getEditableChild = (
return child
}
+
+/**
+ * Get a plaintext representation of the content of a node, accounting for block
+ * elements which get a newline appended.
+ *
+ * The domNode must be attached to the DOM.
+ */
+
+export const getPlainText = (domNode: DOMNode) => {
+ let text = ''
+
+ if (isDOMText(domNode) && domNode.nodeValue) {
+ return domNode.nodeValue
+ }
+
+ if (isDOMElement(domNode)) {
+ for (const childNode of Array.from(domNode.childNodes)) {
+ text += getPlainText(childNode)
+ }
+
+ const display = getComputedStyle(domNode).getPropertyValue('display')
+
+ if (display === 'block' || display === 'list' || domNode.tagName === 'BR') {
+ text += '\n'
+ }
+ }
+
+ return text
+}
diff --git a/packages/slate/src/create-editor.ts b/packages/slate/src/create-editor.ts
index f333b3716..c1c1b25c6 100755
--- a/packages/slate/src/create-editor.ts
+++ b/packages/slate/src/create-editor.ts
@@ -135,6 +135,15 @@ export const createEditor = (): Editor => {
}
},
+ getFragment: () => {
+ const { selection } = editor
+
+ if (selection && Range.isExpanded(selection)) {
+ return Node.fragment(editor, selection)
+ }
+ return []
+ },
+
insertBreak: () => {
Transforms.splitNodes(editor, { always: true })
},
diff --git a/packages/slate/src/interfaces/editor.ts b/packages/slate/src/interfaces/editor.ts
index b228501f4..782c36aed 100755
--- a/packages/slate/src/interfaces/editor.ts
+++ b/packages/slate/src/interfaces/editor.ts
@@ -52,6 +52,7 @@ export interface Editor {
deleteBackward: (unit: 'character' | 'word' | 'line' | 'block') => void
deleteForward: (unit: 'character' | 'word' | 'line' | 'block') => void
deleteFragment: () => void
+ getFragment: () => Descendant[]
insertBreak: () => void
insertFragment: (fragment: Node[]) => void
insertNode: (node: Node) => void