diff --git a/packages/slate-react/src/components/editable.tsx b/packages/slate-react/src/components/editable.tsx index 5539d1089..44fcc60ae 100644 --- a/packages/slate-react/src/components/editable.tsx +++ b/packages/slate-react/src/components/editable.tsx @@ -579,7 +579,7 @@ export const Editable = (props: EditableProps) => { !isEventHandled(event, attributes.onCopy) ) { event.preventDefault() - setFragmentData(event.clipboardData, editor) + ReactEditor.setFragmentData(editor, event.clipboardData) } }, [attributes.onCopy] @@ -592,7 +592,7 @@ export const Editable = (props: EditableProps) => { !isEventHandled(event, attributes.onCut) ) { event.preventDefault() - setFragmentData(event.clipboardData, editor) + ReactEditor.setFragmentData(editor, event.clipboardData) const { selection } = editor if (selection && Range.isExpanded(selection)) { @@ -637,7 +637,7 @@ export const Editable = (props: EditableProps) => { Transforms.select(editor, range) } - setFragmentData(event.dataTransfer, editor) + ReactEditor.setFragmentData(editor, event.dataTransfer) } }, [attributes.onDragStart] @@ -1007,124 +1007,3 @@ const isDOMEventHandled = (event: Event, handler?: (event: Event) => void) => { handler(event) return event.defaultPrevented } - -/** - * Set the currently selected fragment to the clipboard. - */ - -const setFragmentData = ( - dataTransfer: DataTransfer, - editor: ReactEditor -): void => { - const { selection } = editor - - if (!selection) { - return - } - - const [start, end] = Range.edges(selection) - const startVoid = Editor.void(editor, { at: start.path }) - const endVoid = Editor.void(editor, { 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(editor, 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(editor, 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 = Node.fragment(editor, selection) - const string = JSON.stringify(fragment) - const encoded = window.btoa(encodeURIComponent(string)) - attach.setAttribute('data-slate-fragment', encoded) - dataTransfer.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) - 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