反应组件/div 可拖动的推荐方法

我想创建一个可拖动的(即可通过鼠标重新定位) React 组件,它似乎必须包含全局状态和分散的事件处理程序。我可以用肮脏的方法,在我的 JS 文件中使用一个全局变量,甚至可以将它包装在一个很好的闭包接口中,但是我想知道是否有一种方法可以更好地与 React 融合。

此外,由于我以前从未在原始 JavaScript 中做过这个,我想看看专家是如何做到这一点的,以确保我已经处理了所有的角落案例,特别是当它们与 React 相关时。

谢谢。

219136 次浏览

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

评论应该能很好地解释事情,但是如果你有问题请让我知道。

这里有一个小提琴: https://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 of 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

父对象应该拥有可拖动对象的当前位置。在这种情况下,可拖动的程序仍然拥有自己的“ am I being ”状态,但是每当发生 mousemove事件时就会调用 this.props.onChange(x, y)

场景2

父节点只需要拥有“非移动位置”,因此可拖动节点将拥有它的“拖动位置”,但是 onmouseup将调用 this.props.onChange(x, y)并将最终决定推迟到父节点。如果父级不喜欢可拖动的结束位置,它将拒绝状态更新,而可拖动的结束位置将“快速回复”到其初始位置。

混合物还是成分?

@ ssorallen 指出,因为 draggable更多的是一个属性而不是事物本身,所以它可能更适合作为一个 mix in。我使用混合器的经验有限,所以我还没有看到它们在复杂的情况下如何帮助或阻碍。这可能是最好的选择。

我想加一个 第三个场景

移动位置不以任何方式保存。把它想象成一个鼠标移动-你的光标不是一个反应组件,对吗?

所有你做的,是添加一个道具像“拖动”到您的组件和拖动事件的流,将操纵多姆。

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 操作是一件优雅的事情(我从未想过我会这样说)

我实现了 < a href = “ https://github.com/gaearon/response-dnd”> response-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这个组件的拖放行为。可拖放组件和可拖放组件都使用相同的 mix in。

configureDragDrop内部,我们需要为组件支持的每个定制 ItemTypes调用 registerType。例如,您的应用程序中可能有几种图像表示形式,每种形式都为 ItemTypes.IMAGE提供一个 dragSource

dragSource只是一个指定拖动源如何工作的对象。您必须实现 beginDrag来返回表示您正在拖动的数据的项,以及(可选地)一些调整拖动 UI 的选项。您可以选择实现 canDrag来禁止拖动,也可以选择实现 endDrag(didDrop)来在拖放发生(或未发生)时执行某些逻辑。您可以通过让一个共享的 Mixin 为组件生成 dragSource来在组件之间共享这个逻辑。

最后,必须对 render中的一些(一个或多个)元素使用 {...this.dragSourceFor(itemType)}来附加拖动处理程序。这意味着在一个元素中可以有多个“拖动句柄”,它们甚至可以对应不同的项类型。(如果您不熟悉 传播属性语法,请查看它)。

简单降落目标

假设我们希望 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
  • 假设我们添加了其他类型的块,我们可以通过将它们放在一个 mix in 中来重用它们的重新排序逻辑。
  • dropTargetFor(...types)允许同时指定几种类型,因此一个拖放区域可以捕捉许多不同的类型。
  • 当需要更细粒度的控制时,大多数方法都会被传递拖动事件,这会导致它们成为最后一个参数。

有关最新的文档和安装说明,请访问 Github 上的 response-dnd repo

返回文章页面可拖动的反应也很容易使用译者:

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>

你需要他们的风格,这是短期的,否则你不会得到完全预期的行为。比起其他可能的选择,我更喜欢这种行为,但是也有一种叫做 反应-可调整大小和可移动的东西。我试图得到调整大小与拖动工作,但没有喜悦,迄今为止。

@ 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 的回答大错特错。现在这一点都不重要,但它仍然在编辑历史中的答案。)

我已经更新了 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;

以下是 ES6中 useStateuseEffectuseRef的一个简单的现代方法。

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

我已经使用参考文献更新了这个类,因为我在这里看到的所有解决方案都有不再受支持的东西,或者像 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;


以下是2020年 Hook 的答案:

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>
);
}

详细阐述 Evan Conrad 的答案(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>)

这里是一个简单的另一个 React hook 解决方案,没有任何第三方库,基于 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 示例(经过测试) ,其中使用了 response 函数

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>;
}

不要使用反应组件和用效果钩来实现拖动容器的功能

下面是基于 React 类的组件 ES6版本—— >

import React from "react";
import $ from 'jquery';
import { useRef } from "react";


class Temp_Class extends React.Component{


constructor(props){
super(props);
this.state = {
pos: {x:0, y:0},
dragging: false,
rel: null
};
this.onMouseDown = this.onMouseDown.bind(this);


this.onMouseMove = this.onMouseMove.bind(this);


this.onMouseUp = this.onMouseUp.bind(this);
}


componentDidUpdate(props, state){
// console.log("Dragging State is ",this.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)
}
}


onMouseDown(e){
console.log("Mouse Down")
if (e.button !== 0) return
var pos = document.getElementById("contianer").getBoundingClientRect();
//   console.log(pos)
this.setState({
dragging: true,
rel: {
x: e.pageX - pos.left,
y: e.pageY - pos.top
}
})
e.stopPropagation()
e.preventDefault()
}


onMouseUp(e) {
console.log("Mouse Up")
this.setState({dragging: false})
e.stopPropagation()
e.preventDefault()
}


onMouseMove(e) {
console.log("Mouse Move")
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()
console.log("Current State is ", this.state)
}


render(){
return (<div id="contianer" style = \{\{
position: 'absolute',
left: this.state.pos.x + 'px',
top: this.state.pos.y + 'px',
cursor: 'pointer',
width: '200px',
height: '200px',
backgroundColor: '#cca',
}} onMouseDown = {this.onMouseDown}>
Lovepreet Singh
</div>);
}
}




export default Temp_Class;

已经有很多答案了,不过我也会附上我的。这个答案的优点如下:

  • 基于钩子的现代解决方案
  • 使用打字稿
  • 动态添加/删除事件以获得额外的性能好处
  • 合理的封装; 也就是说,拖动的位置是相对于其直接的父
  • 对父级的引用是在 Dragable 组件中计算的,不需要传入。
  • 简单,直观的 CSS
  • 拖动位置清楚明确地夹紧到父母的尺寸

import {
CSSProperties,
useEffect,
useRef,
useState,
MouseEvent as r_MouseEvent,
MutableRefObject,
} from 'react';




interface PositionType {
x: number,
y: number,
}




interface MinMaxType {
min: number,
max: number,
}




interface Props {
text: string,
position: PositionType
isDragging?: boolean,
style?: CSSProperties,
}




const clamp = (num: number, min: number, max: number): number => Math.min(max, Math.max(min, num));




const Draggable = ({
text,
position,
style = {},
}: Props) => {
const [pos, setPos] = useState<PositionType>();
const draggableRef = useRef<HTMLDivElement>();
const [parent, setParent] = useState<HTMLElement | null>();
const [xBounds, setXBounds] = useState<MinMaxType>({ min: 0, max: 0 });
const [yBounds, setYBounds] = useState<MinMaxType>({ min: 0, max: 0 });


useEffect(() => {
const parentElement: HTMLDivElement = draggableRef?.current?.parentElement as HTMLDivElement;
const parentWidth: number = parentElement?.offsetWidth as number;
const parentHeight: number = parentElement?.offsetHeight as number;
const parentLeft: number = parentElement?.offsetLeft as number;
const parentTop: number = parentElement?.offsetTop as number;


const draggableWidth: number = draggableRef?.current?.offsetWidth as number;
const draggableHeight: number = draggableRef?.current?.offsetHeight as number;


setParent(parentElement);


setPos({
x: parentLeft + position.x,
y: parentTop + position.y
});


setXBounds({
min: parentLeft,
max: parentWidth + parentLeft - draggableWidth,
});


setYBounds({
min: parentTop,
max: parentHeight + parentTop - draggableHeight,
});
}, [draggableRef, setParent, setPos, setXBounds, setYBounds, position]);


const mouseDownHandler = (e: r_MouseEvent) => {
if (e.button !== 0) return // only left mouse button


parent?.addEventListener('mousemove', mouseMoveHandler);
parent?.addEventListener('mouseup', mouseUpHandler);
parent?.addEventListener('mouseleave', mouseUpHandler);


e.stopPropagation();
e.preventDefault();
};


const mouseMoveHandler = (e: MouseEvent) => {
setPos({
x: clamp(e.pageX, xBounds?.min, xBounds?.max),
y: clamp(e.pageY, yBounds?.min, yBounds?.max),
});


e.stopPropagation();
e.preventDefault();
};


const mouseUpHandler = (e: MouseEvent) => {
parent?.removeEventListener('mousemove', mouseMoveHandler);
parent?.removeEventListener('mouseup', mouseUpHandler);


e.stopPropagation();
e.preventDefault();
};


const positionStyle = pos && { left: `${pos.x}px`, top: `${pos.y}px` };
const draggableStyle = { ..._styles.draggable, ...positionStyle, ...style } as CSSProperties;


return (
<div ref = { draggableRef as MutableRefObject <HTMLDivElement> }
style = { draggableStyle }
onMouseDown = {mouseDownHandler}>
{ text }
</div>
);
}




const _styles = {
draggable: {
position: 'absolute',
padding: '2px',
border: '1px solid black',
borderRadius: '5px',
},
};




export default Draggable;