Cách đề xuất để làm cho React component / div có thể kéo được


97

Tôi muốn tạo thành phần React có thể kéo (có nghĩa là có thể thay đổi vị trí bằng chuột), có vẻ như nhất thiết phải liên quan đến trạng thái toàn cầu và trình xử lý sự kiện phân tán. Tôi có thể làm điều đó theo cách bẩn thỉu, với một biến toàn cục trong tệp JS của tôi và thậm chí có thể bọc nó trong một giao diện đóng đẹp mắt, nhưng tôi muốn biết liệu có cách nào kết hợp với React tốt hơn không.

Ngoài ra, vì tôi chưa bao giờ làm điều này trong JavaScript thô trước đây, tôi muốn xem cách một chuyên gia thực hiện nó, để đảm bảo rằng tôi đã xử lý tất cả các trường hợp góc cạnh, đặc biệt là khi chúng liên quan đến React.

Cảm ơn.


Trên thực tế, ít nhất tôi sẽ hài lòng với một lời giải thích bằng văn xuôi như mã, hoặc thậm chí chỉ là, "bạn đang làm tốt". Nhưng đây là một JSFiddle công việc của tôi cho đến nay: jsfiddle.net/Z2JtM
Andrew Fleenor

Tôi đồng ý rằng đây là một câu hỏi hợp lệ, cho rằng có rất ít ví dụ về phản ứng mã để nhìn vào hiện
Jared Forsyth

1
Đã tìm thấy một giải pháp HTML5 đơn giản cho trường hợp sử dụng của tôi - youtu.be/z2nHLfiiKBA . Có thể giúp ai đó !!
Prem

Thử cái này. Đó là một HOC đơn giản để biến các phần tử được bọc thành có thể kéo được npmjs.com/package/just-drag
Shan

Câu trả lời:


111

Tôi có lẽ nên biến nó thành một bài đăng trên blog, nhưng đây là một ví dụ khá rõ ràng.

Các nhận xét sẽ giải thích mọi thứ khá tốt, nhưng hãy cho tôi biết nếu bạn có thắc mắc.

Và đây là trò chơi tuyệt vời để chơi với: 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))
  }
})

Suy nghĩ về sở hữu nhà nước, v.v.

"Ai nên sở hữu trạng thái nào" là một câu hỏi quan trọng cần trả lời, ngay từ đầu. Trong trường hợp thành phần "có thể kéo", tôi có thể thấy một vài trường hợp khác nhau.

cảnh 1

cha mẹ phải sở hữu vị trí hiện tại của có thể kéo. Trong trường hợp này, trình kéo sẽ vẫn sở hữu trạng thái "tôi đang kéo", nhưng sẽ gọithis.props.onChange(x, y) bất cứ khi nào xảy ra sự kiện di chuột.

Tình huống 2

cha mẹ chỉ cần sở hữu "vị trí không di chuyển" và do đó, có thể kéo sẽ sở hữu "vị trí kéo" nhưng khi sử dụng nó sẽ gọi this.props.onChange(x, y)và trì hoãn quyết định cuối cùng cho cha mẹ. Nếu cha mẹ không thích nơi có thể kéo kết thúc, nó sẽ không cập nhật trạng thái của nó và có thể kéo sẽ "quay trở lại" vị trí ban đầu trước khi kéo.

Mixin hoặc thành phần?

@ssorallen đã chỉ ra rằng, bởi vì "có thể kéo" là một thuộc tính hơn là một thứ tự thân, nó có thể hoạt động tốt hơn như một hỗn hợp. Kinh nghiệm của tôi với các mixin còn hạn chế, vì vậy tôi chưa biết chúng có thể giúp ích hoặc cản trở gì trong các tình huống phức tạp. Đây cũng có thể là lựa chọn tốt nhất.


4
Ví dụ tuyệt vời. Điều này có vẻ phù hợp hơn với tư cách Mixinlà một lớp đầy đủ vì "Draggable" không thực sự là một đối tượng, nó là một khả năng của một đối tượng.
Ross Allen

2
Tôi chơi xung quanh với nó một chút, nó có vẻ như kéo bên ngoài doesnt mẹ làm bất cứ điều gì, nhưng tất cả các loại điều kỳ lạ xảy ra khi nó kéo vào một phản ứng thành phần
Gorkem Yurtseven

11
Bạn có thể loại bỏ sự phụ thuộc của jquery bằng cách thực hiện: var computedStyle = window.getComputedStyle(this.getDOMNode()); pos = { top: parseInt(computedStyle.top), left: parseInt(computedStyle.left) }; Nếu bạn đang sử dụng jquery với phản ứng thì có thể bạn đang làm sai điều gì đó;) Nếu bạn cần một số plugin jquery, tôi thấy nó thường dễ dàng hơn và ít mã hơn để viết lại nó trong phản ứng thuần túy.
Matt Crinklaw-Vogt

7
Tôi chỉ muốn theo dõi nhận xét ở trên của @ MattCrinklaw-Vogt để nói rằng một giải pháp chống đạn hơn là sử dụng this.getDOMNode().getBoundingClientRect()- getComputedStyle có thể xuất ra bất kỳ thuộc tính CSS hợp lệ nào, bao gồm cả autotrường hợp mã ở trên sẽ dẫn đến a NaN. Xem bài viết MDN: developer.mozilla.org/en-US/docs/Web/API/Element/…
Andru

2
Và để theo dõi lại this.getDOMNode(), điều đó đã không được dùng nữa. Sử dụng một tham chiếu để lấy nút dom. facebook.github.io/react/docs/…
Chris Sattinger

63

Tôi đã triển khai react-dnd , một mixin kéo và thả HTML5 linh hoạt cho React với toàn quyền kiểm soát DOM.

Các thư viện kéo và thả hiện tại không phù hợp với trường hợp sử dụng của tôi nên tôi đã viết thư của riêng mình. Nó tương tự như đoạn mã chúng tôi đã chạy trong khoảng một năm trên Stampsy.com, nhưng được viết lại để tận dụng lợi thế của React và Flux.

Các yêu cầu chính mà tôi có:

  • Không phát ra DOM hoặc CSS của riêng nó, để lại nó cho các thành phần tiêu thụ;
  • Áp đặt cấu trúc càng ít càng tốt trên các thành phần tiêu thụ;
  • Sử dụng kéo và thả HTML5 làm phần phụ trợ chính nhưng có thể thêm các phần phụ trợ khác nhau trong tương lai;
  • Giống như API HTML5 ban đầu, nhấn mạnh việc kéo dữ liệu chứ không chỉ “các chế độ xem có thể kéo”;
  • Ẩn các điều kỳ quặc API HTML5 khỏi mã tiêu thụ;
  • Các thành phần khác nhau có thể là "nguồn kéo" hoặc "mục tiêu thả" cho các loại dữ liệu khác nhau;
  • Cho phép một thành phần chứa nhiều nguồn kéo và thả mục tiêu khi cần thiết;
  • Giúp mục tiêu thả dễ dàng thay đổi giao diện của chúng nếu dữ liệu tương thích đang được kéo hoặc di chuột;
  • Giúp bạn dễ dàng sử dụng hình ảnh để kéo hình thu nhỏ thay vì ảnh chụp màn hình phần tử, tránh những điều kỳ quặc của trình duyệt.

Nếu những điều này nghe quen thuộc với bạn, hãy đọc tiếp.

Sử dụng

Nguồn kéo đơn giản

Đầu tiên, khai báo các loại dữ liệu có thể kéo được.

Chúng được sử dụng để kiểm tra "tính tương thích" của các nguồn kéo và mục tiêu thả:

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

(Nếu bạn không có nhiều kiểu dữ liệu, thì libary này có thể không dành cho bạn.)

Sau đó, hãy tạo một thành phần có thể kéo rất đơn giản, khi được kéo, đại diện 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)} />
    );
  }
);

Bằng cách chỉ định configureDragDrop, chúng tôi nóiDragDropMixin hành vi kéo-thả của thành phần này. Cả hai thành phần có thể kéo và có thể kéo xuống đều sử dụng cùng một loại mixin.

Bên trong configureDragDrop, chúng ta cần gọi registerTypecho từng tùy chỉnh của mình ItemTypesmà thành phần hỗ trợ. Ví dụ: có thể có một số hình ảnh đại diện trong ứng dụng của bạn và mỗi hình ảnh sẽ cung cấp dragSourcechoItemTypes.IMAGE .

A dragSourcechỉ là một đối tượng chỉ định cách thức hoạt động của nguồn kéo. Bạn phải triển khai beginDragđể trả về mục đại diện cho dữ liệu bạn đang kéo và một vài tùy chọn có thể điều chỉnh giao diện người dùng kéo. Bạn có thể tùy chọn triển khai canDragđể cấm kéo hoặc endDrag(didDrop)thực thi một số logic khi thả xảy ra (hoặc chưa). Và bạn có thể chia sẻ logic này giữa các thành phần bằng cách để một mixin được chia sẻ tạo ra dragSourcecho chúng.

Cuối cùng, bạn phải sử dụng {...this.dragSourceFor(itemType)}trên một số (một hoặc nhiều) phần tử renderđể đính kèm các trình xử lý kéo. Điều này có nghĩa là bạn có thể có nhiều “chốt kéo” trong một phần tử và chúng thậm chí có thể tương ứng với các loại mục khác nhau. (Nếu bạn không quen thuộc với Thuộc tính Spread JSX cú pháp của , hãy xem nó).

Mục tiêu thả đơn giản

Giả sử chúng tôi muốn ImageBlocktrở thành mục tiêu giảm giá cho IMAGEs. Đó là khá nhiều giống nhau, ngoại trừ việc chúng ta cần phải đưa ra registerTypemột dropTargetthực hiện:

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

Kéo nguồn + thả mục tiêu trong một thành phần

Giả sử bây giờ chúng tôi muốn người dùng có thể kéo hình ảnh ra khỏi ImageBlock. Chúng tôi chỉ cần thêm thích hợp dragSourcevào nó và một vài trình xử lý:

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

Điều gì khác có thể xảy ra?

Tôi chưa đề cập đến mọi thứ nhưng có thể sử dụng API này theo một số cách khác:

  • Sử dụng getDragState(type)getDropState(type) để tìm hiểu xem tính năng kéo có hoạt động hay không và sử dụng nó để chuyển đổi các lớp hoặc thuộc tính CSS;
  • Chỉ định dragPreviewđể Imagesử dụng hình ảnh làm trình giữ chỗ kéo (sử dụng ImagePreloaderMixinđể tải chúng);
  • Giả sử, chúng tôi muốn đặt ImageBlockshàng lại. Chúng tôi chỉ cần họ thực hiện dropTargetdragSourcecho ItemTypes.BLOCK.
  • Giả sử chúng ta thêm các loại khối khác. Chúng ta có thể sử dụng lại logic sắp xếp lại của chúng bằng cách đặt nó vào một mixin.
  • dropTargetFor(...types) cho phép chỉ định nhiều loại cùng một lúc, vì vậy một vùng thả có thể bắt nhiều loại khác nhau.
  • Khi bạn cần kiểm soát chi tiết hơn, hầu hết các phương thức được truyền sự kiện kéo gây ra chúng như là tham số cuối cùng.

Để có tài liệu cập nhật và hướng dẫn cài đặt, hãy truy cập repo react-dnd trên Github .


5
Kéo và thả và kéo chuột có điểm gì chung khác với việc sử dụng chuột? Câu trả lời của bạn hoàn toàn không liên quan đến một câu hỏi và rõ ràng là một quảng cáo thư viện.
polkovnikov.ph

5
Có vẻ như 29 người đã tìm thấy nó có liên quan đến câu hỏi. React DnD cũng cho phép bạn thực hiện kéo chuột rất tốt. Tôi sẽ nghĩ tốt hơn là chia sẻ công việc của mình miễn phí và làm việc với các ví dụtài liệu mở rộng vào lần sau để tôi không phải mất thời gian đọc các bình luận khó hiểu.
Dan Abramov

7
Vâng, tôi hoàn toàn biết điều đó. Thực tế là bạn có tài liệu ở nơi khác không có nghĩa là đây là câu trả lời cho câu hỏi đã cho. Bạn có thể đã viết "sử dụng Google" cho cùng một kết quả. 29 lượt bình chọn là do một bài đăng dài của một người được biết đến, không phải vì chất lượng của câu trả lời.
polkovnikov.ph

cập nhật liên kết đến các ví dụ chính thức về nội dung có thể kéo tự do với
react

23

Câu trả lời của Jared Forsyth là sai khủng khiếp và lỗi thời. Nó theo sau một tập hợp toàn bộ các phản vật chất như cách sử dụngstopPropagation , trạng thái khởi tạo từ đạo cụ , cách sử dụng jQuery, các đối tượng lồng nhau trong trạng thái và có một sốdragging trường trạng thái . Nếu được viết lại, giải pháp sẽ như sau, nhưng nó vẫn buộc điều chỉnh DOM ảo trên mỗi lần di chuyển chuột và không hiệu quả lắm.

CẬP NHẬT. Câu trả lời của tôi đã sai và lỗi thời một cách khủng khiếp. Giờ đây, mã giảm bớt các vấn đề về vòng đời của thành phần React chậm bằng cách sử dụng các trình xử lý sự kiện gốc và cập nhật kiểu, sử dụng transformvì nó không dẫn đến các dòng lại và điều chỉnh các thay đổi của DOM requestAnimationFrame. Bây giờ, nó luôn là 60 FPS đối với tôi trong mọi trình duyệt tôi đã thử.

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'),
);

và một chút 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;
}

Ví dụ trên JSFiddle.


2
Cảm ơn vì điều này, đây chắc chắn không phải là giải pháp hiệu quả nhất nhưng nó tuân theo các phương pháp xây dựng ứng dụng tốt nhất hiện nay.
Spets

1
@ryanj Không, các giá trị mặc định là xấu, đó là vấn đề. Hành động thích hợp khi đạo cụ thay đổi là gì? Chúng ta có nên đặt lại trạng thái về mặc định mới không? Chúng ta có nên so sánh giá trị mặc định mới với giá trị mặc định cũ để đặt lại trạng thái về mặc định chỉ khi mặc định đã thay đổi không? Không có cách nào để hạn chế người dùng chỉ sử dụng một giá trị không đổi và không có gì khác. Đó là lý do tại sao nó là một phản vật chất. Các giá trị mặc định phải được tạo một cách rõ ràng thông qua các thành phần bậc cao (nghĩa là cho cả lớp, không phải cho một đối tượng) và không bao giờ nên được đặt thông qua đạo cụ.
polkovnikov.ph

1
Tôi tôn trọng không đồng ý - trạng thái thành phần là một nơi tuyệt vời để lưu trữ dữ liệu dành riêng cho giao diện người dùng của một thành phần, chẳng hạn như không liên quan đến toàn bộ ứng dụng. Nếu không có khả năng chuyển các giá trị mặc định làm đạo cụ trong một số trường hợp, các tùy chọn để truy xuất dữ liệu post-mount này bị hạn chế và trong nhiều trường hợp (hầu hết?) Ít mong muốn hơn so với các yếu tố không mong muốn xung quanh một thành phần có khả năng được chuyển một số hỗ trợ someDefaultValue khác tại một ngày sau. Tôi không ủng hộ nó là phương pháp hay nhất hay bất cứ điều gì tương tự, nó chỉ đơn giản là không có hại như bạn đang đề xuất về imo
ryan j

2
Giải pháp rất đơn giản và thanh lịch thực sự. Tôi rất vui khi thấy rằng công việc của tôi cũng tương tự như vậy. Tôi có một câu hỏi: bạn đề cập đến hiệu suất kém, bạn sẽ đề xuất gì để đạt được một tính năng tương tự với hiệu suất?
Guillaume M

1
Dù sao thì bây giờ chúng ta cũng có câu trả lời và tôi phải sớm cập nhật câu trả lời một lần nữa.
polkovnikov.ph

13

Tôi đã cập nhật giải pháp polkovnikov.ph cho React 16 / ES6 với các cải tiến như xử lý cảm ứng và chụp vào lưới, đây là những gì tôi cần cho một trò chơi. Bám vào lưới làm giảm bớt các vấn đề về hiệu suất.

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;

chào @anyhotcountry bạn sử dụng hệ số gridX để làm gì?
AlexNikonov

1
@AlexNikonov đó là kích thước của lưới (snap-to) theo hướng x. Khuyến nghị có gridX và gridY> 1 để cải thiện hiệu suất.
anyhotcountry

Điều này làm việc khá tốt cho tôi. Về thay đổi tôi đã thực hiện trong hàm onStart (): tính toán relX và relY Tôi đã sử dụng e.clienX-this.props.x. Điều này cho phép tôi đặt thành phần có thể kéo bên trong vùng chứa mẹ thay vì phụ thuộc vào toàn bộ trang là vùng kéo. Tôi biết đó là một bình luận muộn nhưng chỉ muốn nói lời cảm ơn.
Geoff

11

react-draggable cũng dễ sử dụng. 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')
);

Và index.html của tôi:

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

Bạn cần phong cách của họ, ngắn gọn, hoặc bạn không có được hành vi như mong đợi. Tôi thích hành vi hơn một số lựa chọn khả thi khác, nhưng cũng có một thứ gọi là phản ứng-có thể thay đổi kích thước-và-di chuyển được . Tôi đang cố gắng thay đổi kích thước làm việc với có thể kéo, nhưng không có niềm vui cho đến nay.


8

Dưới đây là một cách tiếp cận hiện đại đơn giản để này với useState, useEffectuseReftrong 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

Đây dường như là câu trả lời cập nhật nhất ở đây.
codyThompson

2

Tôi muốn thêm một Tình huống thứ 3

Vị trí di chuyển không được lưu theo bất kỳ cách nào. Hãy coi nó như một chuyển động của chuột - con trỏ của bạn không phải là một thành phần React, phải không?

Tất cả những gì bạn làm là thêm một giá đỡ như "draggable" vào thành phần của bạn và một luồng các sự kiện kéo sẽ thao túng 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
    };
},

Trong trường hợp này, thao tác DOM là một điều tuyệt vời (tôi chưa bao giờ nghĩ rằng mình sẽ nói điều này)


1
bạn có thể điền vào setXandYchức năng với một thành phần tưởng tượng?
Thellimist

0

Tôi đã cập nhật lớp bằng cách sử dụng refs vì tất cả các giải pháp tôi thấy ở đây có những thứ không còn được hỗ trợ hoặc sẽ sớm bị giảm giá như ReactDOM.findDOMNode. Có thể là cha của một thành phần con hoặc một nhóm con :)

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;
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.