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


97

我想制作一个可拖动(即可以通过鼠标重定位)的React组件,该组件似乎必然涉及全局状态和分散的事件处理程序。我可以在JS文件中使用一个全局变量来以肮脏的方式进行操作,甚至可以将其包装在一个不错的闭包接口中,但是我想知道是否有一种方法可以更好地与React结合。

另外,由于我以前从未在原始JavaScript中完成过此操作,所以我想看看专家是如何做到的,以确保我处理了所有极端情况,尤其是与React相关的情况。

谢谢。


实际上,我至少对散文的解释会和代码一样满意,甚至只是“您做得很好”。到目前为止,这是我迄今为止的工作中的一个JSFiddle:jsfiddle.net/Z2JtM
Andrew Fleenor 2014年

我同意这是一个有效的问题,因为目前要查看的反应代码示例很少
Jared Forsyth

1
为我的用例找到了一个简单的HTML5解决方案-youtu.be/z2nHLfiiKBA。可能会帮助某人!
炳廷

试试这个。这是一个简单的HOC,可以将包裹的元素变成可拖动的 npmjs.com/package/just-drag
Shan

Answers:


111

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

这些评论应该可以很好地说明问题,但是如果您有任何疑问,请告诉我。

这是玩的小提琴: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))
  }
})

关于国家所有权等的思考

从一开始,“谁应该拥有什么状态”是一个重要的问题。在“可拖动”组件的情况下,我可以看到一些不同的情况。

场景1

父级应拥有可拖动对象的当前位置。在这种情况下,可拖动对象仍将拥有“我正在拖动”状态,但是this.props.onChange(x, y)每当发生mousemove事件时都会调用。

方案2

父级只需要拥有“固定位置”,因此可拖动对象将拥有其“拖动位置”,但是在onmouseup上它将调用this.props.onChange(x, y)并将最终决定推迟到父级。如果父级不喜欢可拖动对象的最终位置,它将不会更新其状态,并且可拖动对象会在拖动之前“快速恢复”到其初始位置。

混合蛋白或成分?

@ssorallen指出,由于“可拖动”与其说是事物本身,不如说是一个属性,它可能更好地用作混合。我对mixin的经验有限,因此我还没有看到它们在复杂情况下如何提供帮助或妨碍。这可能是最好的选择。


4
很好的例子。这似乎Mixin比完整类更合适,因为“可拖动”实际上不是对象,而是对象的能力。
罗斯·艾伦

2
我玩了一下,似乎将其拖到其父对象外面并没有任何作用,但是当它拖入另一个反应组件时,会发生各种奇怪的事情
Gorkem Yurtseven 2014年

11
您可以通过执行以下操作来删除jquery依赖项: var computedStyle = window.getComputedStyle(this.getDOMNode()); pos = { top: parseInt(computedStyle.top), left: parseInt(computedStyle.left) }; 如果您将jquery与react一起使用,则可能做错了;)如果您需要一些jquery插件,我发现它通常更容易,用更少的代码将其重写为纯react。
Matt Crinklaw-Vogt 2014年

7
只是想跟进@ MattCrinklaw-Vogt的上述评论,说一种更防弹的解决方案是使用this.getDOMNode().getBoundingClientRect()-getComputedStyle可以输出任何有效的CSS属性,包括auto在这种情况下,上面的代码将导致NaN。见MDN文章:developer.mozilla.org/en-US/docs/Web/API/Element/...
Andru

2
为了跟进re this.getDOMNode(),不推荐使用。使用引用获取dom节点。 facebook.github.io/react/docs/…–
克里斯·萨丁格

63

我实现了react-dnd,这是一个用于React的灵活HTML5拖放混入,具有完整的DOM控制。

现有的拖放库不适合我的用例,因此我编写了自己的库。它类似于我们在Stampsy.com上运行了大约一年的代码,但是为了利用React和Flux进行了重写。

我的主要要求:

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

如果您觉得这些听起来很熟悉,请继续阅读。

用法

简单拖动源

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

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

// ItemTypes.js
module.exports = {
  BLOCK: 'block',
  IMAGE: 'image'
};

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

然后,让我们制作一个非常简单的可拖动组件,将其拖动时表示IMAGE

var { DragDropMixin } = require('react-dnd'),
    ItemTypes = require('./ItemTypes');

var Image = React.createClass({
  mixins: [DragDropMixin],

  configureDragDrop(registerType) {

    // Specify all supported types by calling registerType(type, { dragSource?, dropTarget? })
    registerType(ItemTypes.IMAGE, {

      // dragSource, when specified, is { beginDrag(), canDrag()?, endDrag(didDrop)? }
      dragSource: {

        // beginDrag should return { item, dragOrigin?, dragPreview?, dragEffect? }
        beginDrag() {
          return {
            item: this.props.image
          };
        }
      }
    });
  },

  render() {

    // {...this.dragSourceFor(ItemTypes.IMAGE)} will expand into
    // { draggable: true, onDragStart: (handled by mixin), onDragEnd: (handled by mixin) }.

    return (
      <img src={this.props.image.url}
           {...this.dragSourceFor(ItemTypes.IMAGE)} />
    );
  }
);

通过指定configureDragDrop,我们可以知道DragDropMixin该组件的拖放行为。可拖动和可拖放组件都使用相同的mixin。

在内部configureDragDrop,我们需要调用该组件支持的registerType每个自定义ItemTypes。例如,有可能是图像的几次交涉中您的应用程序,每个将提供一个dragSourceItemTypes.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>
    );
  }
);

将源+目标拖放到一个组件中

假设我们现在希望用户能够将图像拖出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排序。我们只需要他们实施dropTargetdragSource进行ItemTypes.BLOCK
  • 假设我们添加其他种类的块。我们可以通过将其重新排序逻辑放在mixin中来重用它们。
  • dropTargetFor(...types) 允许一次指定几种类型,因此一个放置区可以捕获许多不同的类型。
  • 当您需要更细粒度的控制时,大多数方法都将通过拖动事件传递,这导致它们成为最后一个参数。

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


5
除了使用鼠标外,拖放和鼠标拖动还有什么共同点?您的答案根本与问题无关,显然是图书馆的广告。
polkovnikov.ph

5
似乎有29个人发现这与问题有关。React DnD也使您可以很好地实现鼠标拖动。我认为比免费共享我的作品更好,并且下次还要处理示例和大量文档,因此,我不必花时间阅读过时的评论。
丹·阿布拉莫夫

7
是的,我完全知道这一点。您在其他地方有文档的事实并不意味着这是对给定问题的答案。您可能会为相同的结果编写“使用Google”。29项投票是由于一个知名人士的职位空缺,而不是因为答案的质量。
polkovnikov.ph '16

更新了指向带有react-dnd的可自由拖动内容的官方示例的链接:基本高级
uryga

23

贾里德·福赛斯(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;
}

JSFiddle上的示例。


2
因此,这绝对不是性能最高的解决方案,而是遵循当今构建应用程序的最佳实践。
Spets

1
@ryanj不,默认值是邪恶的,这就是问题所在。道具变更时应采取的适当措施是什么?我们应该将状态重置为新的默认值吗?我们是否应该将新的默认值与旧的默认值进行比较,以仅在更改默认值时将状态重置为默认值?没有办法限制用户仅使用常数,而不能使用其他任何值。这就是为什么它是反模式。默认值应该通过高阶组件显式创建(即,对于整个类,而不是对象),并且永远不要通过props设置。
polkovnikov.ph

1
我不同意-组件状态是存储特定于组件UI的数据的绝佳场所,例如,它与整个应用程序都不相关。在某些情况下,由于无法潜在地将默认值作为prop传递,用于获取此数据后安装的选项受到限制,并且在很多情况下(比大多数情况下不如组件周围的变化多,可能会在组件上传递不同的someDefaultValue prop)以后的日期。我不主张将其作为最佳实践或类似的任何形式,其危害程度不如您所建议的imo
ryan j

2
确实非常简单而优雅的解决方案。我很高兴看到自己对此的看法有点相似。我确实有一个问题:您提到的性能不佳,您打算如何在实现性能的同时实现类似功能?
纪尧姆M,

1
无论如何,我们现在有了一些问题,我必须尽快再次更新答案。
polkovnikov.ph

13

我已经将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;

您好@anyhotcountry您使用gridX系数做什么?
AlexNikonov

1
@AlexNikonov它是x方向上(捕捉)网格的大小。建议使gridX和gridY> 1以提高性能。
anyhotcountry

这对我来说效果很好。在onStart()函数中进行的更改中:计算relX和relY我使用了e.clienX-this.props.x。这使我可以将可拖动组件放置在父容器内,而不是依赖于整个页面作为拖动区域。我知道这是最新评论,但只想说谢谢。
杰夫

11

可反应的易用性也易于使用。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的东西。我正在尝试使用可拖动的控件调整大小,但到目前为止没有任何乐趣。


8

下面是一个简单现代的方法来此与useStateuseEffectuseRef在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

这似乎是这里最新的答案。
codyThompson

2

我想添加第三个场景

移动位置不会以任何方式保存。将其视为鼠标移动-您的光标不是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操作是一件很优雅的事情(我从没想过要这么说)


1
您能否setXandY用虚构的分量填充函数?
Thellimist

0

我已经使用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;
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.