diff --git a/TODO b/TODO index c7db240f8a..2577d21b0a 100644 --- a/TODO +++ b/TODO @@ -4,6 +4,9 @@ * check symbols * helpers for test context * drag offsets and alignments +* are we okay with this API for refs? ask people & see how that works with actual refs dnd-core * smarter canDrop() +* isOver() without canDrop()? +* state extractor on removed target diff --git a/examples/_dustbin-interesting/Container.js b/examples/_dustbin-interesting/Container.js index 5e1e56bb40..1848a9103e 100644 --- a/examples/_dustbin-interesting/Container.js +++ b/examples/_dustbin-interesting/Container.js @@ -4,7 +4,7 @@ import React, { createClass } from 'react'; import Dustbin from './Dustbin'; import Box from './Box'; import ItemTypes from './ItemTypes'; -import { DragDropContext, HTML5Backend } from 'react-dnd'; +import { DragDropContext, HTML5Backend, NativeTypes } from 'react-dnd'; import shuffle from 'lodash/collection/shuffle'; import update from 'react/lib/update'; @@ -18,7 +18,8 @@ const Container = createClass({ dustbins: [ [ItemTypes.GLASS], [ItemTypes.FOOD], - [ItemTypes.PAPER, ItemTypes.GLASS] + [ItemTypes.PAPER, ItemTypes.GLASS, NativeTypes.URL], + [ItemTypes.PAPER, NativeTypes.FILE] ], boxes: [ { name: 'Bottle', type: ItemTypes.GLASS }, diff --git a/examples/webpack.config.js b/examples/webpack.config.js index f0390b4d87..1d49df76bf 100644 --- a/examples/webpack.config.js +++ b/examples/webpack.config.js @@ -18,7 +18,8 @@ module.exports = { }, module: { loaders: [ - { test: /\.js$/, loaders: ['react-hot-loader', '6to5?experimental'], exclude: /node_modules/ } + // TODO: temp dnd-core for npm link + { test: /\.js$/, loaders: ['6to5?experimental'], exclude: /node_modules|dnd-core/ } ] }, plugins: [ diff --git a/modules/NativeTypes.js b/modules/NativeTypes.js new file mode 100644 index 0000000000..7d3ca38308 --- /dev/null +++ b/modules/NativeTypes.js @@ -0,0 +1,4 @@ +export default { + FILE: '__NATIVE_FILE__', + URL: '__NATIVE_URL__' +}; \ No newline at end of file diff --git a/modules/backends/HTML5.js b/modules/backends/HTML5.js index 5ffe2d8d41..7bfa0d44b9 100644 --- a/modules/backends/HTML5.js +++ b/modules/backends/HTML5.js @@ -1,7 +1,65 @@ +import { DragSource } from 'dnd-core'; +import NativeTypes from '../NativeTypes'; +import warning from 'react/lib/warning'; + +function isUrlDataTransfer(dataTransfer) { + var types = Array.prototype.slice.call(dataTransfer.types); + return types.indexOf('Url') > -1 || types.indexOf('text/uri-list') > -1; +} + +function isFileDataTransfer(dataTransfer) { + var types = Array.prototype.slice.call(dataTransfer.types); + return types.indexOf('Files') > -1; +} + +class FileDragSource extends DragSource { + constructor() { + this.item = { + get files() { + warning(false, 'Browser doesn\'t allow reading file information until the files are dropped.'); + return null; + } + }; + } + + mutateItemByReadingDataTransfer(dataTransfer) { + delete this.item.files; + this.item.files = Array.prototype.slice.call(dataTransfer.files); + } + + beginDrag() { + return this.item; + } +} + +class UrlDragSource extends DragSource { + constructor() { + this.item = { + get urls() { + warning(false, 'Browser doesn\'t allow reading URL information until the link is dropped.'); + return null; + } + }; + } + + mutateItemByReadingDataTransfer(dataTransfer) { + delete this.item.urls; + this.item.urls = ( + dataTransfer.getData('Url') || + dataTransfer.getData('text/uri-list') || '' + ).split('\n'); + } + + beginDrag() { + return this.item; + } +} + export default class HTML5Backend { - constructor(actions, monitor) { + constructor(actions, monitor, registry) { this.actions = actions; this.monitor = monitor; + this.registry = registry; this.nodeHandlers = {}; this.handleTopDragStart = this.handleTopDragStart.bind(this); @@ -55,6 +113,26 @@ export default class HTML5Backend { return [0, 0]; } + isDraggingNativeItem() { + switch (this.monitor.getItemType()) { + case NativeTypes.FILE: + case NativeTypes.URL: + return true; + default: + return false; + } + } + + beginDragNativeUrl() { + const sourceHandle = this.registry.addSource(NativeTypes.URL, new UrlDragSource()); + this.actions.beginDrag(sourceHandle); + } + + beginDragNativeFile() { + const sourceHandle = this.registry.addSource(NativeTypes.FILE, new FileDragSource()); + this.actions.beginDrag(sourceHandle); + } + handleTopDragStartCapture() { this.dragStartSourceHandles = []; this.dragStartOriginalTarget = null; @@ -81,24 +159,22 @@ export default class HTML5Backend { } } - // If none agreed, cancel the dragging. - if (!this.monitor.isDragging()) { + const { dataTransfer } = e; + if (this.monitor.isDragging()) { + // If child drag source refuses drag but parent agrees, + // use parent's node as drag image. This won't work in IE. + const dragOffset = this.getDragImageOffset(node); + dataTransfer.setDragImage(node, ...dragOffset); + dataTransfer.setData('application/json', {}); + + this.dragStartOriginalTarget = e.target; + } else if (isUrlDataTransfer(dataTransfer)) { + // URL dragged from inside the document + this.beginDragNativeUrl(); + } else { + // If by this time no drag source reacted, tell browser not to drag. e.preventDefault(); - return; } - - // Save the original target so we can later check - // dragend events against it. - this.dragStartOriginalTarget = e.target; - - // Specify backend's MIME so other backends - // don't interfere with this drag operation. - e.dataTransfer.setData(this.mime, {}); - - // If child drag source refuses drag but parent agrees, - // use parent's node as drag image. This won't work in IE. - const dragOffset = this.getDragImageOffset(node); - e.dataTransfer.setDragImage(node, ...dragOffset); } handleTopDragEndCapture() { @@ -116,8 +192,12 @@ export default class HTML5Backend { handleTopDragEnd() { } - handleTopDragOverCapture() { + handleTopDragOverCapture(e) { this.dragOverTargetHandles = []; + + if (this.isDraggingNativeItem()) { + e.preventDefault(); + } } handleDragOver(e, targetHandle) { @@ -140,8 +220,21 @@ export default class HTML5Backend { } } - handleTopDragEnterCapture() { + handleTopDragEnterCapture(e) { this.dragEnterTargetHandles = []; + + if (this.monitor.isDragging()) { + return; + } + + const { dataTransfer } = e; + if (isFileDataTransfer(dataTransfer)) { + // File dragged from outside the document + this.beginDragNativeFile(); + } else if (isUrlDataTransfer(dataTransfer)) { + // URL dragged from outside the document + this.beginDragNativeUrl(); + } } handleDragEnter(e, targetHandle) { @@ -164,14 +257,25 @@ export default class HTML5Backend { } } - handleTopDragLeaveCapture() { + handleTopDragLeaveCapture(e) { + if (this.isDraggingNativeItem()) { + e.preventDefault(); + } } handleTopDragLeave() { } - handleTopDropCapture() { + handleTopDropCapture(e) { this.dropTargetHandles = []; + + if (this.isDraggingNativeItem()) { + e.preventDefault(); + + const sourceHandle = this.monitor.getSourceHandle(); + const source = this.registry.getSource(sourceHandle); + source.mutateItemByReadingDataTransfer(e.dataTransfer); + } } handleDrop(e, targetHandle) { @@ -184,6 +288,10 @@ export default class HTML5Backend { this.actions.hover(dropTargetHandles); this.actions.drop(); + + if (this.isDraggingNativeItem()) { + this.actions.endDrag(); + } } updateSourceNode(sourceHandle, node) { diff --git a/modules/index.js b/modules/index.js index f26064de6e..9971b274f4 100644 --- a/modules/index.js +++ b/modules/index.js @@ -1,6 +1,7 @@ export { default as HTML5Backend } from './backends/HTML5'; export { default as DragSource } from './ReactDragSource'; export { default as DropTarget } from './ReactDropTarget'; +export { default as NativeTypes } from './NativeTypes'; // We want need those after React 0.14: export { default as DragDropContext } from './DragDropContext';