build议使React组件/ div可拖动的方法

我想做一个可拖动的(也就是说,可以通过鼠标来重定位)React组件,它似乎必然涉及全局状态和分散的事件处理程序。 我可以用肮脏的方式来做,在我的JS文件中有一个全局variables,甚至可能把它封装在一个不错的闭包界面中,但是我想知道是否有更好的方法与React进行网格化。

另外,因为我以前从来没有在原始JavaScript中做过这件事,所以我想看看专家是如何做到的,以确保我处理了所有的angular落案例,特别是与React相关的案例。

谢谢。

我应该把它变成一个博客文章,但是这里有一个很好的例子。

评论应该解释得很好,但是如果你有问题,请告诉我。

这里的小提琴玩: http : //jsfiddle.net/Af9Jt/2/

var Draggable = React.createClass({ getDefaultProps: function () { return { // allow the initial position to be passed in as a prop initialPos: {x: 0, y: 0} } }, getInitialState: function () { return { pos: this.props.initialPos, dragging: false, rel: null // position relative to the cursor } }, // we could get away with not having this (and just having the listeners on // our div), but then the experience would be possibly be janky. If there's // anything w/ a higher z-index that gets in the way, then you're toast, // etc. componentDidUpdate: function (props, state) { if (this.state.dragging && !state.dragging) { document.addEventListener('mousemove', this.onMouseMove) document.addEventListener('mouseup', this.onMouseUp) } else if (!this.state.dragging && state.dragging) { document.removeEventListener('mousemove', this.onMouseMove) document.removeEventListener('mouseup', this.onMouseUp) } }, // calculate relative position to the mouse and set dragging=true onMouseDown: function (e) { // only left mouse button if (e.button !== 0) return var pos = $(this.getDOMNode()).offset() this.setState({ dragging: true, rel: { x: e.pageX - pos.left, y: e.pageY - pos.top } }) e.stopPropagation() e.preventDefault() }, onMouseUp: function (e) { this.setState({dragging: false}) e.stopPropagation() e.preventDefault() }, onMouseMove: function (e) { if (!this.state.dragging) return this.setState({ pos: { x: e.pageX - this.state.rel.x, y: e.pageY - this.state.rel.y } }) e.stopPropagation() e.preventDefault() }, render: function () { // transferPropsTo will merge style & other props passed into our // component to also be on the child DIV. return this.transferPropsTo(React.DOM.div({ onMouseDown: this.onMouseDown, style: { left: this.state.pos.x + 'px', top: this.state.pos.y + 'px' } }, this.props.children)) } }) 

关于国家所有权的思考等

“谁应该拥有什么国家”是一个重要的问题,从一开始就是这样回答的。 在“可拖动”组件的情况下,我可以看到几个不同的场景。

情况1

父母应该拥有可拖动的当前位置。 在这种情况下,可拖动仍然会拥有“我在拖动”状态,但每当发生mousemove事件时都会调用this.props.onChange(x, y)

情景2

父母只需要拥有“非移动位置”,所以可拖动的将拥有它的“拖动位置”,但是它会调用this.props.onChange(x, y)并将最终决定推迟到父项。 如果父类不喜欢可拖动结束的位置,则不会更新它的状态,拖动之前可拖动到其初始位置。

Mixin或组件?

@ssorallen指出,因为“可拖动”更多的是一个属性而不是一个属性本身,它可能更好地作为一个混合。 我对mixin的经验是有限的,所以我没有看到他们在复杂的情况下如何帮助或阻碍。 这可能是最好的select。

我实现了react-dnd ,这是一个灵活的HTML5拖放混合,用于完全DOM控制的React。

现有的拖放库不适合我的用例,所以我写了自己的。 这与我们在Stampsy.com上运行大约一年的代码类似,但是为了利用React和Flux而重写。

关键要求我有:

  • 发出零DOM或自己的CSS,把它留给消费组件;
  • 在消耗组件上尽可能减less结构;
  • 使用HTML5拖放作为主后端,但可以在将来添加不同的后端;
  • 像原来的HTML5 API一样,强调拖拽数据而不仅仅是“可拖动的视图”;
  • 隐藏消费代码中的HTML5 API怪癖;
  • 不同的组件可能是针对不同种类的数据的“拖动源”或“拖放目标”
  • 允许一个组件包含多个拖动源并在需要时放置目标;
  • 如果兼容的数据被拖拽或hover,使拖放目标更容易改变其外观;
  • 使拖放缩略图而不是元素屏幕截图容易使用图像,避免浏览器的怪癖。

如果这些听起来很熟悉,请继续阅读。

用法

简单的拖动源

首先,声明可以拖动的数据types。

这些用于检查拖动源和放置目标的“兼容性”:

 // ItemTypes.js module.exports = { BLOCK: 'block', IMAGE: 'image' }; 

(如果你没有多种数据types,这个库可能不适合你)

然后,让我们做一个非常简单的可拖动组件,拖动时代表IMAGE

 var { DragDropMixin } = require('react-dnd'), ItemTypes = require('./ItemTypes'); var Image = React.createClass({ mixins: [DragDropMixin], configureDragDrop(registerType) { // Specify all supported types by calling registerType(type, { dragSource?, dropTarget? }) registerType(ItemTypes.IMAGE, { // dragSource, when specified, is { beginDrag(), canDrag()?, endDrag(didDrop)? } dragSource: { // beginDrag should return { item, dragOrigin?, dragPreview?, dragEffect? } beginDrag() { return { item: this.props.image }; } } }); }, render() { // {...this.dragSourceFor(ItemTypes.IMAGE)} will expand into // { draggable: true, onDragStart: (handled by mixin), onDragEnd: (handled by mixin) }. return ( <img src={this.props.image.url} {...this.dragSourceFor(ItemTypes.IMAGE)} /> ); } ); 

通过指定configureDragDrop ,我们告诉DragDropMixin这个组件的拖放行为。 draggable和droppable组件都使用相同的mixin。

configureDragDrop ,我们需要为组件支持的每个自定义ItemTypes调用registerType 。 例如,在您的应用中可能会有几个图像表示forms,并且每个表示forms都会为ItemTypes.IMAGE提供一个dragSource

dragSource只是一个指定拖动源如何工作的对象。 您必须实现beginDrag才能返回表示要拖动的数据的项目,还可以select一些调整拖动UI的选项。 你可以select实现canDrag来禁止拖动,或者当drop(或者没有)时, endDrag(didDrop)去执行一些逻辑。 你可以通过让一个共享的mixin为它们生成dragSource来在这些组件之间共享这个逻辑。

最后,您必须在render一些(一个或多个)元素上使用{...this.dragSourceFor(itemType)}来附加拖动处理程序。 这意味着你可以在一个元素中有几个“拖动手柄”,甚至可以对应不同的项目types。 (如果您不熟悉JSX Spread Attributes语法,请查看)。

简单的下降目标

假设我们希望ImageBlock成为IMAGE的放置目标。 这几乎是一样的,除了我们需要给registerType一个dropTarget实现:

 var { DragDropMixin } = require('react-dnd'), ItemTypes = require('./ItemTypes'); var ImageBlock = React.createClass({ mixins: [DragDropMixin], configureDragDrop(registerType) { registerType(ItemTypes.IMAGE, { // dropTarget, when specified, is { acceptDrop(item)?, enter(item)?, over(item)?, leave(item)? } dropTarget: { acceptDrop(image) { // Do something with image! for example, DocumentActionCreators.setImage(this.props.blockId, image); } } }); }, render() { // {...this.dropTargetFor(ItemTypes.IMAGE)} will expand into // { onDragEnter: (handled by mixin), onDragOver: (handled by mixin), onDragLeave: (handled by mixin), onDrop: (handled by mixin) }. return ( <div {...this.dropTargetFor(ItemTypes.IMAGE)}> {this.props.image && <img src={this.props.image.url} /> } </div> ); } ); 

将源代码拖放到一个组件中

假设我们现在希望用户能够从ImageBlock拖出一个图像。 我们只需要添加适当的dragSource和一些处理程序:

 var { DragDropMixin } = require('react-dnd'), ItemTypes = require('./ItemTypes'); var ImageBlock = React.createClass({ mixins: [DragDropMixin], configureDragDrop(registerType) { registerType(ItemTypes.IMAGE, { // Add a drag source that only works when ImageBlock has an image: dragSource: { canDrag() { return !!this.props.image; }, beginDrag() { return { item: this.props.image }; } } dropTarget: { acceptDrop(image) { DocumentActionCreators.setImage(this.props.blockId, image); } } }); }, render() { return ( <div {...this.dropTargetFor(ItemTypes.IMAGE)}> {/* Add {...this.dragSourceFor} handlers to a nested node */} {this.props.image && <img src={this.props.image.url} {...this.dragSourceFor(ItemTypes.IMAGE)} /> } </div> ); } ); 

还有什么可能的?

我没有覆盖所有内容,但是可以通过其他几种方式使用这个API:

  • 使用getDragState(type)getDropState(type)来了解拖动是否处于活动状态,并使用它来切换CSS类或属性;
  • 指定dragPreviewImage以使用图像作为拖放占位符(使用ImagePreloaderMixin加载它们);
  • 说,我们想让ImageBlocks重新订购。 我们只需要它们为ItemTypes.BLOCK实现dropTargetdragSource
  • 假设我们添加其他种类的块。 我们可以将它们的重新sorting逻辑重新放置在一个混合中。
  • dropTargetFor(...types)允许一次指定几种types,所以一个拖放区可以捕获许多不同的types。
  • 当你需要更细粒度的控制时,大多数方法都会传递拖动事件,导致它们作为最后一个参数。

有关最新的文档和安装说明,请前往Github上的react-dnd repo

Jared Forsyth的回答是非常错误和过时的。 它遵循一整套反模式,如stopPropagation使用, 从道具的状态初始化 ,jQuery的使用,状态的嵌套对象和一些奇怪的dragging状态字段。 如果被重写,解决scheme将如下,但它仍然强制每个鼠标移动滴答虚拟DOM协调,并不是非常高性能。

 const Draggable = React.createClass({ getInitialState() { return { relX: 0, relY: 0 }; }, onMouseDown(e) { if (e.button !== 0) return; const ref = ReactDOM.findDOMNode(this.refs.handle); const body = document.body; const box = ref.getBoundingClientRect(); this.setState({ relX: e.pageX - (box.left + body.scrollLeft - body.clientLeft), relY: e.pageY - (box.top + body.scrollTop - body.clientTop) }); document.addEventListener('mousemove', this.onMouseMove); document.addEventListener('mouseup', this.onMouseUp); e.preventDefault(); }, onMouseUp(e) { document.removeEventListener('mousemove', this.onMouseMove); document.removeEventListener('mouseup', this.onMouseUp); e.preventDefault(); }, onMouseMove(e) { this.props.onMove({ x: e.pageX - this.state.relX, y: e.pageY - this.state.relY }); e.preventDefault(); }, render() { return <div onMouseDown={this.onMouseDown} style={{ position: 'absolute', left: this.props.x, top: this.props.y }} ref="handle" >{this.props.children}</div>; } }) const Test = React.createClass({ getInitialState() { return {x: 100, y: 200}; }, render() { const {x, y} = this.state; return <Draggable x={x} y={y} onMove={this.move}> Drag me </Draggable>; }, move(e) { this.setState(e); } }); ReactDOM.render(<Test />, document.getElementById('container')); 

JSFiddle示例。

react-draggable也很容易使用。 Github上:

https://github.com/mzabriskie/react-draggable

 import React, {Component} from 'react'; import ReactDOM from 'react-dom'; import Draggable from 'react-draggable'; var App = React.createClass({ render() { return ( <div> <h1>Testing Draggable Windows!</h1> <Draggable handle="strong"> <div className="box no-cursor"> <strong className="cursor">Drag Here</strong> <div>You must click my handle to drag me</div> </div> </Draggable> </div> ); } }); ReactDOM.render( <App />, document.getElementById('content') ); 

和我的index.html:

 <html> <head> <title>Testing Draggable Windows</title> <link rel="stylesheet" type="text/css" href="style.css" /> </head> <body> <div id="content"></div> <script type="text/javascript" src="bundle.js" charset="utf-8"></script> <script src="http://localhost:8080/webpack-dev-server.js"></script> </body> </html> 

你需要他们的风格,这是短暂的,或者你没有得到相当预期的行为。 我比其他一些可能的select更喜欢这种行为,但也有一些叫做“可反复 – 可移动”的东西。 我正在尝试使用可拖动的方式resize,但目前为止没有任何乐趣。

我想添加第三个场景

移动的位置不以任何方式保存。 把它看作是一个鼠标移动 – 你的光标不是一个React组件,对吧?

你所要做的就是为你的组件添加一个像“draggable”这样的道具,以及一个将会操纵dom的拖动事件stream。

 setXandY: function(event) { // DOM Manipulation of x and y on your node }, componentDidMount: function() { if(this.props.draggable) { var node = this.getDOMNode(); dragStream(node).onValue(this.setXandY); //baconjs stream }; }, 

在这种情况下,DOM操作是一个优雅的事情(我从来没有想过我会这样说)