mirror of
https://github.com/ianstormtaylor/slate.git
synced 2025-08-17 04:34:00 +02:00
Make setFragmentData and getFragment pluggable in ReactEditor (#3620)
This commit is contained in:
@@ -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 `<div>/<span>` 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 `<span>` 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 <div> 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
|
||||
}
|
||||
|
@@ -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.
|
||||
*/
|
||||
|
@@ -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 = <T extends Editor>(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 `<div>/<span>` 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 `<span>` 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 <div> 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')
|
||||
|
||||
|
@@ -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
|
||||
}
|
||||
|
Reference in New Issue
Block a user