diff --git a/History.md b/History.md index b3bbf3cf5..43cd54268 100644 --- a/History.md +++ b/History.md @@ -5,6 +5,13 @@ This document maintains a list of changes to Slate with each new version. Until --- +### `0.12.0` — _August 9, 2016_ + +#### BREAKING CHANGES + +- **The `data.files` property is now an `Array`. Previously it was a native `FileList` object, but needed to be changed to add full support for pasting an dropping files in all browsers. This shouldn't affect you unless you were specifically depending on it being array-like instead of a true `Array`. + + ### `0.11.0` — _August 4, 2016_ #### BREAKING CHANGES diff --git a/examples/images/index.js b/examples/images/index.js index 9b129f40d..a06476123 100644 --- a/examples/images/index.js +++ b/examples/images/index.js @@ -164,11 +164,27 @@ class Images extends React.Component { * @param {Event} e * @param {Object} data * @param {State} state + * @param {Editor} editor * @return {State} */ - onDrop = (e, data, state) => { - if (data.type != 'node') return + onDrop = (e, data, state, editor) => { + switch (data.type) { + case 'files': return this.onDropOrPasteFiles(e, data, state, editor) + case 'node': return this.onDropNode(e, data, state) + } + } + + /** + * On drop node, insert the node wherever it is dropped. + * + * @param {Event} e + * @param {Object} data + * @param {State} state + * @return {State} + */ + + onDropNode = (e, data, state) => { return state .transform() .removeNodeByKey(data.node.key) @@ -177,17 +193,59 @@ class Images extends React.Component { .apply() } + /** + * On drop or paste files, read and insert the image files. + * + * @param {Event} e + * @param {Object} data + * @param {State} state + * @param {Editor} editor + * @return {State} + */ + + onDropOrPasteFiles = (e, data, state, editor) => { + for (const file of data.files) { + const reader = new FileReader() + const [ type, ext ] = file.type.split('/') + if (type != 'image') continue + + reader.addEventListener('load', () => { + state = editor.getState() + state = this.insertImage(state, reader.result) + editor.onChange(state) + }) + + reader.readAsDataURL(file) + } + } + /** * On paste, if the pasted content is an image URL, insert it. * * @param {Event} e * @param {Object} data * @param {State} state + * @param {Editor} editor + * @return {State} + */ + + onPaste = (e, data, state, editor) => { + switch (data.type) { + case 'files': return this.onDropOrPasteFiles(e, data, state, editor) + case 'text': return this.onPasteText(e, data, state) + } + } + + /** + * On paste text, if the pasted content is an image URL, insert it. + * + * @param {Event} e + * @param {Object} data + * @param {State} state * @return {State} */ - onPaste = (e, data, state) => { - if (data.type != 'text') return + onPasteText = (e, data, state) => { if (!isUrl(data.text)) return if (!isImage(data.text)) return return this.insertImage(state, data.text) diff --git a/lib/components/content.js b/lib/components/content.js index 2da1406a9..5163e17c9 100644 --- a/lib/components/content.js +++ b/lib/components/content.js @@ -5,6 +5,7 @@ import Node from './node' import OffsetKey from '../utils/offset-key' import React from 'react' import Selection from '../models/selection' +import Transfer from '../utils/transfer' import TYPES from '../utils/types' import getWindow from 'get-window' import includes from 'lodash/includes' @@ -318,12 +319,11 @@ class Content extends React.Component { onDragOver = (e) => { if (isNonEditable(e)) return - const data = e.nativeEvent.dataTransfer - // COMPAT: In Firefox, `types` is array-like. (2016/06/21) - const types = Array.from(data.types) + const { dataTransfer } = e.nativeEvent + const transfer = new Transfer(dataTransfer) // Prevent default when nodes are dragged to allow dropping. - if (includes(types, TYPES.NODE)) { + if (transfer.getType() == 'node') { e.preventDefault() } @@ -345,17 +345,16 @@ class Content extends React.Component { this.tmp.isDragging = true this.tmp.isInternalDrag = true - const data = e.nativeEvent.dataTransfer - // COMPAT: In Firefox, `types` is array-like. (2016/06/21) - const types = Array.from(data.types) + const { dataTransfer } = e.nativeEvent + const transfer = new Transfer(dataTransfer) // If it's a node being dragged, the data type is already set. - if (includes(types, TYPES.NODE)) return + if (transfer.getType() == 'node') return const { state } = this.props const { fragment } = state const encoded = Base64.serializeNode(fragment) - data.setData(TYPES.FRAGMENT, encoded) + dataTransfer.setData(TYPES.FRAGMENT, encoded) debug('onDragStart') } @@ -376,10 +375,8 @@ class Content extends React.Component { const { state, renderDecorations } = this.props const { selection } = state const { dataTransfer, x, y } = e.nativeEvent - const data = {} - - // COMPAT: In Firefox, `types` is array-like. (2016/06/21) - const types = Array.from(dataTransfer.types) + const transfer = new Transfer(dataTransfer) + const data = transfer.getData() // Resolve the point where the drop occured. let range @@ -406,46 +403,14 @@ class Content extends React.Component { // If the target is inside a void node, abort. if (state.document.hasVoidParent(point.key)) return - // Handle Slate fragments. - if (includes(types, TYPES.FRAGMENT)) { - const encoded = dataTransfer.getData(TYPES.FRAGMENT) - const fragment = Base64.deserializeNode(encoded) - data.type = 'fragment' - data.fragment = fragment - data.isInternal = this.tmp.isInternalDrag - } - - // Handle Slate nodes. - else if (includes(types, TYPES.NODE)) { - const encoded = dataTransfer.getData(TYPES.NODE) - const node = Base64.deserializeNode(encoded) - data.type = 'node' - data.node = node - data.isInternal = this.tmp.isInternalDrag - } - - // Handle files. - else if (dataTransfer.files.length) { - data.type = 'files' - data.files = dataTransfer.files - } - - // Handle HTML. - else if (includes(types, TYPES.HTML)) { - data.type = 'html' - data.text = dataTransfer.getData(TYPES.TEXT) - data.html = dataTransfer.getData(TYPES.HTML) - } - - // Handle plain text. - else { - data.type = 'text' - data.text = dataTransfer.getData(TYPES.TEXT) - } - + // Add drop-specific information to the data. data.target = target data.effect = dataTransfer.dropEffect + if (data.type == 'fragment' || data.type == 'node') { + data.isInternal = this.tmp.isInternalDrag + } + debug('onDrop', data) this.props.onDrop(e, data) } @@ -575,42 +540,8 @@ class Content extends React.Component { if (isNonEditable(e)) return e.preventDefault() - - const { clipboardData } = e - const data = {} - - // COMPAT: In Firefox, `types` is array-like. (2016/06/21) - const types = Array.from(clipboardData.types) - - // Handle files. - if (clipboardData.files.length) { - data.type = 'files' - data.files = clipboardData.files - } - - // Treat it as rich text if there is HTML content. - else if (includes(types, TYPES.HTML)) { - data.type = 'html' - data.text = clipboardData.getData(TYPES.TEXT) - data.html = clipboardData.getData(TYPES.HTML) - } - - // Treat everything else as plain text. - else { - data.type = 'text' - data.text = clipboardData.getData(TYPES.TEXT) - } - - // If html, and the html includes a `data-fragment` attribute, it's actually - // a raw-serialized JSON fragment from a previous cut/copy, so deserialize - // it and update the data. - if (data.type == 'html' && ~data.html.indexOf(' 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('