推荐使React组件/ div可拖动的方法

And*_*nor 82 javascript reactjs

我想制作一个可拖动的(也就是说,可以通过鼠标重新定位)React组件,它似乎必然涉及全局状态和分散的事件处理程序.我可以用脏的方式,在我的JS文件中使用全局变量,甚至可以将它包装在一个漂亮的闭包界面中,但我想知道是否有一种方法可以更好地与React相结合.

此外,由于我之前从未在原始JavaScript中完成此操作,因此我想看看专家是如何做到这一点的,以确保我已经处理了所有角落案例,特别是与React相关的案例.

谢谢.

Jar*_*yth 96

我应该把它变成一篇博文,但这里有一个非常可靠的例子.

评论应该很好地解释,但如果您有疑问,请告诉我.

这里有小提琴: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))
  }
})
Run Code Online (Sandbox Code Playgroud)

关于国家所有权等的思考

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

场景1

父母应该拥有可拖动的当前位置.在这种情况下,draggable仍将拥有"我在拖动"状态,但this.props.onChange(x, y)只要发生mousemove事件就会调用.

情景2

父母只需要拥有"非移动位置",因此可拖动将拥有它的"拖动位置",但是,它会调用this.props.onChange(x, y)并将最终决定推迟到父级.如果父级不喜欢可拖动的最终位置,则它不会更新它的状态,并且拖动将在拖动之前"快速"回到它的初始位置.

Mixin或组件?

@ssorallen指出,因为"draggable"本身就是一个属性,而不是一个东西,它可能更适合作为mixin.我对mixin的经验是有限的,所以我还没有看到他们在复杂的情况下如何帮助或妨碍他们.这可能是最好的选择.

  • 您可以通过执行以下操作来删除jquery依赖项:`var computedStyle = window.getComputedStyle(this.getDOMNode()); pos = {top:parseInt(computedStyle.top),left:parseInt(computedStyle.left)};`如果你正在使用jquery做出反应,你可能做错了;)如果你需要一些jquery插件,我发现它是通常更简单,更少的代码重写纯反应. (10认同)
  • 只想跟进@ MattCrinklaw-Vogt上面的评论,说更防弹的解决方案是使用`this.getDOMNode().getBoundingClientRect()` - getComputedStyle可以输出任何有效的CSS属性,包括`auto` in在哪种情况下,上面的代码将导致"NaN".请参阅MDN文章:https://developer.mozilla.org/en-US/docs/Web/API/Element/getBoundingClientRect (6认同)
  • 很好的例子.这似乎更适合作为[`Mixin`](http://facebook.github.io/react/docs/reusable-components.html#mixins)而不是完整的类,因为"Draggable"实际上不是一个对象,它是对象的能力. (4认同)
  • 我玩了一下,似乎将其拖到其父对象外面并没有任何作用,但是当它拖到另一个反应组件中时,会发生各种奇怪的事情 (2认同)
  • 为了跟进`this.getDOMNode()`,已经弃用了。使用引用获取dom节点。https://facebook.github.io/react/docs/more-about-refs.html#the-ref-callback-attribute (2认同)

Dan*_*mov 61

我实现了react-dnd,一个灵活的HTML5拖放混合,用于React和完整的DOM控件.

现有的拖放库不适合我的用例,所以我写了自己的.它类似于我们在Stampsy.com上运行了大约一年的代码,但改写后利用了React和Flux.

我的主要要求:

  • 发出自己的零DOM或CSS,将其留给消费组件;
  • 在消耗组件上施加尽可能少的结构;
  • 使用HTML5拖放作为主要后端,但可以在将来添加不同的后端;
  • 与原始HTML5 API一样,强调拖动数据而不仅仅是"可拖动视图";
  • 隐藏消费代码中的HTML5 API怪癖;
  • 对于不同类型的数据,不同的组件可以是"拖动源"或"丢弃目标";
  • 允许一个组件包含多个拖动源并在需要时放下目标;
  • 如果拖动或悬停兼容数据,则可以轻松删除目标以更改其外观;
  • 可以轻松使用图像拖动缩略图而不是元素屏幕截图,避免浏览器怪癖.

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

用法

简单的拖动源

首先,声明可以拖动的数据类型.

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

// ItemTypes.js
module.exports = {
  BLOCK: 'block',
  IMAGE: 'image'
};
Run Code Online (Sandbox Code Playgroud)

(如果您没有多种数据类型,则此库可能不适合您.)

然后,让我们创建一个非常简单的可拖动组件,当拖动时,它代表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)} />
    );
  }
);
Run Code Online (Sandbox Code Playgroud)

通过指定configureDragDrop,我们告诉DragDropMixin该组件的拖放行为.可拖动和可放置的组件都使用相同的mixin.

在内部configureDragDrop,我们需要调用组件支持的registerType每个自定义ItemTypes.例如,您的应用中可能会有多个图像表示形式,每个图像都会提供一个dragSourcefor ItemTypes.IMAGE.

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

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

简单掉落目标

假设我们希望ImageBlock成为IMAGEs 的下降目标.除了我们需要提供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>
    );
  }
);
Run Code Online (Sandbox Code Playgroud)

在一个组件中拖动Source + Drop Target

假设我们现在希望用户能够将图像拖出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>
    );
  }
);
Run Code Online (Sandbox Code Playgroud)

还有什么可能吗?

我没有涵盖所有内容,但可以通过以下几种方式使用此API:

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

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

  • 是的,我完全知道.您在其他地方拥有文档这一事实并不意味着这是对给定问题的回答.您可以写"使用Google"获得相同的结果.29个赞成是由于知名人士的长篇文章,而不是因为答案的质量. (5认同)
  • 除了使用鼠标之外,拖放和鼠标拖动有什么共同之处?你的答案与一个问题无关,显然是一个图书馆广告. (4认同)
  • 似乎有29个人发现这与问题有关。React DnD还可以让您很好地实现[鼠标拖动](https://gaearon.github.io/react-dnd/examples-drag-around-custom-drag-layer.html)。我认为比免费共享我的作品并在[示例](https://gaearon.github.io/react-dnd/examples-chessboard-tutorial-app.html)和[扩展文档](https下次再访问://gaearon.github.io/react-dnd/docs-overview.html),这样我就不必花时间阅读过时的评论。 (4认同)
  • @DanAbramov 我感谢你为发表这篇文章所做的努力,没有它我可能不会这么快就转向 *react-dnd* 。像您的许多产品一样,它**一开始**看起来不必要地复杂,但是以非常专业的方式组合在一起(通过您的文档,我通常会学到一两件关于软件工程的知识......)我读过“那些不知道 unix 的人注定要重新实现它......很糟糕”,我觉得类似的事情可能适用于你的许多创作(包括这个)。 (2认同)

raf*_*nes 20

这是一个简单的现代方法,在useState,useEffectuseRefES6 中。

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
Run Code Online (Sandbox Code Playgroud)

  • 这似乎是这里最新的答案。 (3认同)
  • 最新的,但不正确。`position` 存储为状态,这使得组件在每次 `mousemove` 时渲染。更糟糕的是,它没有任何限制。 (2认同)

pol*_*.ph 19

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'),
);
Run Code Online (Sandbox Code Playgroud)

还有一点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;
}
Run Code Online (Sandbox Code Playgroud)

关于JSFiddle的例子.

  • 感谢这一点,这绝对不是性能最高的解决方案,但它遵循当今构建应用程序的最佳实践。 (2认同)
  • 非常简单而优雅的解决方案.我很高兴看到我对它的看法有点类似.我确实有一个问题:你提到性能不佳,你会提出什么样的功能可以实现类似功能? (2认同)

Jos*_*son 10

反应可拖动也很容易使用.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')
);
Run Code Online (Sandbox Code Playgroud)

我的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>
Run Code Online (Sandbox Code Playgroud)

你需要他们的风格,这很短,或者你没有达到预期的行为.我比其他一些可能的选择更喜欢这种行为,但也有一种叫做反应可重复和可移动的东西.我正试图调整大小与可拖动工作,但到目前为止没有喜悦.


any*_*try 9

我已经将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;
Run Code Online (Sandbox Code Playgroud)

  • @AlexNikonov 它是 x 方向上(对齐)网格的大小。建议 gridX 和 gridY &gt; 1 以提高性能。 (2认同)

Eva*_*rad 8

这是带有 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];
}
Run Code Online (Sandbox Code Playgroud)

然后是一个使用钩子的组件:


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>
  );
}
Run Code Online (Sandbox Code Playgroud)

  • 我完全按照你写的做了,但我有一个问题,当我单击元素时,它远离鼠标并开始跟踪移动,但在不同的针点内 (2认同)

Tsu*_*oka 5

这是另一个简单的 React hooks 解决方案,没有任何第三方库,基于 codewithfeeling 和 Evan Conrad 的解决方案。 /sf/answers/4472124051/ /sf/answers/4316726641/

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;
`;
Run Code Online (Sandbox Code Playgroud)