我想制作一个可拖动(即可以通过鼠标重定位)的React组件,该组件似乎必然涉及全局状态和分散的事件处理程序。我可以在JS文件中使用一个全局变量来以肮脏的方式进行操作,甚至可以将其包装在一个不错的闭包接口中,但是我想知道是否有一种方法可以更好地与React结合。
另外,由于我以前从未在原始JavaScript中完成过此操作,所以我想看看专家是如何做到的,以确保我处理了所有极端情况,尤其是与React相关的情况。
谢谢。
我想制作一个可拖动(即可以通过鼠标重定位)的React组件,该组件似乎必然涉及全局状态和分散的事件处理程序。我可以在JS文件中使用一个全局变量来以肮脏的方式进行操作,甚至可以将其包装在一个不错的闭包接口中,但是我想知道是否有一种方法可以更好地与React结合。
另外,由于我以前从未在原始JavaScript中完成过此操作,所以我想看看专家是如何做到的,以确保我处理了所有极端情况,尤其是与React相关的情况。
谢谢。
Answers:
我可能应该把它变成一篇博客文章,但这是一个很可靠的例子。
这些评论应该可以很好地说明问题,但是如果您有任何疑问,请告诉我。
这是玩的小提琴: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))
}
})
从一开始,“谁应该拥有什么状态”是一个重要的问题。在“可拖动”组件的情况下,我可以看到一些不同的情况。
父级应拥有可拖动对象的当前位置。在这种情况下,可拖动对象仍将拥有“我正在拖动”状态,但是this.props.onChange(x, y)
每当发生mousemove事件时都会调用。
父级只需要拥有“固定位置”,因此可拖动对象将拥有其“拖动位置”,但是在onmouseup上它将调用this.props.onChange(x, y)
并将最终决定推迟到父级。如果父级不喜欢可拖动对象的最终位置,它将不会更新其状态,并且可拖动对象会在拖动之前“快速恢复”到其初始位置。
@ssorallen指出,由于“可拖动”与其说是事物本身,不如说是一个属性,它可能更好地用作混合。我对mixin的经验有限,因此我还没有看到它们在复杂情况下如何提供帮助或妨碍。这可能是最好的选择。
var computedStyle = window.getComputedStyle(this.getDOMNode()); pos = { top: parseInt(computedStyle.top), left: parseInt(computedStyle.left) };
如果您将jquery与react一起使用,则可能做错了;)如果您需要一些jquery插件,我发现它通常更容易,用更少的代码将其重写为纯react。
this.getDOMNode().getBoundingClientRect()
-getComputedStyle可以输出任何有效的CSS属性,包括auto
在这种情况下,上面的代码将导致NaN
。见MDN文章:developer.mozilla.org/en-US/docs/Web/API/Element/...
this.getDOMNode()
,不推荐使用。使用引用获取dom节点。 facebook.github.io/react/docs/…–
我实现了react-dnd,这是一个用于React的灵活HTML5拖放混入,具有完整的DOM控制。
现有的拖放库不适合我的用例,因此我编写了自己的库。它类似于我们在Stampsy.com上运行了大约一年的代码,但是为了利用React和Flux进行了重写。
我的主要要求:
如果您觉得这些听起来很熟悉,请继续阅读。
首先,声明可以拖动的数据类型。
这些用于检查拖动源和放置目标的“兼容性”:
// 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
供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
。dropTargetFor(...types)
允许一次指定几种类型,因此一个放置区可以捕获许多不同的类型。有关最新文档和安装说明,请访问Github上的react-dnd repo。
贾里德·福赛斯(Jared Forsyth)的答案是完全错误和过时的。它遵循一整套反模式,例如使用stopPropagation
,从props初始化状态,使用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;
}
我已经将polkovnikov.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;
可反应的易用性也易于使用。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-resizable-and-movable的东西。我正在尝试使用可拖动的控件调整大小,但到目前为止没有任何乐趣。
下面是一个简单现代的方法来此与useState
,useEffect
并useRef
在ES6。
import React, { useRef, useState, useEffect } from 'react'
const quickAndDirtyStyle = {
width: "200px",
height: "200px",
background: "#FF9900",
color: "#FFFFFF",
display: "flex",
justifyContent: "center",
alignItems: "center"
}
const DraggableComponent = () => {
const [pressed, setPressed] = useState(false)
const [position, setPosition] = useState({x: 0, y: 0})
const ref = useRef()
// Monitor changes to position state and update DOM
useEffect(() => {
if (ref.current) {
ref.current.style.transform = `translate(${position.x}px, ${position.y}px)`
}
}, [position])
// Update the current position if mouse is down
const onMouseMove = (event) => {
if (pressed) {
setPosition({
x: position.x + event.movementX,
y: position.y + event.movementY
})
}
}
return (
<div
ref={ ref }
style={ quickAndDirtyStyle }
onMouseMove={ onMouseMove }
onMouseDown={ () => setPressed(true) }
onMouseUp={ () => setPressed(false) }>
<p>{ pressed ? "Dragging..." : "Press to drag" }</p>
</div>
)
}
export default DraggableComponent
我想添加第三个场景
移动位置不会以任何方式保存。将其视为鼠标移动-您的光标不是React组件,对吗?
您要做的就是在组件中添加诸如“可拖动”之类的道具,以及将操纵dom的一系列拖动事件。
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操作是一件很优雅的事情(我从没想过要这么说)
setXandY
用虚构的分量填充函数?
我已经使用refs更新了该类,因为我在这里看到的所有解决方案都不再支持或即将折旧的东西,例如ReactDOM.findDOMNode
。可以是子组件或一组子组件的父:)
import React, { Component } from 'react';
class Draggable extends Component {
constructor(props) {
super(props);
this.myRef = React.createRef();
this.state = {
counter: this.props.counter,
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(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 = (e) => {
if (e.button !== 0) return;
let pos = { left: this.myRef.current.offsetLeft, top: this.myRef.current.offsetTop }
this.setState({
dragging: true,
rel: {
x: e.pageX - pos.left,
y: e.pageY - pos.top
}
});
e.stopPropagation();
e.preventDefault();
}
onMouseUp = (e) => {
this.setState({ dragging: false });
e.stopPropagation();
e.preventDefault();
}
onMouseMove = (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() {
return (
<span ref={this.myRef} onMouseDown={this.onMouseDown} style={{ position: 'absolute', left: this.state.pos.x + 'px', top: this.state.pos.y + 'px' }}>
{this.props.children}
</span>
)
}
}
export default Draggable;