1
0
mirror of https://github.com/ianstormtaylor/slate.git synced 2025-04-21 13:51:59 +02:00

refactor data transfer to be less complex, closes #498 (#519)

This commit is contained in:
Ian Storm Taylor 2016-12-10 12:38:45 -08:00 committed by GitHub
parent a5dc3b7c8d
commit 4e177e4092
3 changed files with 100 additions and 296 deletions

View File

@ -6,7 +6,7 @@ import OffsetKey from '../utils/offset-key'
import React from 'react'
import ReactDOM from 'react-dom'
import Selection from '../models/selection'
import Transfer from '../utils/transfer'
import getTransferData from '../utils/get-transfer-data'
import TYPES from '../constants/types'
import getWindow from 'get-window'
import keycode from 'keycode'
@ -328,10 +328,10 @@ class Content extends React.Component {
if (!this.isInContentEditable(event)) return
const { dataTransfer } = event.nativeEvent
const transfer = new Transfer(dataTransfer)
const data = getTransferData(dataTransfer)
// Prevent default when nodes are dragged to allow dropping.
if (transfer.getType() == 'node') {
if (data.type == 'node') {
event.preventDefault()
}
@ -354,10 +354,10 @@ class Content extends React.Component {
this.tmp.isDragging = true
this.tmp.isInternalDrag = true
const { dataTransfer } = event.nativeEvent
const transfer = new Transfer(dataTransfer)
const data = getTransferData(dataTransfer)
// If it's a node being dragged, the data type is already set.
if (transfer.getType() == 'node') return
if (data.type == 'node') return
const { state } = this.props
const { fragment } = state
@ -383,8 +383,7 @@ class Content extends React.Component {
const { state } = this.props
const { nativeEvent } = event
const { dataTransfer, x, y } = nativeEvent
const transfer = new Transfer(dataTransfer)
const data = transfer.getData()
const data = getTransferData(dataTransfer)
// Resolve the point where the drop occured.
let range
@ -584,8 +583,7 @@ class Content extends React.Component {
if (!this.isInContentEditable(event)) return
event.preventDefault()
const transfer = new Transfer(event.clipboardData)
const data = transfer.getData()
const data = getTransferData(event.clipboardData)
// Attach the `isShift` flag, so that people can use it to trigger "Paste
// and Match Style" logic.

View File

@ -0,0 +1,93 @@
import Base64 from '../serializers/base-64'
import TYPES from '../constants/types'
/**
* Fragment matching regexp for HTML nodes.
*
* @type {RegExp}
*/
const FRAGMENT_MATCHER = /data-slate-fragment="([^\s]+)"/
/**
* Get the data and type from a native data `transfer`.
*
* @param {DataTransfer} transfer
* @return {Object}
*/
function getTransferData(transfer) {
let fragment = transfer.getData(TYPES.FRAGMENT) || null
let node = transfer.getData(TYPES.NODE) || null
let html = transfer.getData('text/html') || null
let rich = transfer.getData('text/rtf') || null
let text = transfer.getData('text/plain') || null
let files
// If there isn't a fragment, but there is HTML, check to see if the HTML is
// actually an encoded fragment.
if (
!fragment &&
html &&
~html.indexOf('<span data-slate-fragment="')
) {
const matches = FRAGMENT_MATCHER.exec(html)
const [ full, encoded ] = matches // eslint-disable-line no-unused-vars
if (encoded) fragment = encoded
}
// Decode a fragment or node if they exist.
if (fragment) fragment = Base64.deserializeNode(fragment)
if (node) node = Base64.deserializeNode(node)
// Get and normalize files if they exist.
if (transfer.items && transfer.items.length) {
const fileItems = Array.from(transfer.items)
.map(item => item.kind == 'file' ? item.getAsFile() : null)
.filter(exists => exists)
if (fileItems.length) files = fileItems
}
if (transfer.files && transfer.files.length) {
files = Array.from(files)
}
// Determine the type of the data.
const data = { files, fragment, html, node, rich, text }
data.type = getTransferType(data)
return data
}
/**
* Get the type of a transfer from its `data`.
*
* @param {Object} data
* @return {String}
*/
function getTransferType(data) {
if (data.fragment) return 'fragment'
if (data.node) return 'node'
// COMPAT: Microsoft Word adds an image of the selected text to the data.
// Since files are preferred over HTML or text, this would cause the type to
// be considered `files`. But it also adds rich text data so we can check
// for that and properly set the type to `html` or `text`. (2016/11/21)
if (data.rich && data.html) return 'html'
if (data.rich && data.text) return 'text'
if (data.files) return 'files'
if (data.html) return 'html'
if (data.text) return 'text'
return 'unknown'
}
/**
* Export.
*
* @type {Function}
*/
export default getTransferData

View File

@ -1,287 +0,0 @@
import Base64 from '../serializers/base-64'
import TYPES from '../constants/types'
/**
* Fragment matching regexp for HTML nodes.
*
* @type {RegExp}
*/
const FRAGMENT_MATCHER = /data-slate-fragment="([^\s]+)"/
/**
* Data transfer helper.
*
* @type {Transfer}
*/
class Transfer {
/**
* Constructor.
*
* @param {DataTransfer} data
*/
constructor(data) {
this.data = data
this.cache = {}
}
/**
* Get a data object representing the transfer's primary content type.
*
* @return {Object}
*/
getData() {
const type = this.getType()
const data = {}
data.type = type
switch (type) {
case 'files':
data.files = this.getFiles()
break
case 'fragment':
data.fragment = this.getFragment()
break
case 'html':
data.html = this.getHtml()
data.text = this.getText()
break
case 'node':
data.node = this.getNode()
break
case 'text':
data.text = this.getText()
break
}
return data
}
/**
* Get the Files content of the data transfer.
*
* @return {Array|Void}
*/
getFiles() {
if ('files' in this.cache) return this.cache.files
const { data } = this
let files
if (data.items && data.items.length) {
const fileItems = Array.from(data.items)
.map(item => item.kind == 'file' ? item.getAsFile() : null)
.filter(exists => exists)
if (fileItems.length) files = fileItems
}
if (data.files && data.files.length) {
files = Array.from(data.files)
}
this.cache.files = files
return files
}
/**
* Get the Slate document fragment content of the data transfer.
*
* @return {Document || Void}
*/
getFragment() {
if ('fragment' in this.cache) return this.cache.fragment
const html = this.getHtml()
let encoded = this.data.getData(TYPES.FRAGMENT)
let fragment
// If there's html content, and the html includes a `data-fragment`
// attribute, it's actually a Base64-serialized fragment from a cut/copy.
if (!encoded && html && ~html.indexOf('<span data-slate-fragment="')) {
const matches = FRAGMENT_MATCHER.exec(html)
const [ full, attribute ] = matches // eslint-disable-line no-unused-vars
encoded = attribute
}
if (encoded) {
fragment = Base64.deserializeNode(encoded)
}
this.cache.fragment = fragment
return fragment
}
/**
* Get the HTML content of the data transfer.
*
* @return {String|Void}
*/
getHtml() {
if ('html' in this.cache) return this.cache.html
let html
const string = this.data.getData('text/html')
if (string != '') html = string
this.cache.html = html
return html
}
/**
* Get the Slate node content of the data transfer.
*
* @return {Node|Void}
*/
getNode() {
if ('node' in this.cache) return this.cache.node
const encoded = this.data.getData(TYPES.NODE)
let node
if (encoded) {
node = Base64.deserializeNode(encoded)
}
this.cache.node = node
return node
}
/**
* Get the rich text content of the data transfer.
*
* @return {String|Void}
*/
getRichText() {
if ('richtext' in this.cache) return this.cache.richtext
let richtext
const string = this.data.getData('text/rtf')
if (string != '') richtext = string
this.cache.richtext = richtext
return richtext
}
/**
* Get the text content of the data transfer.
*
* @return {String|Void}
*/
getText() {
if ('text' in this.cache) return this.cache.text
let text
const string = this.data.getData('text/plain')
if (string != '') text = string
this.cache.text = text
return text
}
/**
* Get the primary type of the data transfer.
*
* @return {String}
*/
getType() {
if (this.hasFragment()) return 'fragment'
if (this.hasNode()) return 'node'
// COMPAT: Microsoft Word adds an image of the selected text to the data.
// Since files are preferred over HTML or text, this would cause the type to
// be considered `files`. But it also adds rich text data so we can check
// for that and properly set the type to `html` or `text`. (2016/11/21)
if (this.hasRichText() && this.hasHtml()) return 'html'
if (this.hasRichText() && this.hasText()) return 'text'
if (this.hasFiles()) return 'files'
if (this.hasHtml()) return 'html'
if (this.hasText()) return 'text'
return 'unknown'
}
/**
* Check whether the data transfer has File content.
*
* @return {Boolean}
*/
hasFiles() {
return this.getFiles() != null
}
/**
* Check whether the data transfer has HTML content.
*
* @return {Boolean}
*/
hasHtml() {
return this.getHtml() != null
}
/**
* Check whether the data transfer has rich text content.
*
* @return {Boolean}
*/
hasRichText() {
return this.getRichText() != null
}
/**
* Check whether the data transfer has text content.
*
* @return {Boolean}
*/
hasText() {
return this.getText() != null
}
/**
* Check whether the data transfer has a Slate document fragment as content.
*
* @return {Boolean}
*/
hasFragment() {
return this.getFragment() != null
}
/**
* Check whether the data transfer has a Slate node as content.
*
* @return {Boolean}
*/
hasNode() {
return this.getNode() != null
}
}
/**
* Export.
*
* @type {Transfer}
*/
export default Transfer