首页
学习
活动
专区
工具
TVP
发布
社区首页 >问答首页 >使React组件/div可拖动的推荐方法

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

Stack Overflow用户
提问于 2014-01-05 04:46:40
回答 10查看 175.6K关注 0票数 126

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

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

谢谢。

EN

回答 10

Stack Overflow用户

回答已采纳

发布于 2014-01-05 07:13:25

我可能应该把这篇文章写成博客文章,但这里有一个非常可靠的例子。

评论应该能很好地解释事情,但如果你有问题请告诉我。

下面是可以使用的小提琴:http://jsfiddle.net/Af9Jt/2/

代码语言:javascript
复制
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

父级应拥有可拖动对象的当前位置。在这种情况下,可拖动对象仍然拥有"am I dragging“状态,但只要发生mousemove事件,就会调用this.props.onChange(x, y)

场景2

父对象只需要拥有“固定位置”,因此可拖动对象将拥有它的“拖动位置”,但在鼠标向上时,它将调用this.props.onChange(x, y),并将最终决策推迟到父对象。如果父对象不喜欢可拖动对象的结束位置,它就不会更新它的状态,并且可拖动对象将在拖动之前“快速返回”到它的初始位置。

混入还是组件?

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

票数 132
EN

Stack Overflow用户

发布于 2016-08-28 23:44:23

Jared Forsyth的回答大错特错,而且已经过时了。它遵循一整套反模式,例如usage of stopPropagationinitializing state from props、jQuery的用法、状态中的嵌套对象以及一些奇怪的dragging状态字段。如果被重写,解决方案将如下所示,但它仍然会在每次鼠标移动时强制进行虚拟DOM协调,并且性能不是很好。

UPD。我的回答大错特错,而且过时了。现在,代码通过使用本地事件处理程序和样式更新缓解了React组件生命周期缓慢的问题,使用了transform,因为它不会导致回流,并通过requestAnimationFrame限制DOM更改。现在,我尝试过的每种浏览器的速度都是60帧/秒。

代码语言:javascript
复制
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

代码语言:javascript
复制
.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;
}

Example on JSFiddle.

票数 29
EN

Stack Overflow用户

发布于 2020-05-08 04:51:00

这里有一个简单的现代方法,在ES6中使用useStateuseEffectuseRef

代码语言:javascript
复制
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
票数 22
EN
页面原文内容由Stack Overflow提供。腾讯云小微IT领域专用引擎提供翻译支持
原文链接:

https://stackoverflow.com/questions/20926551

复制
相关文章

相似问题

领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档