From 31c75590aee87927caef8e2aec9a7d947141abcb Mon Sep 17 00:00:00 2001 From: mjadobson Date: Wed, 12 Jul 2017 16:43:38 +0100 Subject: [PATCH] Fix Edge Drag & Drop Issues (#923) * Fix Edge doctype warning * Remove getData() calls in onDragOver Access to data within ondragover is prohibited * Handle Edge errors accessing `dataTransfer.dropEffect` * Work around Edge not supporting custom data in drag events If unable to use `setData()` with custom type, then uses JSON obj to store data in 'text/plain' * Fix more Edge errors Edge sometimes throws 'NotSupportedError' whena accessing `items` property on `dataTransfer` * Fix linting errors * Fix formatting --- examples/index.html | 1 + src/components/content.js | 21 ++++++------ src/components/node.js | 6 ++-- src/utils/get-transfer-data.js | 58 +++++++++++++++++++++++++++++----- src/utils/set-transfer-data.js | 42 ++++++++++++++++++++++++ 5 files changed, 109 insertions(+), 19 deletions(-) create mode 100644 src/utils/set-transfer-data.js diff --git a/examples/index.html b/examples/index.html index 63441d7f1..4d1a26cb1 100644 --- a/examples/index.html +++ b/examples/index.html @@ -1,3 +1,4 @@ + diff --git a/src/components/content.js b/src/components/content.js index 854ca16a6..d026ebae3 100644 --- a/src/components/content.js +++ b/src/components/content.js @@ -14,6 +14,7 @@ import findClosestNode from '../utils/find-closest-node' import findDeepestNode from '../utils/find-deepest-node' import getPoint from '../utils/get-point' import getTransferData from '../utils/get-transfer-data' +import setTransferData from '../utils/set-transfer-data' import { IS_FIREFOX, IS_MAC } from '../constants/environment' /** @@ -444,13 +445,7 @@ class Content extends React.Component { onDragOver = (event) => { if (!this.isInEditor(event.target)) return - const { dataTransfer } = event.nativeEvent - const data = getTransferData(dataTransfer) - - // Prevent default when nodes are dragged to allow dropping. - if (data.type == 'node') { - event.preventDefault() - } + event.preventDefault() if (this.tmp.isDragging) return this.tmp.isDragging = true @@ -479,7 +474,8 @@ class Content extends React.Component { const { state } = this.props const { fragment } = state const encoded = Base64.serializeNode(fragment) - dataTransfer.setData(TYPES.FRAGMENT, encoded) + + setTransferData(dataTransfer, TYPES.FRAGMENT, encoded) debug('onDragStart', { event }) } @@ -530,7 +526,14 @@ class Content extends React.Component { // Add drop-specific information to the data. data.target = target - data.effect = dataTransfer.dropEffect + + // COMPAT: Edge throws "Permission denied" errors when + // accessing `dropEffect` or `effectAllowed` (2017/7/12) + try { + data.effect = dataTransfer.dropEffect + } catch (err) { + data.effect = null + } if (data.type == 'fragment' || data.type == 'node') { data.isInternal = this.tmp.isInternalDrag diff --git a/src/components/node.js b/src/components/node.js index 301f1ee8e..b121c5c41 100644 --- a/src/components/node.js +++ b/src/components/node.js @@ -10,6 +10,7 @@ import Leaf from './leaf' import Void from './void' import getWindow from 'get-window' import scrollToSelection from '../utils/scroll-to-selection' +import setTransferData from '../utils/set-transfer-data' /** * Debug. @@ -218,8 +219,9 @@ class Node extends React.Component { } const encoded = Base64.serializeNode(node, { preserveKeys: true }) - const data = e.nativeEvent.dataTransfer - data.setData(TYPES.NODE, encoded) + const { dataTransfer } = e.nativeEvent + + setTransferData(dataTransfer, TYPES.NODE, encoded) this.debug('onDragStart', e) } diff --git a/src/utils/get-transfer-data.js b/src/utils/get-transfer-data.js index 6c0ffb208..56b0a89e6 100644 --- a/src/utils/get-transfer-data.js +++ b/src/utils/get-transfer-data.js @@ -22,7 +22,7 @@ function getTransferData(transfer) { let node = transfer.getData(TYPES.NODE) || null const html = transfer.getData('text/html') || null const rich = transfer.getData('text/rtf') || null - const text = transfer.getData('text/plain') || 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 @@ -37,17 +37,35 @@ function getTransferData(transfer) { if (encoded) fragment = encoded } + // COMPAT: Edge doesn't handle custom data types + // These will be embedded in text/plain in this case (2017/7/12) + if (text) { + const embeddedTypes = getEmbeddedTypes(text) + + if (embeddedTypes[TYPES.FRAGMENT]) fragment = embeddedTypes[TYPES.FRAGMENT] + if (embeddedTypes[TYPES.NODE]) node = embeddedTypes[TYPES.NODE] + if (embeddedTypes['text/plain']) text = embeddedTypes['text/plain'] + } + // 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) { - files = Array.from(transfer.items) - .map(item => item.kind == 'file' ? item.getAsFile() : null) - .filter(exists => exists) - } else if (transfer.files && transfer.files.length) { - files = Array.from(transfer.files) + // COMPAT: Edge sometimes throws 'NotSupportedError' + // when accessing `transfer.items` (2017/7/12) + try { + // Get and normalize files if they exist. + if (transfer.items && transfer.items.length) { + files = Array.from(transfer.items) + .map(item => item.kind == 'file' ? item.getAsFile() : null) + .filter(exists => exists) + } else if (transfer.files && transfer.files.length) { + files = Array.from(transfer.files) + } + } catch (err) { + if (transfer.files && transfer.files.length) { + files = Array.from(transfer.files) + } } // Determine the type of the data. @@ -56,6 +74,30 @@ function getTransferData(transfer) { return data } +/** + * Takes text input, checks whether contains embedded data + * and returns object with original text +/- additional data + * + * @param {String} text + * @return {Object} + */ + +function getEmbeddedTypes(text) { + const prefix = 'SLATE-DATA-EMBED::' + + if (text.substring(0, prefix.length) !== prefix) { + return { 'text/plain': text } + } + + // Attempt to parse, if fails then just standard text/plain + // Otherwise, already had data embedded + try { + return JSON.parse(text.substring(prefix.length)) + } catch (err) { + throw new Error('Unable to parse custom embedded drag data') + } +} + /** * Get the type of a transfer from its `data`. * diff --git a/src/utils/set-transfer-data.js b/src/utils/set-transfer-data.js new file mode 100644 index 000000000..3143cd676 --- /dev/null +++ b/src/utils/set-transfer-data.js @@ -0,0 +1,42 @@ +/** + * Set data on dataTransfer + * COMPAT: In Edge, custom types throw errors, so embed all non-standard + * types in text/plain compound object. (2017/7/12) + * + * @param {DataTransfer} dataTransfer + * @param {String} type + * @param {String} content + */ + +function setTransferData(dataTransfer, type, content) { + try { + dataTransfer.setData(type, content) + } catch (err) { + const prefix = 'SLATE-DATA-EMBED::' + let obj = {} + const text = dataTransfer.getData('text/plain') + + // If prefixed, assume embedded drag data + if (text.substring(0, prefix.length) === prefix) { + try { + obj = JSON.parse(text.substring(prefix.length)) + } catch (err2) { + throw new Error('Unable to parse custom embedded drag data') + } + } else { + obj['text/plain'] = text + } + + obj[type] = content + + dataTransfer.setData('text/plain', `${prefix}${JSON.stringify(obj)}`) + } +} + +/** + * Export. + * + * @type {Function} + */ + +export default setTransferData