Metodo consigliato per rendere trascinabile il componente / div React


97

Voglio creare un componente React trascinabile (cioè riposizionabile dal mouse), che sembra coinvolgere necessariamente gestori di eventi globali e sparsi. Posso farlo in modo sporco, con una variabile globale nel mio file JS, e probabilmente potrei anche avvolgerlo in una bella interfaccia di chiusura, ma voglio sapere se c'è un modo che si adatta meglio a React.

Inoltre, dal momento che non l'ho mai fatto in JavaScript grezzo prima, mi piacerebbe vedere come lo fa un esperto, per assicurarmi di aver gestito tutti i casi d'angolo, soprattutto per quanto riguarda React.

Grazie.


In realtà, sarei almeno altrettanto felice con una spiegazione in prosa come in codice, o anche solo, "lo stai facendo bene". Ma ecco un JSFiddle del mio lavoro finora: jsfiddle.net/Z2JtM
Andrew Fleenor

Sono d'accordo che questa sia una domanda valida, dato che ci sono pochissimi esempi di codice di reazione da guardare attualmente
Jared Forsyth

1
Ho trovato una semplice soluzione HTML5 per il mio caso d'uso: youtu.be/z2nHLfiiKBA . Potrebbe aiutare qualcuno !!
Prem il

Prova questo. È un semplice HOC trasformare elementi avvolti in trascinabili npmjs.com/package/just-drag
Shan

Risposte:


111

Probabilmente dovrei trasformarlo in un post sul blog, ma ecco un esempio piuttosto concreto.

I commenti dovrebbero spiegare le cose abbastanza bene, ma fammi sapere se hai domande.

Ed ecco il violino con cui suonare: 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))
  }
})

Pensieri sulla proprietà statale, ecc.

"Chi dovrebbe possedere quale stato" è una domanda importante a cui rispondere, fin dall'inizio. Nel caso di un componente "trascinabile", ho potuto vedere alcuni scenari diversi.

scenario 1

il genitore dovrebbe possedere la posizione corrente del trascinabile. In questo caso, il trascinabile sarebbe ancora proprietario dello stato "sto trascinando", ma chiamerebbethis.props.onChange(x, y) ogni volta che si verifica un evento di rimozione del mouse.

Scenario 2

il genitore ha solo bisogno di possedere la "posizione non mobile", quindi il trascinabile sarebbe proprietario della sua "posizione di trascinamento", ma al primo piano richiamerebbe this.props.onChange(x, y)e rimanderebbe la decisione finale al genitore. Se al genitore non piace dove è finito il trascinabile, semplicemente non aggiornerà il suo stato e il trascinabile "tornerà" alla sua posizione iniziale prima di trascinare.

Mixin o componente?

@ssorallen ha sottolineato che, poiché "trascinabile" è più un attributo che una cosa in sé, potrebbe servire meglio come mixin. La mia esperienza con i mixin è limitata, quindi non ho visto come potrebbero aiutare o intromettersi in situazioni complicate. Questa potrebbe essere l'opzione migliore.


4
Ottimo esempio. Questo sembra più appropriato come una Mixinclasse completa poiché "Trascinabile" non è effettivamente un oggetto, è un'abilità di un oggetto.
Ross Allen

2
Ci ho giocato un po ', sembra che trascinare fuori il suo genitore non faccia nulla, ma ogni sorta di cose strane accadono quando viene trascinato in un altro componente di reazione
Gorkem Yurtseven

11
Puoi rimuovere la dipendenza jquery facendo: var computedStyle = window.getComputedStyle(this.getDOMNode()); pos = { top: parseInt(computedStyle.top), left: parseInt(computedStyle.left) }; Se stai usando jquery con react probabilmente stai facendo qualcosa di sbagliato;) Se hai bisogno di qualche plugin jquery trovo che di solito sia più semplice e meno codice riscriverlo in pure react.
Matt Crinklaw-Vogt

7
Volevo solo dare seguito al commento sopra di @ MattCrinklaw-Vogt per dire che una soluzione più a prova di proiettile è usare this.getDOMNode().getBoundingClientRect(): getComputedStyle può produrre qualsiasi proprietà CSS valida, incluso autonel qual caso il codice sopra risulterà in un file NaN. Vedi l'articolo MDN: developer.mozilla.org/en-US/docs/Web/API/Element/…
Andru

2
E per dare seguito a this.getDOMNode()ciò, è stato deprecato. Usa un ref per ottenere il nodo dom. facebook.github.io/react/docs/…
Chris Sattinger

63

Ho implementato React-dnd , un flessibile mixin HTML5 drag-and-drop per React con pieno controllo DOM.

Le librerie drag-and-drop esistenti non si adattavano al mio caso d'uso, quindi ho scritto le mie. È simile al codice che abbiamo eseguito per circa un anno su Stampsy.com, ma riscritto per sfruttare React e Flux.

Requisiti chiave che avevo:

  • Emette zero DOM o CSS propri, lasciandolo ai componenti che consumano;
  • Imporre la minor struttura possibile al consumo di componenti;
  • Usa il drag and drop HTML5 come backend principale ma rendi possibile aggiungere backend diversi in futuro;
  • Come l'API HTML5 originale, enfatizza il trascinamento dei dati e non solo le "visualizzazioni trascinabili";
  • Nascondi le stranezze dell'API HTML5 dal codice che consuma;
  • Componenti diversi possono essere "sorgenti di trascinamento" o "destinazioni di rilascio" per diversi tipi di dati;
  • Consentire a un componente di contenere diverse sorgenti di trascinamento e obiettivi di rilascio quando necessario;
  • Semplifica la modifica dell'aspetto delle destinazioni di rilascio se i dati compatibili vengono trascinati o spostati con il mouse;
  • Semplifica l'uso delle immagini per trascinare le miniature invece degli screenshot degli elementi, aggirando le stranezze del browser.

Se ti sembrano familiari, continua a leggere.

Utilizzo

Fonte di trascinamento semplice

Innanzitutto, dichiara i tipi di dati che possono essere trascinati.

Questi vengono utilizzati per verificare la "compatibilità" delle sorgenti di trascinamento e delle destinazioni di rilascio:

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

(Se non disponi di più tipi di dati, questa libreria potrebbe non essere adatta a te.)

Quindi, creiamo un componente trascinabile molto semplice che, quando trascinato, rappresenta 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)} />
    );
  }
);

Specificando configureDragDrop, diciamoDragDropMixin il comportamento del drag-drop di questo componente. Entrambi i componenti trascinabili e droppabili utilizzano lo stesso mixin.

All'interno configureDragDrop, dobbiamo richiedere registerTypeciascuna delle nostre abitudini ItemTypessupportate dal componente. Ad esempio, potrebbero esserci diverse rappresentazioni di immagini nella tua app e ognuna fornirebbe un dragSourceperItemTypes.IMAGE .

A dragSourceè solo un oggetto che specifica come funziona la sorgente di trascinamento. È necessario implementare beginDragper restituire l'elemento che rappresenta i dati che stai trascinando e, facoltativamente, alcune opzioni che regolano l'interfaccia utente di trascinamento. È possibile opzionalmente implementare canDragper vietare il trascinamento o endDrag(didDrop)per eseguire una logica quando il rilascio si è verificato (o meno). E puoi condividere questa logica tra i componenti lasciando che un mixin condiviso generi dragSourceper loro.

Infine, è necessario utilizzare {...this.dragSourceFor(itemType)}su alcuni (uno o più) elementi renderper collegare i gestori di trascinamento. Ciò significa che puoi avere più "maniglie di trascinamento" in un elemento e possono anche corrispondere a diversi tipi di elemento. (Se non hai familiarità con JSX Spread Attributes sintassi degli , dai un'occhiata).

Target di rilascio semplice

Diciamo che vogliamo ImageBlockessere un obiettivo di rilascio per IMAGEs. È più o meno lo stesso, tranne per il fatto che dobbiamo dareregisterTypedropTarget un'implementazione:

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

Trascina sorgente + rilascio destinazione in un componente

Supponiamo che ora vogliamo che l'utente sia in grado di trascinare un'immagine fuori da ImageBlock. Dobbiamo solo aggiungere appropriatodragSource ad esso e alcuni gestori:

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

Cos'altro è possibile?

Non ho coperto tutto ma è possibile utilizzare questa API in alcuni altri modi:

  • Uso getDragState(type) egetDropState(type) per sapere se il trascinamento è attivo e usalo per attivare / disattivare classi o attributi CSS;
  • Specificare dragPreviewper Imageutilizzare le immagini come segnaposto di trascinamento (utilizzareImagePreloaderMixin per caricarle);
  • Diciamo, vogliamo renderlo ImageBlocksriordinabile. Abbiamo solo bisogno di loro per implementare dropTargete dragSourceperItemTypes.BLOCK .
  • Supponiamo di aggiungere altri tipi di blocchi. Possiamo riutilizzare la loro logica di riordino inserendola in un mixin.
  • dropTargetFor(...types) consente di specificare diversi tipi contemporaneamente, quindi una zona di rilascio può catturare molti tipi diversi.
  • Quando è necessario un controllo più dettagliato, alla maggior parte dei metodi viene passato l'evento di trascinamento che li ha causati come ultimo parametro.

Per la documentazione e le istruzioni di installazione aggiornate, vai al repository react-dnd su Github .


5
Cosa hanno in comune il trascinamento e il trascinamento del mouse oltre all'uso del mouse? La tua risposta non è affatto correlata a una domanda e chiaramente è una pubblicità di biblioteca.
polkovnikov.ph

5
Sembra che 29 persone l'abbiano trovata correlata alla domanda. React DnD ti consente anche di implementare il trascinamento del mouse troppo bene. La prossima volta penserò meglio che condividere il mio lavoro gratuitamente e lavorare su esempi e ampia documentazione, in modo da non dover perdere tempo a leggere commenti irriverenti.
Dan Abramov

7
Sì, lo so perfettamente. Il fatto che tu abbia documentazione altrove non significa che questa sia una risposta alla domanda data. Avresti potuto scrivere "usa Google" per lo stesso risultato. 29 voti positivi sono dovuti a un lungo post di una persona conosciuta, non alla qualità della risposta.
polkovnikov.ph

link aggiornati ad esempi ufficiali di materiale liberamente trascinabile con react-dnd: basic , advanced
uryga

23

La risposta di Jared Forsyth è orribilmente sbagliata e obsoleta. Segue un intero set di antipattern come l' utilizzo distopPropagation , l' inizializzazione dello stato dagli oggetti di scena , l'uso di jQuery, oggetti annidati nello stato e ha alcuni stranidragging campo di stato . In caso di riscrittura, la soluzione sarà la seguente, ma forza comunque la riconciliazione DOM virtuale a ogni tick di movimento del mouse e non è molto performante.

UPD. La mia risposta era orribilmente sbagliata e obsoleta. Ora il codice allevia i problemi del ciclo di vita del componente React lento utilizzando gestori di eventi nativi e aggiornamenti di stile, utilizza transformin quanto non porta a reflow e limita le modifiche al DOM requestAnimationFrame. Ora sono costantemente 60 FPS per me in ogni browser che ho provato.

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

e un po 'di 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;
}

Esempio su JSFiddle.


2
Grazie per questo, questa non è sicuramente la soluzione più performante, ma segue le migliori pratiche di creazione di applicazioni odierne.
Spets

1
@ryanj No, i valori predefiniti sono malvagi, questo è il problema. Qual è l'azione corretta quando cambiano gli oggetti di scena? Dobbiamo ripristinare lo stato al nuovo valore predefinito? Dovremmo confrontare il nuovo valore predefinito con un vecchio valore predefinito per ripristinare lo stato su predefinito solo quando l'impostazione predefinita è cambiata? Non c'è modo di limitare l'utente a utilizzare solo un valore costante e nient'altro. Ecco perché è un antipattern. I valori predefiniti dovrebbero essere creati esplicitamente tramite componenti di ordine elevato (cioè per l'intera classe, non per un oggetto) e non dovrebbero mai essere impostati tramite props.
polkovnikov.ph

1
Rispettosamente non sono d'accordo: lo stato del componente è un luogo eccellente per archiviare i dati specifici dell'interfaccia utente di un componente, che non ha rilevanza per l'app nel suo insieme, ad esempio. Senza essere in grado di passare potenzialmente i valori predefiniti come oggetti di scena in alcuni casi, le opzioni per il recupero di questi dati dopo il montaggio sono limitate e in molte (la maggior parte?) Circostanze meno desiderabili dei capricci intorno a un componente che potenzialmente viene passato a un puntello diverso di valore predefinito in un data successiva. Non lo sto sostenendo come best practice o qualcosa del genere, semplicemente non è così dannoso come stai suggerendo imo
ryan j

2
Soluzione davvero molto semplice ed elegante. Sono felice di vedere che il mio punto di vista è stato simile. Ho una domanda: parli di prestazioni scadenti, cosa proporresti per ottenere una funzione simile tenendo conto delle prestazioni?
Guillaume M

1
Comunque ora abbiamo dei ganci e devo aggiornare di nuovo una risposta presto.
polkovnikov.ph

13

Ho aggiornato la soluzione polkovnikov.ph a React 16 / ES6 con miglioramenti come la gestione del tocco e lo snap a una griglia che è ciò di cui ho bisogno per un gioco. L'aggancio a una griglia allevia i problemi di prestazioni.

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;

ciao @anyhotcountry per cosa usi il coefficiente gridX ?
AlexNikonov

1
@AlexNikonov è la dimensione della griglia (snap-to) nella direzione x. Si consiglia di avere gridX e gridY> 1 per migliorare le prestazioni.
anyhotcountry

Questo ha funzionato abbastanza bene per me. Sulla modifica che ho fatto nella funzione onStart (): calcolando relX e relY ho usato e.clienX-this.props.x. Questo mi ha permesso di posizionare il componente trascinabile all'interno di un contenitore principale piuttosto che a seconda che l'intera pagina fosse l'area di trascinamento. So che è un commento in ritardo ma volevo solo dire grazie.
Geoff

11

Reagire trascinabile è anche facile da usare. 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')
);

E il mio index.html:

<html>
    <head>
        <title>Testing Draggable Windows</title>
        <link rel="stylesheet" type="text/css" href="style.css" />
    </head>
    <body>
        <div id="content"></div>
        <script type="text/javascript" src="bundle.js" charset="utf-8"></script>    
    <script src="http://localhost:8080/webpack-dev-server.js"></script>
    </body>
</html>

Hai bisogno dei loro stili, che è breve, o non ottieni il comportamento previsto. Mi piace il comportamento più di alcune delle altre scelte possibili, ma c'è anche qualcosa chiamato React-Resizable-and-Movable . Sto cercando di ottenere il ridimensionamento lavorando con trascinabile, ma finora non è stato piacevole.


8

Ecco un semplice approccio moderno a questo con useState, useEffecte useRefin 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

Questa sembra essere la risposta più aggiornata qui.
codyThompson

2

Vorrei aggiungere un terzo scenario

La posizione di spostamento non viene salvata in alcun modo. Consideralo come un movimento del mouse: il tuo cursore non è un componente React, giusto?

Tutto quello che fai è aggiungere un oggetto di scena come "trascinabile" al tuo componente e un flusso di eventi di trascinamento che manipoleranno il dominio.

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

In questo caso, una manipolazione del DOM è una cosa elegante (non avrei mai pensato di dirlo)


1
potresti riempire la setXandYfunzione con un componente immaginario?
Thellimist

0

Ho aggiornato la classe utilizzando refs poiché tutte le soluzioni che vedo qui hanno cose che non sono più supportate o saranno presto ammortizzate come ReactDOM.findDOMNode. Può essere il genitore di un componente figlio o di un gruppo di figli :)

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;

0

Ecco una risposta del 2020 con un 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];
}

Quindi un componente che utilizza l'hook:


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>
  );
}
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.