我想制作一个可拖动的(也就是说,可以通过鼠标重新定位)React组件,它似乎必然涉及全局状态和分散的事件处理程序.我可以用脏的方式,在我的JS文件中使用全局变量,甚至可以将它包装在一个漂亮的闭包界面中,但我想知道是否有一种方法可以更好地与React相结合.
此外,由于我之前从未在原始JavaScript中完成此操作,因此我想看看专家是如何做到这一点的,以确保我已经处理了所有角落案例,特别是与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)) } })
"谁应该拥有什么样的国家"是一个重要的问题,从一开始就要回答.在"可拖动"组件的情况下,我可以看到一些不同的场景.
父母应该拥有可拖动的当前位置.在这种情况下,draggable仍将拥有"我在拖动"状态,但this.props.onChange(x, y)
只要发生mousemove事件就会调用.
父母只需要拥有"非移动位置",因此可拖动将拥有它的"拖动位置",但是,它会调用this.props.onChange(x, y)
并将最终决定推迟到父级.如果父级不喜欢可拖动的最终位置,则它不会更新它的状态,并且拖动将在拖动之前"快速"回到它的初始位置.
@ssorallen指出,因为"draggable"本身就是一个属性,而不是一个东西,它可能更适合作为mixin.我对mixin的经验是有限的,所以我还没有看到他们在复杂的情况下如何帮助或妨碍他们.这可能是最好的选择.
Jared Forsyth的回答是非常错误和过时的.它遵循一整套反模式,例如使用stopPropagation
,从道具初始化状态,jQuery的使用,状态中的嵌套对象以及具有一些奇怪的dragging
状态字段.如果被重写,解决方案将是以下,但它仍然强制虚拟DOM协调每次鼠标移动滴答,并不是非常高效.
UPD.我的回答是可怕的错误和过时的.现在,代码通过使用本机事件处理程序和样式更新缓解了React组件生命周期缓慢的问题,transform
因为它不会导致重排,并且会限制DOM的更改requestAnimationFrame
.现在,在我尝试的每个浏览器中,它始终为60 FPS.
const throttle = (f) => { let token = null, lastArgs = null; const invoke = () => { f(...lastArgs); token = null; }; const result = (...args) => { lastArgs = args; if (!token) { token = requestAnimationFrame(invoke); } }; result.cancel = () => token && cancelAnimationFrame(token); return result; }; class Draggable extends React.PureComponent { _relX = 0; _relY = 0; _ref = React.createRef(); _onMouseDown = (event) => { if (event.button !== 0) { return; } const {scrollLeft, scrollTop, clientLeft, clientTop} = document.body; // Try to avoid calling `getBoundingClientRect` if you know the size // of the moving element from the beginning. It forces reflow and is // the laggiest part of the code right now. Luckily it's called only // once per click. const {left, top} = this._ref.current.getBoundingClientRect(); this._relX = event.pageX - (left + scrollLeft - clientLeft); this._relY = event.pageY - (top + scrollTop - clientTop); document.addEventListener('mousemove', this._onMouseMove); document.addEventListener('mouseup', this._onMouseUp); event.preventDefault(); }; _onMouseUp = (event) => { document.removeEventListener('mousemove', this._onMouseMove); document.removeEventListener('mouseup', this._onMouseUp); event.preventDefault(); }; _onMouseMove = (event) => { this.props.onMove( event.pageX - this._relX, event.pageY - this._relY, ); event.preventDefault(); }; _update = throttle(() => { const {x, y} = this.props; this._ref.current.style.transform = `translate(${x}px, ${y}px)`; }); componentDidMount() { this._ref.current.addEventListener('mousedown', this._onMouseDown); this._update(); } componentDidUpdate() { this._update(); } componentWillUnmount() { this._ref.current.removeEventListener('mousedown', this._onMouseDown); this._update.cancel(); } render() { return ( <div className="draggable" ref={this._ref}> {this.props.children} </div> ); } } class Test extends React.PureComponent { state = { x: 100, y: 200, }; _move = (x, y) => this.setState({x, y}); // you can implement grid snapping logic or whatever here /* _move = (x, y) => this.setState({ x: ~~((x - 5) / 10) * 10 + 5, y: ~~((y - 5) / 10) * 10 + 5, }); */ render() { const {x, y} = this.state; return ( <Draggable x={x} y={y} onMove={this._move}> Drag me </Draggable> ); } } ReactDOM.render( <Test />, document.getElementById('container'), );
还有一点CSS
.draggable { /* just to size it to content */ display: inline-block; /* opaque background is important for performance */ background: white; /* avoid selecting text while dragging */ user-select: none; }
关于JSFiddle的例子.
反应可拖动也很容易使用.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>
你需要他们的风格,这很短,或者你没有达到预期的行为.我比其他一些可能的选择更喜欢这种行为,但也有一种叫做反应可重复和可移动的东西.我正试图调整大小与可拖动工作,但到目前为止没有喜悦.
我实现了react-dnd,一个灵活的HTML5拖放混合,用于React和完整的DOM控件.
现有的拖放库不适合我的用例,所以我写了自己的.它类似于我们在Stampsy.com上运行了大约一年的代码,但改写后利用了React和Flux.
我的主要要求:
发出自己的零DOM或CSS,将其留给消费组件;
在消耗组件上施加尽可能少的结构;
使用HTML5拖放作为主要后端,但可以在将来添加不同的后端;
与原始HTML5 API一样,强调拖动数据而不仅仅是"可拖动视图";
隐藏消费代码中的HTML5 API怪癖;
对于不同类型的数据,不同的组件可以是"拖动源"或"丢弃目标";
允许一个组件包含多个拖动源并在需要时放下目标;
如果拖动或悬停兼容数据,则可以轻松删除目标以更改其外观;
可以轻松使用图像拖动缩略图而不是元素屏幕截图,避免浏览器怪癖.
如果这些听起来很熟悉,请继续阅读.
首先,声明可以拖动的数据类型.
这些用于检查拖动源和放置目标的"兼容性":
// ItemTypes.js module.exports = { BLOCK: 'block', IMAGE: 'image' };
(如果您没有多种数据类型,则此库可能不适合您.)
然后,让我们创建一个非常简单的可拖动组件,当拖动时,它代表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
该组件的拖放行为.可拖动和可放置的组件都使用相同的mixin.
在内部configureDragDrop
,我们需要调用组件支持的registerType
每个自定义ItemTypes
.例如,您的应用中可能会有多个图像表示形式,每个图像都会提供一个dragSource
for ItemTypes.IMAGE
.
A dragSource
只是一个指定拖动源如何工作的对象.您必须实现beginDrag
以返回表示您正在拖动的数据的项目,以及可选的一些调整拖动UI的选项.您可以选择实现canDrag
禁止拖动,或者endDrag(didDrop)
在丢弃(或尚未发生)时执行某些逻辑.您可以通过让共享mixin dragSource
为它们生成来在组件之间共享此逻辑.
最后,您必须使用{...this.dragSourceFor(itemType)}
某些(一个或多个)元素render
来附加拖动处理程序.这意味着您可以在一个元素中包含多个"拖动句柄",它们甚至可以对应于不同的项类型.(如果您不熟悉JSX Spread Attributes语法,请查看).
假设我们希望ImageBlock
成为IMAGE
s 的下降目标.除了我们需要提供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类或属性;
指定dragPreview
将Image
图像用作拖动占位符(用于ImagePreloaderMixin
加载它们);
说,我们想要重新制作ImageBlocks
.我们只需要他们实施dropTarget
和dragSource
进行ItemTypes.BLOCK
.
假设我们添加其他类型的块.我们可以通过将它们放在mixin中来重用它们的重新排序逻辑.
dropTargetFor(...types)
允许一次指定多个类型,因此一个放置区可以捕获许多不同的类型.
当您需要更细粒度的控件时,大多数方法都会传递拖动事件,导致它们成为最后一个参数.
有关最新文档和安装说明,请访问Github上的react-dnd repo.
我已经将polovnikov.ph解决方案更新到React 16/ES6,其增强功能包括触摸处理和捕捉到网格,这是我需要的游戏.捕捉到网格可以缓解性能问题.
import React from 'react'; import ReactDOM from 'react-dom'; import PropTypes from 'prop-types'; class Draggable extends React.Component { constructor(props) { super(props); this.state = { relX: 0, relY: 0, x: props.x, y: props.y }; this.gridX = props.gridX || 1; this.gridY = props.gridY || 1; this.onMouseDown = this.onMouseDown.bind(this); this.onMouseMove = this.onMouseMove.bind(this); this.onMouseUp = this.onMouseUp.bind(this); this.onTouchStart = this.onTouchStart.bind(this); this.onTouchMove = this.onTouchMove.bind(this); this.onTouchEnd = this.onTouchEnd.bind(this); } static propTypes = { onMove: PropTypes.func, onStop: PropTypes.func, x: PropTypes.number.isRequired, y: PropTypes.number.isRequired, gridX: PropTypes.number, gridY: PropTypes.number }; onStart(e) { const ref = ReactDOM.findDOMNode(this.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) }); } onMove(e) { const x = Math.trunc((e.pageX - this.state.relX) / this.gridX) * this.gridX; const y = Math.trunc((e.pageY - this.state.relY) / this.gridY) * this.gridY; if (x !== this.state.x || y !== this.state.y) { this.setState({ x, y }); this.props.onMove && this.props.onMove(this.state.x, this.state.y); } } onMouseDown(e) { if (e.button !== 0) return; this.onStart(e); document.addEventListener('mousemove', this.onMouseMove); document.addEventListener('mouseup', this.onMouseUp); e.preventDefault(); } onMouseUp(e) { document.removeEventListener('mousemove', this.onMouseMove); document.removeEventListener('mouseup', this.onMouseUp); this.props.onStop && this.props.onStop(this.state.x, this.state.y); e.preventDefault(); } onMouseMove(e) { this.onMove(e); e.preventDefault(); } onTouchStart(e) { this.onStart(e.touches[0]); document.addEventListener('touchmove', this.onTouchMove, {passive: false}); document.addEventListener('touchend', this.onTouchEnd, {passive: false}); e.preventDefault(); } onTouchMove(e) { this.onMove(e.touches[0]); e.preventDefault(); } onTouchEnd(e) { document.removeEventListener('touchmove', this.onTouchMove); document.removeEventListener('touchend', this.onTouchEnd); this.props.onStop && this.props.onStop(this.state.x, this.state.y); e.preventDefault(); } render() { return <div onMouseDown={this.onMouseDown} onTouchStart={this.onTouchStart} style={{ position: 'absolute', left: this.state.x, top: this.state.y, touchAction: 'none' }} ref={(div) => { this.handle = div; }} > {this.props.children} </div>; } } export default Draggable;