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

 mobiledu2502917243 发布于 2023-02-06 15:33

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

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

谢谢.

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

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

    这里有小提琴: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

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

    情景2

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

    Mixin或组件?

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

    2023-02-06 15:34 回答
  • 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'),
    );
    

    还有一点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的例子.

    2023-02-06 15:34 回答
  • 反应可拖动也很容易使用.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>
    

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

    2023-02-06 15:34 回答
  • 我实现了react-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该组件的拖放行为.可拖动和可放置的组件都使用相同的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>
        );
      }
    );
    

    在一个组件中拖动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>
        );
      }
    );
    

    还有什么可能吗?

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

    使用getDragState(type)getDropState(type)了解拖动是否处于活动状态并使用它来切换CSS类或属性;

    指定dragPreviewImage图像用作拖动占位符(用于ImagePreloaderMixin加载它们);

    说,我们想要重新制作ImageBlocks.我们只需要他们实施dropTargetdragSource进行ItemTypes.BLOCK.

    假设我们添加其他类型的块.我们可以通过将它们放在mixin中来重用它们的重新排序逻辑.

    dropTargetFor(...types) 允许一次指定多个类型,因此一个放置区可以捕获许多不同的类型.

    当您需要更细粒度的控件时,大多数方法都会传递拖动事件,导致它们成为最后一个参数.

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

    2023-02-06 15:36 回答
  • 我已经将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;
    

    2023-02-06 15:36 回答
撰写答案
今天,你开发时遇到什么问题呢?
立即提问
热门标签
PHP1.CN | 中国最专业的PHP中文社区 | PNG素材下载 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有