diff --git a/lib/components/content.js b/lib/components/content.js index 866717229..7e14b34e6 100644 --- a/lib/components/content.js +++ b/lib/components/content.js @@ -1,29 +1,47 @@ +import Fragment from '../utils/fragment' import Key from '../utils/key' -import Selection from '../models/selection' import OffsetKey from '../utils/offset-key' import Raw from '../serializers/raw' import React from 'react' +import Selection from '../models/selection' import Text from './text' import includes from 'lodash/includes' import keycode from 'keycode' -import Base64 from '../utils/base64' import { IS_FIREFOX } from '../utils/environment' /** * Noop. + * + * @type {Function} */ function noop() {} +/** + * Content types. + * + * @type {Object} + */ + +const TYPES = { + HTML: 'text/html', + SLATE: 'application/x-slate', + TEXT: 'text/plain' +} + /** * Content. + * + * @type {Component} */ class Content extends React.Component { /** * Property types. + * + * @type {Object} */ static propTypes = { @@ -44,6 +62,8 @@ class Content extends React.Component { /** * Default properties. + * + * @type {Object} */ static defaultProps = { @@ -215,9 +235,7 @@ class Content extends React.Component { const { state } = this.props const { fragment } = state - const raw = Raw.serializeNode(fragment) - const string = JSON.stringify(raw) - const encoded = Base64.encode(string) + const encoded = Fragment.serialize(fragment) // Wrap the first character of the selection in a span that has the encoded // fragment attached as an attribute, so it will show up in the copied HTML. @@ -291,6 +309,11 @@ class Content extends React.Component { onDragStart = (e) => { this.tmp.isDragging = true this.tmp.isInternalDrag = true + const { state } = this.props + const { fragment } = state + const encoded = Fragment.serialize(fragment) + const data = e.nativeEvent.dataTransfer + data.setData(TYPES.SLATE, encoded) } /** @@ -354,23 +377,31 @@ class Content extends React.Component { // COMPAT: In Firefox, `types` is array-like. (2016/06/21) const types = Array.from(data.types) + // Handle external Slate drags. + if (includes(types, TYPES.SLATE)) { + const encoded = data.getData(TYPES.SLATE) + const fragment = Fragment.deserialize(encoded) + drop.type = 'fragment' + drop.fragment = fragment + } + // Handle files. - if (data.files.length) { + else if (data.files.length) { drop.type = 'files' drop.files = data.files } // Handle HTML. - else if (includes(types, 'text/html')) { + else if (includes(types, TYPES.HTML)) { drop.type = 'html' - drop.text = data.getData('text/plain') - drop.html = data.getData('text/html') + drop.text = data.getData(TYPES.TEXT) + drop.html = data.getData(TYPES.HTML) } // Handle plain text. else { drop.type = 'text' - drop.text = data.getData('text/plain') + drop.text = data.getData(TYPES.TEXT) } drop.data = data @@ -479,16 +510,16 @@ class Content extends React.Component { } // Treat it as rich text if there is HTML content. - else if (includes(types, 'text/html')) { + else if (includes(types, TYPES.HTML)) { paste.type = 'html' - paste.text = data.getData('text/plain') - paste.html = data.getData('text/html') + paste.text = data.getData(TYPES.TEXT) + paste.html = data.getData(TYPES.HTML) } // Treat everything else as plain text. else { paste.type = 'text' - paste.text = data.getData('text/plain') + paste.text = data.getData(TYPES.TEXT) } // If html, and the html includes a `data-fragment` attribute, it's actually @@ -498,14 +529,12 @@ class Content extends React.Component { const regexp = /data-fragment="([^\s]+)"/ const matches = regexp.exec(paste.html) const [ full, encoded ] = matches - const string = Base64.decode(encoded) - const json = JSON.parse(string) - const fragment = Raw.deserialize(json) + const fragment = Fragment.deserialize(encoded) let { state } = this.props state = state .transform() - .insertFragment(fragment.document) + .insertFragment(fragment) .apply() this.onChange(state) diff --git a/lib/plugins/core.js b/lib/plugins/core.js index 38c9f4cd4..bc7ad6c83 100644 --- a/lib/plugins/core.js +++ b/lib/plugins/core.js @@ -150,6 +150,13 @@ function Plugin(options = {}) { onDrop(e, drop, state, editor) { switch (drop.type) { + case 'fragment': { + return state + .transform() + .moveTo(drop.target) + .insertFragment(drop.fragment) + .apply() + } case 'text': case 'html': { let transform = state diff --git a/lib/utils/base64.js b/lib/utils/base64.js deleted file mode 100644 index 330df2560..000000000 --- a/lib/utils/base64.js +++ /dev/null @@ -1,31 +0,0 @@ - -/** - * Encode a `string` as Base64. - * - * @param {String} string - * @return {String} - */ - -function encode(string) { - return window.btoa(window.unescape(window.encodeURIComponent(string))) -} - -/** - * Decode a `string` as Base64. - * - * @param {String} string - * @return {String} - */ - -function decode(string) { - return window.decodeURIComponent(window.escape(window.atob(string))) -} - -/** - * Export. - */ - -export default { - encode, - decode -} diff --git a/lib/utils/fragment.js b/lib/utils/fragment.js new file mode 100644 index 000000000..eb0e982eb --- /dev/null +++ b/lib/utils/fragment.js @@ -0,0 +1,39 @@ + +import Raw from '../serializers/raw' + +/** + * Serialize a `string` as Base64. + * + * @param {Document} fragment + * @return {String} encoded + */ + +function serialize(fragment) { + const raw = Raw.serializeNode(fragment) + const string = JSON.stringify(raw) + const encoded = window.btoa(window.unescape(window.encodeURIComponent(string))) + return encoded +} + +/** + * Deserialize a `fragment` as Base64. + * + * @param {String} encoded + * @return {Document} fragment + */ + +function deserialize(encoded) { + const string = window.decodeURIComponent(window.escape(window.atob(encoded))) + const json = JSON.parse(string) + const state = Raw.deserialize(json) + return state.document +} + +/** + * Export. + */ + +export default { + serialize, + deserialize +}