Cara yang disarankan untuk membuat komponen / div React dapat diseret

100

Saya ingin membuat komponen React yang dapat diseret (yaitu, dapat diposisikan ulang oleh mouse), yang tampaknya melibatkan keadaan global dan penangan acara yang tersebar. Saya dapat melakukannya dengan cara yang kotor, dengan variabel global dalam file JS saya, dan mungkin bahkan dapat membungkusnya dalam antarmuka penutupan yang bagus, tetapi saya ingin tahu apakah ada cara yang cocok dengan React lebih baik.

Juga, karena saya belum pernah melakukan ini dalam JavaScript mentah sebelumnya, saya ingin melihat bagaimana seorang ahli melakukannya, untuk memastikan saya telah menangani semua kasus sudut, terutama yang berkaitan dengan React.

Terima kasih.

Andrew Fleenor
sumber
Sebenarnya, saya setidaknya akan senang dengan penjelasan prosa sebagai kode, atau bahkan hanya, "Anda melakukannya dengan baik". Tapi inilah JSFiddle dari pekerjaan saya sejauh ini: jsfiddle.net/Z2JtM
Andrew Fleenor
Saya setuju bahwa ini adalah pertanyaan yang valid, mengingat hanya ada sedikit contoh kode reaksi untuk dilihat saat ini
Jared Forsyth
1
Menemukan solusi HTML5 sederhana untuk kasus penggunaan saya - youtu.be/z2nHLfiiKBA . Mungkin membantu seseorang !!
Prem
Coba ini. Ini adalah HOC sederhana untuk mengubah elemen yang dibungkus menjadi dapat diseret npmjs.com/package/just-drag
Shan

Jawaban:

114

Saya mungkin harus mengubahnya menjadi posting blog, tetapi inilah contoh yang cukup solid.

Komentar harus menjelaskan semuanya dengan cukup baik, tetapi beri tahu saya jika Anda memiliki pertanyaan.

Dan inilah biola untuk dimainkan: 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))
  }
})

Pemikiran tentang kepemilikan negara, dll.

"Siapa yang harus memiliki negara bagian apa" adalah pertanyaan penting untuk dijawab, sejak awal. Dalam kasus komponen "yang dapat diseret", saya dapat melihat beberapa skenario berbeda.

skenario 1

induk harus memiliki posisi draggable saat ini. Dalam kasus ini, yang dapat diseret akan tetap memiliki status "sedang saya menyeret", tetapi akan memanggilthis.props.onChange(x, y) setiap kali terjadi peristiwa mousemove.

Skenario 2

induk hanya perlu memiliki "posisi tidak bergerak", sehingga yang dapat diseret akan memiliki "posisi seret", tetapi pada saat tikus itu akan memanggil this.props.onChange(x, y) dan menunda keputusan akhir kepada induk. Jika induk tidak suka di mana tempat draggable berakhir, itu hanya tidak akan memperbarui statusnya, dan draggable akan "kembali" ke posisi awal sebelum menyeret.

Mixin atau komponen?

@ssorallen menunjukkan bahwa, karena "draggable" lebih merupakan atribut daripada benda itu sendiri, itu mungkin berfungsi lebih baik sebagai mixin. Pengalaman saya dengan mixin terbatas, jadi saya belum melihat bagaimana mereka dapat membantu atau menghalangi dalam situasi yang rumit. Ini mungkin pilihan terbaik.

Jared Forsyth
sumber
4
Contoh yang bagus. Ini tampaknya lebih tepat sebagai Mixinkelas penuh karena "Draggable" sebenarnya bukan sebuah objek, ini adalah kemampuan dari sebuah objek.
Ross Allen
2
Saya bermain-main dengannya sedikit, sepertinya menyeret keluar induknya tidak melakukan apa-apa, tetapi segala macam hal aneh terjadi ketika diseret ke komponen reaksi lain
Gorkem Yurtseven
11
Anda dapat menghapus ketergantungan jquery dengan melakukan: var computedStyle = window.getComputedStyle(this.getDOMNode()); pos = { top: parseInt(computedStyle.top), left: parseInt(computedStyle.left) }; Jika Anda menggunakan jquery dengan react, Anda mungkin melakukan sesuatu yang salah;) Jika Anda memerlukan beberapa plugin jquery, saya merasa biasanya easer dan lebih sedikit kode untuk menulis ulang dalam reaksi murni.
Matt Crinklaw-Vogt
7
Hanya ingin menindaklanjuti komentar di atas oleh @ MattCrinklaw-Vogt untuk mengatakan bahwa solusi yang lebih anti peluru adalah dengan menggunakan this.getDOMNode().getBoundingClientRect()- getComputedStyle dapat menampilkan properti CSS yang valid termasuk autodalam hal ini kode di atas akan menghasilkan a NaN. Lihat artikel MDN: developer.mozilla.org/en-US/docs/Web/API/Element/…
Andru
2
Dan untuk menindaklanjuti kembali this.getDOMNode(), itu sudah usang. Gunakan ref untuk mendapatkan node dom. facebook.github.io/react/docs/…
Chris Sattinger
65

Saya menerapkan react-dnd , mixin seret dan lepas HTML5 yang fleksibel untuk Bereaksi dengan kontrol DOM penuh.

Pustaka seret dan lepas yang ada tidak sesuai dengan kasus penggunaan saya, jadi saya menulisnya sendiri. Ini mirip dengan kode yang telah kami jalankan selama sekitar satu tahun di Stampsy.com, tetapi ditulis ulang untuk memanfaatkan React dan Flux.

Persyaratan utama yang saya miliki:

  • Emisi nol DOM atau CSS-nya sendiri, serahkan pada komponen yang memakan;
  • Tetapkan struktur sesedikit mungkin untuk mengonsumsi komponen;
  • Gunakan seret dan lepas HTML5 sebagai backend utama tetapi memungkinkan untuk menambahkan backend yang berbeda di masa mendatang;
  • Seperti API HTML5 asli, tekankan menyeret data dan bukan hanya "tampilan yang dapat diseret";
  • Sembunyikan kebiasaan API HTML5 dari kode konsumsi;
  • Komponen yang berbeda mungkin berupa "sumber seret" atau "target jatuhkan" untuk jenis data yang berbeda;
  • Izinkan satu komponen berisi beberapa sumber seret dan jatuhkan target bila diperlukan;
  • Permudah target drop untuk mengubah tampilannya jika data yang kompatibel sedang diseret atau di-hover;
  • Permudah penggunaan gambar untuk menyeret thumbnail alih-alih screenshot elemen, menghindari kebiasaan browser.

Jika ini terdengar asing bagi Anda, baca terus.

Pemakaian

Sumber Drag Sederhana

Pertama, nyatakan jenis data yang bisa diseret.

Ini digunakan untuk memeriksa "kompatibilitas" sumber seret dan target pelepasan:

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

(Jika Anda tidak memiliki beberapa tipe data, perpustakaan ini mungkin bukan untuk Anda.)

Kemudian, mari kita buat komponen yang dapat diseret sangat sederhana yang, saat diseret, mewakili 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)} />
    );
  }
);

Dengan menentukan configureDragDrop, kami memberi tahu DragDropMixinperilaku seret-lepas dari komponen ini. Baik komponen yang dapat diseret maupun yang dapat dilepas menggunakan campuran yang sama.

Di dalam configureDragDrop, kita perlu memanggil registerTypesetiap kebiasaan kita ItemTypesyang didukung komponen. Misalnya, mungkin ada beberapa representasi gambar di aplikasi Anda, dan masing-masing akan menyediakan dragSourceuntukItemTypes.IMAGE .

A dragSourcehanyalah sebuah objek yang menentukan cara kerja sumber seret. Anda harus mengimplementasikan beginDraguntuk mengembalikan item yang mewakili data yang Anda seret dan, secara opsional, beberapa opsi yang menyesuaikan UI menyeret. Anda dapat menerapkan secara opsional canDraguntuk melarang menyeret, atau endDrag(didDrop)untuk menjalankan beberapa logika saat penurunan telah (atau belum) terjadi. Dan Anda dapat membagikan logika ini di antara komponen dengan membiarkan mixin bersama membuatkan dragSourceuntuk mereka.

Terakhir, Anda harus menggunakan {...this.dragSourceFor(itemType)}beberapa (satu atau lebih) elemen renderuntuk memasang penangan tarik. Ini berarti Anda dapat memiliki beberapa "pegangan seret" dalam satu elemen, dan mereka bahkan mungkin sesuai dengan jenis item yang berbeda. (Jika Anda tidak terbiasa dengan JSX Spread Attributes sintaks , periksalah).

Target Jatuhkan Sederhana

Katakanlah kita ingin ImageBlockmenjadi target penurunan untuk IMAGEs. Ini hampir sama, kecuali bahwa kita perlu memberikan registerTypesebuah dropTargetimplementasi:

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

Seret Sumber + Jatuhkan Target Dalam Satu Komponen

Katakanlah kita sekarang ingin pengguna dapat menarik keluar gambar ImageBlock. Kami hanya perlu menambahkan yang sesuai dragSourceuntuk itu dan beberapa penangan:

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

Apa Lagi yang Mungkin?

Saya belum membahas semuanya tetapi mungkin untuk menggunakan API ini dalam beberapa cara lagi:

  • Gunakan getDragState(type)dangetDropState(type) untuk mempelajari apakah menyeret aktif dan menggunakannya untuk mengalihkan kelas atau atribut CSS;
  • Tentukan dragPreviewuntuk Imagemenggunakan gambar sebagai tempat penampung seret (gunakan ImagePreloaderMixinuntuk memuatnya);
  • Katakanlah, kami ingin membuat pemesanan ulang ImageBlocks. Kami hanya membutuhkan mereka untuk diimplementasikan dropTargetdan dragSourceuntuk ItemTypes.BLOCK.
  • Misalkan kita menambahkan jenis blok lainnya. Kita dapat menggunakan kembali logika penyusunan ulang mereka dengan menempatkannya dalam sebuah mixin.
  • dropTargetFor(...types) memungkinkan untuk menentukan beberapa jenis sekaligus, sehingga satu zona tetesan dapat menangkap berbagai jenis.
  • Saat Anda membutuhkan kontrol yang lebih halus, sebagian besar metode akan meneruskan peristiwa tarik yang menyebabkannya sebagai parameter terakhir.

Untuk dokumentasi terbaru dan instruksi instalasi, kunjungi repo react-dnd di Github .

Dan Abramov
sumber
6
Apa kesamaan seret dan lepas dan seret mouse selain menggunakan mouse? Jawaban Anda sama sekali tidak terkait dengan pertanyaan dan jelas merupakan iklan perpustakaan.
polkovnikov.ph
5
Sepertinya 29 orang menganggapnya terkait dengan pertanyaan itu. React DnD juga memungkinkan Anda menerapkan penarikan mouse dengan baik. Saya akan berpikir lebih baik daripada membagikan pekerjaan saya secara gratis dan mengerjakan contoh dan dokumentasi yang luas di lain waktu sehingga saya tidak perlu menghabiskan waktu membaca komentar yang tajam.
Dan Abramov
7
Ya, saya sangat tahu itu. Fakta bahwa Anda memiliki dokumentasi di tempat lain tidak berarti ini adalah jawaban atas pertanyaan yang diberikan. Anda bisa saja menulis "gunakan Google" untuk hasil yang sama. 29 suara positif disebabkan oleh postingan yang panjang oleh orang yang dikenal, bukan karena kualitas jawabannya.
polkovnikov.ph
tautan yang diperbarui ke contoh resmi barang yang dapat diseret dengan bebas dengan react-dnd: basic , advanced
uryga
@DanAbramov Saya menghargai upaya Anda untuk membuat pos, tanpanya saya mungkin tidak akan tertarik untuk bereaksi begitu cepat. Seperti banyak dari produk Anda, pada awalnya tampak tidak perlu rumit , tetapi disatukan dengan cara yang sangat profesional (melalui dokumentasi Anda, saya biasanya belajar satu atau dua hal tentang rekayasa perangkat lunak ...) Saya telah membaca "mereka yang tidak tahu unix ditakdirkan untuk mengimplementasikannya kembali ... buruk, "dan saya merasa hal serupa mungkin berlaku untuk banyak kreasi Anda (termasuk yang ini).
Nathan Chappell
23

Jawaban Jared Forsyth salah besar dan ketinggalan zaman. Ini mengikuti seluruh rangkaian antipattern seperti penggunaanstopPropagation , menginisialisasi status dari props , penggunaan jQuery, objek bersarang dalam status dan memiliki beberapa draggingbidang status ganjil . Jika ditulis ulang, solusinya adalah sebagai berikut, tetapi tetap memaksa rekonsiliasi DOM virtual pada setiap centang gerakan mouse dan tidak terlalu berkinerja.

UPD. Jawaban saya sangat salah dan ketinggalan zaman. Sekarang kode tersebut mengurangi masalah siklus hidup komponen React yang lambat dengan menggunakan penangan kejadian asli dan pembaruan gaya, menggunakannya transformkarena tidak mengarah ke perubahan posisi, dan menghambat perubahan DOM requestAnimationFrame. Sekarang secara konsisten 60 FPS untuk saya di setiap browser yang saya coba.

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

dan sedikit 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;
}

Contoh di JSFiddle.

polkovnikov.ph
sumber
2
Terima kasih untuk ini, ini jelas bukan solusi yang paling berkinerja tetapi mengikuti praktik terbaik membangun aplikasi saat ini.
Spesialis
1
@ryanj Tidak, nilai default adalah jahat, itulah masalahnya. Apa tindakan yang tepat saat alat peraga berubah? Haruskah kami menyetel ulang status ke default baru? Haruskah kita membandingkan nilai default baru dengan nilai default lama untuk menyetel ulang status ke default hanya jika default berubah? Tidak ada cara untuk membatasi pengguna agar hanya menggunakan nilai konstan, dan tidak ada yang lain. Itulah mengapa itu antipattern. Nilai default harus dibuat secara eksplisit melalui komponen tingkat tinggi (yaitu untuk seluruh kelas, bukan untuk suatu objek), dan tidak boleh ditetapkan melalui props.
polkovnikov.ph
1
Saya sangat tidak setuju - status komponen adalah tempat yang sangat baik untuk menyimpan data yang khusus untuk UI komponen, yang tidak memiliki relevansi dengan aplikasi secara keseluruhan, misalnya. Tanpa bisa berpotensi melewatkan nilai default sebagai props dalam beberapa kasus, opsi untuk mengambil data post-mount ini terbatas dan dalam banyak (kebanyakan?) Keadaan kurang diinginkan daripada liku-liku sekitar komponen yang berpotensi diteruskan prop someDefaultValue berbeda di a tanggal kemudian. Saya tidak menganjurkannya sebagai praktik terbaik atau semacamnya, itu sama sekali tidak berbahaya seperti yang Anda sarankan imo
ryan j
2
Solusi yang sangat sederhana dan elegan. Saya senang melihat pendapat saya tentang itu agak mirip. Saya punya satu pertanyaan: Anda menyebutkan kinerja yang buruk, apa yang akan Anda usulkan untuk mencapai fitur serupa dengan mempertimbangkan kinerja?
Guillaume M
1
Pokoknya kita punya pengait sekarang, dan saya harus memperbarui jawaban sekali lagi segera.
polkovnikov.ph
14

Saya telah memperbarui solusi polkovnikov.ph ke React 16 / ES6 dengan peningkatan seperti penanganan sentuh dan snap ke grid yang saya butuhkan untuk sebuah game. Memasukkan ke kisi meringankan masalah kinerja.

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
sumber
hai @anyhotcountry untuk apa Anda menggunakan koefisien gridX ?
AlexNikonov
1
@AlexNikonov adalah ukuran kisi (snap-to) di arah x. Direkomendasikan untuk memiliki gridX dan gridY> 1 untuk meningkatkan kinerja.
anyhotcountry
Ini bekerja cukup baik untuk saya. Pada perubahan yang saya buat di fungsi onStart (): menghitung relX dan mengandalkan saya menggunakan e.clienX-this.props.x. Ini memungkinkan saya untuk menempatkan komponen yang dapat diseret di dalam wadah induk daripada bergantung pada seluruh halaman yang menjadi area tarik. Saya tahu ini adalah komentar yang terlambat tetapi hanya ingin mengucapkan terima kasih.
Geoff
11

react-draggable juga mudah digunakan. 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')
);

Dan index.html saya:

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

Anda membutuhkan gaya mereka, yang pendek, atau Anda tidak mendapatkan perilaku yang diharapkan. Saya lebih menyukai perilakunya daripada beberapa kemungkinan pilihan lain, tetapi ada juga sesuatu yang disebut react-resizable-and-movable . Saya mencoba mengubah ukuran bekerja dengan dapat diseret, tetapi sejauh ini tidak ada kegembiraan.

Joseph Larson
sumber
9

Berikut pendekatan modern sederhana untuk ini dengan useState, useEffectdan useRefdi 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
codewithfeeling
sumber
1
Ini sepertinya jawaban paling mutakhir di sini.
codyThompson
2

Saya ingin menambahkan file Skenario ke 3

Posisi bergerak tidak disimpan dengan cara apapun. Anggap saja sebagai gerakan mouse - kursor Anda bukan komponen React, bukan?

Yang Anda lakukan hanyalah menambahkan prop seperti "draggable" ke komponen Anda dan aliran peristiwa menyeret yang akan memanipulasi 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
    };
},

Dalam hal ini, manipulasi DOM adalah hal yang elegan (saya tidak pernah berpikir saya akan mengatakan ini)

Thomas Deutsch
sumber
1
dapatkah Anda mengisi setXandYfungsi dengan komponen imajiner?
Thellimist
2

Inilah jawaban 2020 dengan Hook:

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

Kemudian komponen yang menggunakan pengait:


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>
  );
}
Evan Conrad
sumber
1

Saya telah memperbarui kelas menggunakan referensi karena semua solusi yang saya lihat di sini memiliki hal-hal yang tidak lagi didukung atau akan segera disusutkan ReactDOM.findDOMNode. Dapat menjadi orang tua untuk komponen anak atau sekelompok anak :)

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;

Paul Ologeh
sumber