我想做一个可拖动的(即可以通过鼠标重新定位)React 组件,它似乎必然涉及全局状态和分散的事件处理程序。我可以用肮脏的方式来做,在我的 JS 文件中使用一个全局变量,甚至可以将它包装在一个漂亮的闭包接口中,但我想知道是否有一种方法可以更好地与 React 啮合。
此外,由于我以前从未在原始 JavaScript 中这样做过,我想看看专家是如何做到的,以确保我已经处理了所有极端情况,尤其是与 React 相关的情况。
谢谢。
我想做一个可拖动的(即可以通过鼠标重新定位)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))
}
})
“谁应该拥有什么状态”从一开始就是一个需要回答的重要问题。在“可拖动”组件的情况下,我可以看到一些不同的场景。
父级应该拥有可拖动的当前位置。在这种情况下,可拖动对象仍将拥有“我正在拖动”状态,但this.props.onChange(x, y)
只要发生 mousemove 事件就会调用。
父级只需要拥有“非移动位置”,因此可拖动对象将拥有它的“拖动位置”,但 onmouseup 它将调用this.props.onChange(x, y)
并将最终决定推迟到父级。如果父级不喜欢可拖动对象的最终位置,它就不会更新它的状态,并且可拖动对象会在拖动之前“弹回”到它的初始位置。
@ssorallen 指出,因为“可拖动”更多的是属性而不是事物本身,所以它可能更好地用作混合。我对 mixins 的经验是有限的,所以我还没有看到它们在复杂的情况下如何提供帮助或阻碍。这可能是最好的选择。
我实现了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
for ItemTypes.IMAGE
.
AdragSource
只是一个指定拖动源如何工作的对象。您必须实现beginDrag
返回代表您正在拖动的数据的项目,以及一些调整拖动 UI 的选项(可选)。您可以选择实现canDrag
禁止拖动,或endDrag(didDrop)
在发生(或未发生)放置时执行某些逻辑。您可以通过让共享的 mixindragSource
为它们生成组件来在组件之间共享此逻辑。
最后,您必须使用{...this.dragSourceFor(itemType)}
一些(一个或多个)元素render
来附加拖动处理程序。这意味着您可以在一个元素中拥有多个“拖动手柄”,它们甚至可能对应于不同的项目类型。(如果您不熟悉JSX 传播属性语法,请查看)。
假设我们想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)
andgetDropState(type)
了解拖动是否处于活动状态,并使用它来切换 CSS 类或属性;dragPreview
将Image
图像用作拖动占位符(用于ImagePreloaderMixin
加载它们);ImageBlocks
排序。我们只需要他们实现dropTarget
和dragSource
for ItemTypes.BLOCK
。dropTargetFor(...types)
允许一次指定多种类型,因此一个放置区可以捕获许多不同的类型。有关最新的文档和安装说明,请前往Github 上的 react-dnd 存储库。
@codewithfeeling 的答案非常错误,并且滞后于您的页面!这是他的代码的一个版本,其中修复和注释了问题。这应该是现在最新的基于钩子的答案。
import React, { useRef, useState, useEffect, useCallback } from "react";
/// throttle.ts
export 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;
};
/// use-draggable.ts
const id = (x) => x;
// complex logic should be a hook, not a component
const useDraggable = ({ onDrag = id } = {}) => {
// this state doesn't change often, so it's fine
const [pressed, setPressed] = useState(false);
// do not store position in useState! even if you useEffect on
// it and update `transform` CSS property, React still rerenders
// on every state change, and it LAGS
const position = useRef({ x: 0, y: 0 });
const ref = useRef();
// we've moved the code into the hook, and it would be weird to
// return `ref` and `handleMouseDown` to be set on the same element
// why not just do the job on our own here and use a function-ref
// to subscribe to `mousedown` too? it would go like this:
const unsubscribe = useRef();
const legacyRef = useCallback((elem) => {
// in a production version of this code I'd use a
// `useComposeRef` hook to compose function-ref and object-ref
// into one ref, and then would return it. combining
// hooks in this way by hand is error-prone
// then I'd also split out the rest of this function into a
// separate hook to be called like this:
// const legacyRef = useDomEvent('mousedown');
// const combinedRef = useCombinedRef(ref, legacyRef);
// return [combinedRef, pressed];
ref.current = elem;
if (unsubscribe.current) {
unsubscribe.current();
}
if (!elem) {
return;
}
const handleMouseDown = (e) => {
// don't forget to disable text selection during drag and drop
// operations
e.target.style.userSelect = "none";
setPressed(true);
};
elem.addEventListener("mousedown", handleMouseDown);
unsubscribe.current = () => {
elem.removeEventListener("mousedown", handleMouseDown);
};
}, []);
// useEffect(() => {
// return () => {
// // this shouldn't really happen if React properly calls
// // function-refs, but I'm not proficient enough to know
// // for sure, and you might get a memory leak out of it
// if (unsubscribe.current) {
// unsubscribe.current();
// }
// };
// }, []);
useEffect(() => {
// why subscribe in a `useEffect`? because we want to subscribe
// to mousemove only when pressed, otherwise it will lag even
// when you're not dragging
if (!pressed) {
return;
}
// updating the page without any throttling is a bad idea
// requestAnimationFrame-based throttle would probably be fine,
// but be aware that naive implementation might make element
// lag 1 frame behind cursor, and it will appear to be lagging
// even at 60 FPS
const handleMouseMove = throttle((event) => {
// needed for TypeScript anyway
if (!ref.current || !position.current) {
return;
}
const pos = position.current;
// it's important to save it into variable here,
// otherwise we might capture reference to an element
// that was long gone. not really sure what's correct
// behavior for a case when you've been scrolling, and
// the target element was replaced. probably some formulae
// needed to handle that case. TODO
const elem = ref.current;
position.current = onDrag({
x: pos.x + event.movementX,
y: pos.y + event.movementY
});
elem.style.transform = `translate(${pos.x}px, ${pos.y}px)`;
});
const handleMouseUp = (e) => {
e.target.style.userSelect = "auto";
setPressed(false);
};
// subscribe to mousemove and mouseup on document, otherwise you
// can escape bounds of element while dragging and get stuck
// dragging it forever
document.addEventListener("mousemove", handleMouseMove);
document.addEventListener("mouseup", handleMouseUp);
return () => {
handleMouseMove.cancel();
document.removeEventListener("mousemove", handleMouseMove);
document.removeEventListener("mouseup", handleMouseUp);
};
// if `onDrag` wasn't defined with `useCallback`, we'd have to
// resubscribe to 2 DOM events here, not to say it would mess
// with `throttle` and reset its internal timer
}, [pressed, onDrag]);
// actually it makes sense to return an array only when
// you expect that on the caller side all of the fields
// will be usually renamed
return [legacyRef, pressed];
// > seems the best of them all to me
// this code doesn't look pretty anymore, huh?
};
/// example.ts
const quickAndDirtyStyle = {
width: "200px",
height: "200px",
background: "#FF9900",
color: "#FFFFFF",
display: "flex",
justifyContent: "center",
alignItems: "center"
};
const DraggableComponent = () => {
// handlers must be wrapped into `useCallback`. even though
// resubscribing to `mousedown` on every tick is quite cheap
// due to React's event system, `handleMouseDown` might be used
// in `deps` argument of another hook, where it would really matter.
// as you never know where return values of your hook might end up,
// it's just generally a good idea to ALWAYS use `useCallback`
// it's nice to have a way to at least prevent element from
// getting dragged out of the page
const handleDrag = useCallback(
({ x, y }) => ({
x: Math.max(0, x),
y: Math.max(0, y)
}),
[]
);
const [ref, pressed] = useDraggable({
onDrag: handleDrag
});
return (
<div ref={ref} style={quickAndDirtyStyle}>
<p>{pressed ? "Dragging..." : "Press to drag"}</p>
</div>
);
};
请在此处查看此代码,这是一个改进了光标定位的版本,此处有约束onDrag
,这里有硬核钩子展示。
此答案的预挂钩版本:
Jared Forsyth 的回答是非常错误和过时的。它遵循一整套反模式,例如 的用法stopPropagation
、从 props 初始化状态、jQuery 的用法、状态中的嵌套对象以及一些奇怪的dragging
状态字段。如果被重写,解决方案将如下所示,但它仍然会在每次鼠标移动滴答时强制进行虚拟 DOM 协调,并且性能不是很好。
UPD。我的回答是非常错误和过时的。现在代码通过使用原生事件处理程序和样式更新来缓解 React 组件生命周期缓慢的问题,使用transform
它不会导致回流,并通过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;
}
这是使用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
我已将 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;
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>
您需要他们的样式,它很短,或者您没有得到完全预期的行为。与其他一些可能的选择相比,我更喜欢这种行为,但还有一种叫做react-resizable-and-movable的东西。我正在尝试使用可拖动来调整大小,但到目前为止还没有任何乐趣。
这是带有 Hook 的 2020 年答案:
function useDragging() {
const [isDragging, setIsDragging] = useState(false);
const [pos, setPos] = useState({ x: 0, y: 0 });
const ref = useRef(null);
function onMouseMove(e) {
if (!isDragging) return;
setPos({
x: e.x - ref.current.offsetWidth / 2,
y: e.y - ref.current.offsetHeight / 2,
});
e.stopPropagation();
e.preventDefault();
}
function onMouseUp(e) {
setIsDragging(false);
e.stopPropagation();
e.preventDefault();
}
function onMouseDown(e) {
if (e.button !== 0) return;
setIsDragging(true);
setPos({
x: e.x - ref.current.offsetWidth / 2,
y: e.y - ref.current.offsetHeight / 2,
});
e.stopPropagation();
e.preventDefault();
}
// When the element mounts, attach an mousedown listener
useEffect(() => {
ref.current.addEventListener("mousedown", onMouseDown);
return () => {
ref.current.removeEventListener("mousedown", onMouseDown);
};
}, [ref.current]);
// Everytime the isDragging state changes, assign or remove
// the corresponding mousemove and mouseup handlers
useEffect(() => {
if (isDragging) {
document.addEventListener("mouseup", onMouseUp);
document.addEventListener("mousemove", onMouseMove);
} else {
document.removeEventListener("mouseup", onMouseUp);
document.removeEventListener("mousemove", onMouseMove);
}
return () => {
document.removeEventListener("mouseup", onMouseUp);
document.removeEventListener("mousemove", onMouseMove);
};
}, [isDragging]);
return [ref, pos.x, pos.y, isDragging];
}
然后是一个使用钩子的组件:
function Draggable() {
const [ref, x, y, isDragging] = useDragging();
return (
<div
ref={ref}
style={{
position: "absolute",
width: 50,
height: 50,
background: isDragging ? "blue" : "gray",
left: x,
top: y,
}}
></div>
);
}
我想添加第三个场景
移动位置不会以任何方式保存。把它想象成一个鼠标移动——你的光标不是一个 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 操作是一件优雅的事情(我从没想过我会这么说)
我已经使用 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;
这是另一个简单的 React hooks 解决方案,没有任何第三方库,基于 codewithfeeling 和 Evan Conrad 的解决方案。 https://stackoverflow.com/a/63887486/1309218 https://stackoverflow.com/a/61667523/1309218
import React, { useCallback, useRef, useState } from "react";
import styled, { css } from "styled-components/macro";
const Component: React.FC = () => {
const [position, setPosition] = useState({ x: 0, y: 0 });
const elementRef = useRef<HTMLDivElement>(null);
const onMouseDown = useCallback(
(event) => {
const onMouseMove = (event: MouseEvent) => {
position.x += event.movementX;
position.y += event.movementY;
const element = elementRef.current;
if (element) {
element.style.transform = `translate(${position.x}px, ${position.y}px)`;
}
setPosition(position);
};
const onMouseUp = () => {
document.removeEventListener("mousemove", onMouseMove);
document.removeEventListener("mouseup", onMouseUp);
};
document.addEventListener("mousemove", onMouseMove);
document.addEventListener("mouseup", onMouseUp);
},
[position, setPosition, elementRef]
);
return (
<Container>
<DraggableItem ref={elementRef} onMouseDown={onMouseDown}>
</DraggableItem>
</Container>
);
};
const Container = styled.div`
position: absolute;
width: 100%;
height: 100%;
top: 0;
left: 0;
overflow: hidden;
`;
const DraggableItem = styled.div`
position: absolute;
z-index: 1;
left: 20px;
top: 20px;
width: 100px;
height: 100px;
background-color: green;
`;
这是一个使用反应功能的可拖动 div 示例(已测试)
function Draggable() {
const startX = 300;
const startY = 200;
const [pos, setPos] = useState({ left: startX , top: startY });
const [isDragging, setDragging] = useState(false);
const isDraggingRef = React.useRef(isDragging);
const setDraggingState = (data) => {
isDraggingRef.current = data;
setDragging(data);
};
function onMouseDown(e) {
setDraggingState(true);
e.stopPropagation();
e.preventDefault();
}
function onMouseMove(e) {
if (isDraggingRef.current) {
const rect = e.target.parentNode.getBoundingClientRect();
let newLeft = e.pageX - rect.left - 20;
let newTop = e.pageY - rect.top - 20;
if (
newLeft > 0 &&
newTop > 0 &&
newLeft < rect.width &&
newTop < rect.height
) {
setPos({
left: newLeft,
top: newTop,
});
} else setDraggingState(false);
}
e.stopPropagation();
e.preventDefault();
}
function onMouseUp(e) {
setDraggingState(false);
e.stopPropagation();
e.preventDefault();
}
useEffect(() => {
document.addEventListener("mousemove", onMouseMove);
document.addEventListener("mouseup", onMouseUp);
}, []);
useEffect(() => {
console.log(pos)
}, [pos]);
return <div style={pos} className="draggableDiv" onMouseDown={onMouseDown}></div>;
}
详细阐述埃文康拉德的回答(https://stackoverflow.com/a/63887486/1531141)我来到了这种打字稿方法:
import { RefObject, useEffect, useRef, useState } from "react";
export enum DraggingState {
undefined = -1,
starts = 0,
moves = 1,
finished = 2
}
export default function useDragging() {
const [state, setState] = useState(DraggingState.undefined);
const [point, setPoint] = useState({x: 0, y: 0}); // point of cursor in relation to the element's parent
const [elementOffset, setElementOffset] = useState({x: 0, y: 0}); // offset of element in relation to it's parent
const [touchOffset, setTouchOffset] = useState({x: 0, y: 0}); // offset of mouse down point in relation to the element
const ref = useRef() as RefObject<HTMLDivElement>;
// shows active state of dragging
const isDragging = () => {
return (state === DraggingState.starts) || (state === DraggingState.moves);
}
function onMouseDown(e: MouseEvent) {
const parentElement = ref.current?.offsetParent as HTMLElement;
if (e.button !== 0 || !ref.current || !parentElement) return;
// First entry to the flow.
// We save touchOffset value as parentElement's offset
// to calculate element's offset on the move.
setPoint({
x: e.x - parentElement.offsetLeft,
y: e.y - parentElement.offsetTop
});
setElementOffset({
x: ref.current.offsetLeft,
y: ref.current.offsetTop
});
setTouchOffset({
x: e.x - parentElement.offsetLeft - ref.current.offsetLeft,
y: e.y - parentElement.offsetTop - ref.current.offsetTop
});
setState(DraggingState.starts);
}
function onMouseMove(e: MouseEvent) {
const parentElement = ref.current?.offsetParent as HTMLElement;
if (!isDragging() || !ref.current || !parentElement) return;
setState(DraggingState.moves);
setPoint({
x: e.x - parentElement.offsetLeft,
y: e.y - parentElement.offsetTop
});
setElementOffset({
x: e.x - touchOffset.x - parentElement.offsetLeft,
y: e.y - touchOffset.y - parentElement.offsetTop
});
}
function onMouseUp(e: MouseEvent) {
// ends up the flow by setting the state
setState(DraggingState.finished);
}
function onClick(e: MouseEvent) {
// that's a fix for touch pads that transfer touches to click,
// e.g "Tap to click" on macos. When enabled, on tap mouseDown is fired,
// but mouseUp isn't. In this case we invoke mouseUp manually, to trigger
// finishing state;
setState(DraggingState.finished);
}
// When the element mounts, attach an mousedown listener
useEffect(() => {
const element = ref.current;
element?.addEventListener("mousedown", onMouseDown);
return () => {
element?.removeEventListener("mousedown", onMouseDown);
};
}, [ref.current]);
// Everytime the state changes, assign or remove
// the corresponding mousemove, mouseup and click handlers
useEffect(() => {
if (isDragging()) {
document.addEventListener("mouseup", onMouseUp);
document.addEventListener("mousemove", onMouseMove);
document.addEventListener("click", onClick);
} else {
document.removeEventListener("mouseup", onMouseUp);
document.removeEventListener("mousemove", onMouseMove);
document.removeEventListener("click", onClick);
}
return () => {
document.removeEventListener("mouseup", onMouseUp);
document.removeEventListener("mousemove", onMouseMove);
document.removeEventListener("click", onClick);
};
}, [state]);
return {
ref: ref,
state: state,
point: point,
elementOffset: elementOffset,
touchOffset: touchOffset
}
}
还添加了 onClick 处理程序,就像在触摸板上启用了点击选项一样 onClick 和 mouseDown 在同一时刻发生,但 mouseUp 永远不会被触发来关闭手势。
此外,此挂钩返回三对坐标 - 元素到其父级的偏移量,元素内的抓取点和元素父级内的点。详情见代码内注释;
像这样使用:
const dragging = useDragging();
const ref = dragging.ref;
const style: CSSProperties = {
marginLeft: dragging.elementOffset.x,
marginTop: dragging.elementOffset.y,
border: "1px dashed red"
}
return (<div ref={ref} style={style}>
{dragging.state === DraggingState.moves ? "is dragging" : "not dragging"}
</div>)