React bileşenini / div'i sürüklenebilir yapmanın önerilen yolu


100

Küresel durum ve dağınık olay işleyicileri gerektiriyor gibi görünen sürüklenebilir (yani fare ile yeniden konumlandırılabilir) bir React bileşeni yapmak istiyorum. Bunu, JS dosyamdaki global bir değişkenle kirli bir şekilde yapabilirim ve muhtemelen onu hoş bir kapatma arayüzüne bile sarabilirim, ancak React ile daha iyi örtüşen bir yol olup olmadığını bilmek istiyorum.

Ayrıca, bunu daha önce ham JavaScript'te hiç yapmadığım için, bir uzmanın bunu nasıl yaptığını görmek isterim, özellikle de React ile ilgili olan tüm köşe durumları ele aldığımdan emin olmak için.

Teşekkürler.


Aslında, düzyazı bir açıklamadan en az kod kadar mutlu olurum, hatta sadece "iyi yapıyorsun". Ama işte şimdiye kadarki çalışmalarımın bir JSFiddle'ı: jsfiddle.net/Z2JtM
Andrew Fleenor

Şu anda bakılması gereken çok az tepki kodu örneği olduğu göz önüne alındığında, bunun geçerli bir soru olduğunu kabul ediyorum
Jared Forsyth

1
Kullanım durumum için basit bir HTML5 çözümü buldum - youtu.be/z2nHLfiiKBA . Birine yardımcı olabilir !!
Prem

Bunu dene. Sarılmış
Shan

Yanıtlar:


114

Muhtemelen bunu bir blog gönderisine dönüştürmeliyim, ama işte oldukça sağlam bir örnek.

Yorumlar her şeyi oldukça iyi açıklamalıdır, ancak sorularınız olursa bana bildirin.

Ve işte oynayabileceğiniz keman: 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))
  }
})

Devlet mülkiyeti vb. Üzerine düşünceler

"Hangi eyalete kim sahip olmalı", en baştan yanıtlanması gereken önemli bir sorudur. Bir "sürüklenebilir" bileşen durumunda, birkaç farklı senaryo görebilirim.

Senaryo 1

üst öğe, sürüklenebilir ürünün mevcut konumuna sahip olmalıdır. Bu durumda, sürüklenebilir "sürüklüyor muyum" durumuna sahip olmaya devam eder, ancak this.props.onChange(x, y)her fare hareketi gerçekleştiğinde çağırır .

Senaryo 2

ebeveynin yalnızca "hareket etmeyen konuma" sahip olması gerekir ve bu nedenle sürüklenebilir, "sürükleme konumuna" sahip olur, ancak fareden this.props.onChange(x, y)sonra son kararı ebeveyne çağırır ve erteleyecektir. Ebeveyn sürüklenebilirin nerede sona erdiğini beğenmiyorsa, durumunu güncellemeyecek ve sürüklenebilir, sürüklemeden önce ilk konumuna "geri dönecektir".

Mixin mi, bileşen mi?

@ssorallen, "sürüklenebilir" in kendi başına bir şeyden çok bir öznitelik olduğundan, bir mixin olarak daha iyi hizmet edebileceğine işaret etti. Mixins deneyimim sınırlıdır, bu yüzden karmaşık durumlarda nasıl yardımcı olabileceklerini veya engel olabileceklerini görmedim. Bu en iyi seçenek olabilir.


4
Harika bir örnek. Mixin"Sürüklenebilir" aslında bir nesne değil, bir nesnenin yeteneği olduğundan, bu tam bir sınıftan daha uygun görünüyor .
Ross Allen

2
Onunla biraz oynadım, ebeveyninin dışına sürüklenmek hiçbir şey yapmıyor gibi görünüyor, ancak başka bir tepki bileşenine sürüklendiğinde her türlü tuhaf şey oluyor
Gorkem Yurtseven

11
Jquery bağımlılığını şu şekilde kaldırabilirsiniz: var computedStyle = window.getComputedStyle(this.getDOMNode()); pos = { top: parseInt(computedStyle.top), left: parseInt(computedStyle.left) }; React ile jquery kullanıyorsanız muhtemelen yanlış bir şey yapıyorsunuzdur;) Bazı jquery eklentisine ihtiyacınız varsa, onu saf react ile yeniden yazmanın genellikle daha kolay ve daha az kod olduğunu düşünüyorum.
Matt Crinklaw-Vogt

7
@ MattCrinklaw-Vogt tarafından, daha kurşun geçirmez bir çözümün kullanılması gerektiğini söylemek için yukarıdaki yorumu takip etmek istedim this.getDOMNode().getBoundingClientRect()- getComputedStyle, autoyukarıdaki kodun bir NaN. MDN makalesine bakın: developer.mozilla.org/en-US/docs/Web/API/Element/…
Andru

2
Ve yeniden takip etmek this.getDOMNode(), kullanımdan kaldırıldı. Dom düğümünü almak için bir ref kullanın. facebook.github.io/react/docs/…
Chris Sattinger

65

Tam DOM kontrolü ile React için esnek bir HTML5 sürükle ve bırak karışımı olan react-dnd'yi uyguladım .

Mevcut sürükle ve bırak kitaplıkları kullanım durumuma uymuyordu, bu yüzden kendim yazdım. Stampsy.com'da yaklaşık bir yıldır çalıştırdığımız koda benziyor, ancak React ve Flux'tan yararlanmak için yeniden yazıldı.

Sahip olduğum temel gereksinimler:

  • Kendi başına sıfır DOM veya CSS yayınlayarak onu tüketen bileşenlere bırakır;
  • Bileşenleri tüketmeye mümkün olduğunca az yapı uygulayın;
  • HTML5 sürükle ve bırak özelliğini birincil arka uç olarak kullanın, ancak gelecekte farklı arka uçlar eklemeyi mümkün kılın;
  • Orijinal HTML5 API'sinde olduğu gibi, yalnızca "sürüklenebilir görünümler" yerine verileri sürüklemeyi vurgulayın;
  • HTML5 API tuhaflıklarını tüketen koddan gizleyin;
  • Farklı bileşenler, farklı veri türleri için "sürükleme kaynakları" veya "bırakma hedefleri" olabilir;
  • Bir bileşenin birkaç sürükleme kaynağı içermesine izin verin ve gerektiğinde hedefi bırakın;
  • Uyumlu veriler sürükleniyorsa veya üzerinde geziniliyorsa, bırakma hedeflerinin görünümlerini değiştirmesini kolaylaştırın;
  • Tarayıcı tuhaflıklarını atlatarak öğe ekran görüntüleri yerine küçük resimleri sürüklemek için resimleri kullanmayı kolaylaştırın.

Bunlar size tanıdık geliyorsa, okumaya devam edin.

Kullanım

Basit Sürükle Kaynağı

İlk olarak, sürüklenebilecek veri türlerini belirtin.

Bunlar sürükle kaynaklarının ve bırakma hedeflerinin "uyumluluğunu" kontrol etmek için kullanılır:

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

(Birden fazla veri türünüz yoksa, bu kitaplık size göre olmayabilir.)

Ardından, sürüklendiğinde aşağıdakileri temsil eden çok basit bir sürüklenebilir bileşen yapalım 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)} />
    );
  }
);

Belirterek configureDragDrop, DragDropMixinbu bileşenin sürükle bırak davranışını anlatıyoruz . Hem sürüklenebilir hem de bırakılabilir bileşenler aynı karışımı kullanır.

İçeride configureDragDrop, bileşenin desteklediği registerTypeher özelliğimizi aramamız gerekiyor ItemTypes. Örneğin, uygulamanızda resimlerin birkaç temsili olabilir ve her biri birdragSource için ItemTypes.IMAGE.

A dragSource, sadece sürükleme kaynağının nasıl çalıştığını belirleyen bir nesnedir. beginDragSürüklediğiniz verileri temsil eden öğeyi döndürmek için ve isteğe bağlı olarak, sürükleme kullanıcı arayüzünü ayarlayan birkaç seçenek uygulamanız gerekir . İsteğe bağlı olarak canDragsürüklemeyi yasaklamak için uygulayabilir veya endDrag(didDrop)bırakma gerçekleştiğinde (veya olmadığında) bazı mantık yürütebilirsiniz. Ve paylaşılan bir karışımın oluşturulmasına izin vererek bu mantığı bileşenler arasında paylaşabilirsiniz.dragSource , onlar için .

Son olarak, sürükleme işleyicileri eklemek {...this.dragSourceFor(itemType)}için bazı (bir veya daha fazla) öğe üzerinde kullanmanız gerekir render. Bu, tek bir öğede birkaç "sürükleme tutamacına" sahip olabileceğiniz ve hatta farklı öğe türlerine karşılık gelebilecekleri anlamına gelir. ( JSX Spread Attributes sözdizimine aşina değilseniz , kontrol edin).

Basit Düşürme Hedefi

Diyelim ki s ImageBlockiçin bir düşme hedefi olmak istiyoruz IMAGE. Hemen hemen aynıdır, ancak registerTypebir dropTargetuygulama vermemiz gerekir :

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

Kaynağı Sürükle + Hedefi Bir Bileşene Bırak

Diyelim ki, kullanıcının bir görüntüyü dışarı sürükleyebilmesini istiyoruz ImageBlock. Sadece buna uygun dragSourceve birkaç işleyici eklememiz gerekiyor :

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

Başka Ne Mümkün?

Her şeyi ele almadım, ancak bu API'yi birkaç şekilde daha kullanmak mümkündür:

  • Kullanım getDragState(type)vegetDropState(type) sürükleyerek aktiftir ve CSS sınıflarını veya özelliklerini değiştirmek için kullanırsanız öğrenmek için;
  • Görüntüleri sürükleme yer tutucusu olarak kullanmak dragPreviewiçin belirtin Image( ImagePreloaderMixinbunları yüklemek için kullanın );
  • Diyelim ki yeniden düzenlenebilir hale getirmek ImageBlocksistiyoruz. Onlara sadece uygulamak için ihtiyacımız var dropTargetvedragSourceItemTypes.BLOCK .
  • Başka tür bloklar eklediğimizi varsayalım. Yeniden sıralama mantığını bir mixin içine yerleştirerek yeniden kullanabiliriz.
  • dropTargetFor(...types) aynı anda birkaç tür belirtmeye izin verir, böylece bir bırakma bölgesi birçok farklı türü yakalayabilir.
  • Daha ayrıntılı kontrole ihtiyaç duyduğunuzda, çoğu yöntem, onlara son parametre olarak neden olan sürükleme olayından geçer.

Güncel dokümantasyon ve kurulum talimatları için Github'da react-dnd deposuna gidin .


6
Sürükle ve bırak ile fareyle sürüklemenin fare kullanmaktan başka ortak yönü nedir? Cevabınız bir soruyla hiç alakalı değil ve açıkça bir kütüphane reklamı.
polkovnikov.ph

5
Görünüşe göre 29 kişi bunu soruyla ilgili buldu. React DnD ayrıca fare sürüklemeyi çok iyi uygulamanıza izin verir . Çalışmamı ücretsiz olarak paylaşmaktan ve bir dahaki sefere örnekler ve kapsamlı dokümantasyon üzerinde çalışmaktan daha iyi düşüneceğim, böylece alaycı yorumları okumak zorunda kalmayacağım.
Dan Abramov

7
Evet, bunu çok iyi biliyorum. Başka bir yerde belgelere sahip olmanız, bunun verilen soruya bir cevap olduğu anlamına gelmez. Aynı sonuç için "Google'ı kullan" yazabilirdin. 29 olumlu oy, cevabın kalitesinden değil, bilinen bir kişinin uzun bir gönderisinden kaynaklanıyor.
polkovnikov.ph

react-dnd ile serbestçe sürüklenebilir şeylerin resmi örneklerinin güncellenmiş bağlantıları: temel , gelişmiş
uryga

@DanAbramov Gönderiyi yapmak için gösterdiğiniz çabayı takdir ediyorum, o olmasaydı muhtemelen bu kadar çabuk tepki vermeyi düşünmezdim . Ürünlerinizin birçoğu gibi, başlangıçta gereksiz yere karmaşık görünebilir, ancak birinci derecede profesyonel bir şekilde bir araya getirilirse (belgelerinize bakarak genellikle yazılım mühendisliği hakkında bir iki şey öğrenirim ...) Unix'in onu yeniden uygulamaya mahkum olduğunu biliyorum ... kötü bir şekilde, "ve benzer bir şeyin birçok kreasyonunuz için geçerli olabileceğini hissediyorum (bu dahil).
Nathan Chappell

23

Jared Forsyth'ın cevabı korkunç derecede yanlış ve modası geçmiş. Bu, kullanımstopPropagation , props'dan durum başlatma , jQuery kullanımı, durumdaki iç içe nesneler gibi bir dizi antipattern izler ve bazı garip draggingdurum alanlarına sahiptir. Yeniden yazılırsa, çözüm aşağıdaki gibi olacaktır, ancak yine de her fare hareketi işaretinde sanal DOM mutabakatını zorlar ve çok başarılı değildir.

UPD. Cevabım korkunç derecede yanlış ve modası geçmişti. Artık kod, yerel olay işleyicileri ve stil güncellemelerini kullanarak yavaş React bileşeni yaşam döngüsü sorunlarını hafifletiyor transform, yeniden akışlara yol açmadığı için kullanıyor ve DOM değişikliklerini yavaşlatıyor requestAnimationFrame. Şimdi denediğim her tarayıcıda benim için sürekli olarak 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'),
);

ve biraz 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 ile ilgili örnek.


2
Bunun için teşekkürler, bu kesinlikle en yüksek performanslı çözüm değil, ancak günümüzdeki uygulama uygulamalarının en iyi uygulamalarını takip ediyor.
Spets

1
@ryanj Hayır, varsayılan değerler kötüdür, sorun bu. Sahne değiştiğinde doğru eylem nedir? Durumu yeni varsayılana sıfırlamalı mıyız? Sadece varsayılan değiştiğinde durumu varsayılana sıfırlamak için yeni varsayılan değeri eski bir varsayılan değerle karşılaştırmalı mıyız? Kullanıcıyı yalnızca sabit bir değer kullanacak şekilde kısıtlamanın bir yolu yoktur, başka hiçbir şey yoktur. Bu yüzden bir antipattern. Varsayılan değerler, yüksek dereceli bileşenler aracılığıyla açıkça oluşturulmalıdır (yani bir nesne için değil, tüm sınıf için) ve hiçbir zaman props aracılığıyla ayarlanmamalıdır.
polkovnikov.ph

1
Saygıyla katılmıyorum - bileşen durumu, örneğin bir bütün olarak uygulama ile hiçbir ilgisi olmayan bir bileşenin kullanıcı arayüzüne özgü verileri depolamak için mükemmel bir yerdir. Bazı durumlarda varsayılan değerleri potansiyel olarak props olarak geçiremeden, bu veriyi bağlama sonrası alma seçenekleri sınırlıdır ve birçok (çoğu?) Durumda, bir bileşenin etrafındaki belirsizliklerden daha az arzu edilir, potansiyel olarak bir sonraki tarih. Bunu en iyi uygulama veya benzeri bir şey olarak savunmuyorum, imo
ryan j

2
Gerçekten çok basit ve zarif bir çözüm. Bu konudaki bakış açımın benzer olduğunu görmekten mutluyum. Bir sorum var: düşük performanstan bahsediyorsunuz, performansı göz önünde bulundurarak benzer bir özelliği elde etmek için ne önerirsiniz?
Guillaume M

1
Her neyse şimdi kancalarımız var ve kısa süre sonra bir cevabı tekrar güncellemem gerekiyor.
polkovnikov.ph

14

Polkovnikov.ph çözümünü React 16 / ES6'ya, dokunmatik işleme ve bir oyun için ihtiyacım olan bir ızgaraya yapıştırma gibi geliştirmelerle güncelledim. Bir ızgaraya yapıştırmak performans sorunlarını azaltır.

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;

merhaba @anyhotcountry gridX katsayısını ne için kullanıyorsunuz ?
AlexNikonov

1
@AlexNikonov x yönündeki (tutturma) ızgaranın boyutudur. Performansı artırmak için gridX ve gridY> 1 olması tavsiye edilir.
anyhotcountry

Bu benim için oldukça iyi çalıştı. OnStart () işlevinde yaptığım değişiklik üzerine: relX ve relY'yi hesaplarken e.clienX-this.props.x kullandım. Bu, tüm sayfanın sürükleme alanı olmasına bağlı olmak yerine, sürüklenebilir bileşeni bir üst konteynerin içine yerleştirmeme izin verdi. Geç bir yorum olduğunu biliyorum ama sadece teşekkür etmek istedim.
Geoff

11

react-draggable'ın kullanımı da kolaydır. 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')
);

Ve index.html'm:

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

Kısa olan stillerine ihtiyacınız var, yoksa beklenen davranışı tam olarak alamıyorsunuz. Davranışı diğer olası seçeneklerden daha çok seviyorum, ama aynı zamanda yeniden boyutlandırılabilir ve taşınabilir denen bir şey de var . Sürüklenebilir ile çalışarak yeniden boyutlandırmaya çalışıyorum, ancak şimdiye kadar hiç sevinç yok.


9

İşte bu kadar basit, modern bir yaklaşım useState, useEffectve useRefES6 içinde.

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

1
Bu, buradaki en güncel cevap gibi görünüyor.
codyThompson

2

3. bir Senaryo eklemek istiyorum

Hareketli pozisyon hiçbir şekilde kaydedilmez. Bunu bir fare hareketi olarak düşünün - imleciniz bir React bileşeni değil, değil mi?

Tek yapmanız gereken, bileşeninize "sürüklenebilir" gibi bir destek ve domu işleyecek sürükleme olaylarının bir akışını eklemektir.

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

Bu durumda, DOM manipülasyonu zarif bir şeydir (Bunu söyleyeceğimi hiç düşünmemiştim)


1
setXandYişlevi hayali bir bileşenle doldurabilir misiniz ?
Thellimist

2

İşte bir Hook ile 2020 cevabı:

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

Ardından kancayı kullanan bir bileşen:


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

1

Burada gördüğüm tüm çözümlerde artık desteklenmeyen veya yakında amortismana tabi olacak şeyler olduğu için refs kullanarak sınıfı güncelledim ReactDOM.findDOMNode. Bir alt bileşenin veya bir grup çocuğun ebeveyni olabilir :)

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;

Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.