1
0
mirror of https://github.com/ianstormtaylor/slate.git synced 2025-08-17 20:51:20 +02:00

Make setFragmentData and getFragment pluggable in ReactEditor (#3620)

This commit is contained in:
Jamie Talbot
2020-04-27 15:05:28 -04:00
committed by GitHub
parent 7369bb9e97
commit ddef719467
6 changed files with 139 additions and 126 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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