Forma recomendada de hacer que el componente React / div sea arrastrable

97

Quiero hacer un componente React que se pueda arrastrar (es decir, reposicionable con el mouse), que parece involucrar necesariamente el estado global y los controladores de eventos dispersos. Puedo hacerlo de la manera sucia, con una variable global en mi archivo JS, y probablemente podría incluso envolverlo en una interfaz de cierre agradable, pero quiero saber si hay una forma que se adapte mejor a React.

Además, dado que nunca antes había hecho esto en JavaScript sin formato, me gustaría ver cómo lo hace un experto, para asegurarme de que tengo todos los casos de esquina manejados, especialmente en lo que se relaciona con React.

Gracias.

Andrew Fleenor
fuente
En realidad, estaría al menos tan feliz con una explicación en prosa como con el código, o incluso simplemente, "lo estás haciendo bien". Pero aquí hay un JSFiddle de mi trabajo hasta ahora: jsfiddle.net/Z2JtM
Andrew Fleenor
Estoy de acuerdo en que esta es una pregunta válida, dado que hay muy pocos ejemplos de código de reacción para mirar actualmente
Jared Forsyth
1
Encontré una solución HTML5 simple para mi caso de uso: youtu.be/z2nHLfiiKBA . ¡¡Podría ayudar a alguien !!
Prem
Prueba esto. Es un HOC simple convertir elementos envueltos para que se puedan arrastrar npmjs.com/package/just-drag
Shan

Respuestas:

111

Probablemente debería convertir esto en una publicación de blog, pero aquí hay un ejemplo bastante sólido.

Los comentarios deberían explicar las cosas bastante bien, pero avíseme si tiene preguntas.

Y aquí está el violín para jugar: 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))
  }
})

Pensamientos sobre la propiedad estatal, etc.

"Quién debe ser dueño de qué estado" es una pregunta importante a responder, desde el principio. En el caso de un componente "arrastrable", pude ver algunos escenarios diferentes.

escenario 1

el padre debe poseer la posición actual del arrastrable. En este caso, el arrastrable aún poseería el estado "estoy arrastrando", pero llamaría this.props.onChange(x, y)cada vez que ocurra un evento mousemove.

Escenario 2

el padre solo necesita poseer la "posición sin movimiento", por lo que el arrastrable poseería su "posición de arrastre", pero en el mouse llamaría this.props.onChange(x, y)y pospondría la decisión final al padre. Si al padre no le gusta dónde terminó el arrastrable, simplemente no actualizará su estado, y el arrastrable volverá a su posición inicial antes de arrastrarlo.

¿Mixin o componente?

@ssorallen señaló que, debido a que "arrastrable" es más un atributo que una cosa en sí misma, podría servir mejor como mezcla. Mi experiencia con mixins es limitada, por lo que no he visto cómo podrían ayudar o interponerse en situaciones complicadas. Esta bien podría ser la mejor opción.

Jared Forsyth
fuente
4
Gran ejemplo. Esto parece más apropiado Mixinque una clase completa, ya que "Arrastrable" no es en realidad un objeto, es una capacidad de un objeto.
Ross Allen
2
Jugué
11
Puede eliminar la dependencia de jquery haciendo lo siguiente: var computedStyle = window.getComputedStyle(this.getDOMNode()); pos = { top: parseInt(computedStyle.top), left: parseInt(computedStyle.left) }; Si está usando jquery con react, probablemente esté haciendo algo mal;) Si necesita algún complemento de jquery, encuentro que generalmente es más fácil y menos código reescribirlo en puro react.
Matt Crinklaw-Vogt
7
Solo quería continuar con el comentario anterior de @ MattCrinklaw-Vogt para decir que se debe usar una solución más a prueba de balas this.getDOMNode().getBoundingClientRect(): getComputedStyle puede generar cualquier propiedad CSS válida, incluido autoen cuyo caso el código anterior dará como resultado un NaN. Consulte el artículo de MDN: developer.mozilla.org/en-US/docs/Web/API/Element/…
Andru
2
Y para dar seguimiento this.getDOMNode(), eso ha sido obsoleto. Utilice una referencia para obtener el nodo dom. facebook.github.io/react/docs/…
Chris Sattinger
63

Implementé react-dnd , un mixin flexible de arrastrar y soltar HTML5 para React con control DOM completo.

Las bibliotecas de arrastrar y soltar existentes no se ajustaban a mi caso de uso, así que escribí la mía propia. Es similar al código que hemos estado ejecutando durante aproximadamente un año en Stampsy.com, pero reescrito para aprovechar React y Flux.

Requisitos clave que tenía:

  • Emite cero DOM o CSS propio, dejándolo en manos de los componentes consumidores;
  • Imponer la menor estructura posible a los componentes consumidores;
  • Use HTML5 arrastrar y soltar como backend principal pero haga posible agregar diferentes backends en el futuro;
  • Al igual que la API HTML5 original, enfatice el arrastre de datos y no solo las "vistas que se pueden arrastrar";
  • Ocultar las peculiaridades de la API HTML5 del código consumidor;
  • Los diferentes componentes pueden ser "fuentes de arrastre" o "destinos de colocación" para diferentes tipos de datos;
  • Permitir que un componente contenga varias fuentes de arrastre y soltar destinos cuando sea necesario;
  • Facilite que los destinos para soltar cambien su apariencia si se arrastran o se desplazan datos compatibles
  • Facilite el uso de imágenes para arrastrar miniaturas en lugar de capturas de pantalla de elementos, evitando las peculiaridades del navegador.

Si te suenan familiares, sigue leyendo.

Uso

Fuente de arrastre simple

Primero, declare los tipos de datos que se pueden arrastrar.

Se utilizan para comprobar la "compatibilidad" de las fuentes de arrastre y los destinos de colocación:

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

(Si no tiene varios tipos de datos, es posible que esta biblioteca no sea para usted).

Luego, hagamos un componente arrastrable muy simple que, cuando se arrastra, representa 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)} />
    );
  }
);

Al especificar configureDragDrop, contamos DragDropMixinel comportamiento de arrastrar y soltar de este componente. Tanto los componentes que se pueden arrastrar como los que se pueden soltar utilizan la misma mezcla.

En el interior configureDragDrop, necesitamos llamar registerTypepara cada uno de nuestros ItemTypescomponentes personalizados que admite. Por ejemplo, puede haber varias representaciones de imágenes en su aplicación, y cada proporcionaría una dragSourcedeItemTypes.IMAGE .

A dragSourcees solo un objeto que especifica cómo funciona la fuente de arrastre. Debe implementar beginDragpara devolver el elemento que representa los datos que está arrastrando y, opcionalmente, algunas opciones que ajustan la IU de arrastre. Opcionalmente, puede implementar canDragpara prohibir el arrastre, o endDrag(didDrop)ejecutar alguna lógica cuando la caída ha ocurrido (o no). Y puede compartir esta lógica entre componentes dejando que se genere un mixin compartido dragSourcepara ellos.

Finalmente, debe usar {...this.dragSourceFor(itemType)}en algunos (uno o más) elementos renderpara adjuntar controladores de arrastre. Esto significa que puede tener varios "controles de arrastre" en un elemento, e incluso pueden corresponder a diferentes tipos de elementos. (Si no está familiarizado con los atributos de difusión de JSX sintaxis de , compruébelo).

Destino de caída simple

Digamos que queremos ImageBlockser un objetivo de caída para IMAGEs. Es más o menos lo mismo, excepto que necesitamos dar registerTypeuna dropTargetimplementación:

var { DragDropMixin } = require('react-dnd'),
    ItemTypes = require('./ItemTypes');

var ImageBlock = React.createClass({
  mixins: [DragDropMixin],

  configureDragDrop(registerType) {

    registerType(ItemTypes.IMAGE, {

      // dropTarget, when specified, is { acceptDrop(item)?, enter(item)?, over(item)?, leave(item)? }
      dropTarget: {
        acceptDrop(image) {
          // Do something with image! for example,
          DocumentActionCreators.setImage(this.props.blockId, image);
        }
      }
    });
  },

  render() {

    // {...this.dropTargetFor(ItemTypes.IMAGE)} will expand into
    // { onDragEnter: (handled by mixin), onDragOver: (handled by mixin), onDragLeave: (handled by mixin), onDrop: (handled by mixin) }.

    return (
      <div {...this.dropTargetFor(ItemTypes.IMAGE)}>
        {this.props.image &&
          <img src={this.props.image.url} />
        }
      </div>
    );
  }
);

Arrastrar origen + soltar destino en un componente

Supongamos que ahora queremos que el usuario pueda arrastrar una imagen fuera de ImageBlock. Solo necesitamos agregarle lo apropiado dragSourcey algunos controladores:

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

¿Qué más es posible?

No he cubierto todo, pero es posible usar esta API de varias maneras más:

  • Utilice getDragState(type)ygetDropState(type) para saber si el arrastre está activo y úselo para alternar clases o atributos de CSS;
  • Especifique si dragPreviewdesea Imageusar imágenes como marcadores de posición de arrastre (use ImagePreloaderMixinpara cargarlas);
  • Digamos, queremos hacer ImageBlocksreordenables. Solo los necesitamos para implementar dropTargety dragSourcepara ItemTypes.BLOCK.
  • Supongamos que agregamos otros tipos de bloques. Podemos reutilizar su lógica de reordenamiento colocándola en un mixin.
  • dropTargetFor(...types) permite especificar varios tipos a la vez, por lo que una zona de caída puede capturar muchos tipos diferentes.
  • Cuando necesita un control más detallado, a la mayoría de los métodos se les pasa el evento de arrastre que los causó como último parámetro.

Para obtener documentación e instrucciones de instalación actualizadas, diríjase a react-dnd repo en Github .

Dan Abramov
fuente
5
¿Qué tienen en común arrastrar y soltar y arrastrar el mouse además de usar un mouse? Su respuesta no está relacionada en absoluto con una pregunta y claramente es un anuncio de biblioteca.
polkovnikov.ph
5
Parece que 29 personas encontraron que estaba relacionado con la pregunta. React DnD también le permite implementar el arrastre del mouse muy bien. Pensaré mejor que compartir mi trabajo gratis y trabajar en ejemplos y documentación expansiva la próxima vez para no tener que perder tiempo leyendo comentarios sarcásticos.
Dan Abramov
7
Sí, lo sé perfectamente. El hecho de que tenga documentación en otro lugar no significa que esta sea una respuesta a la pregunta dada. Podría haber escrito "use Google" para obtener el mismo resultado. 29 votos positivos se deben a una publicación larga de una persona conocida, no a la calidad de la respuesta.
polkovnikov.ph
enlaces actualizados a ejemplos oficiales de cosas que se pueden arrastrar libremente con react-dnd: básico , avanzado
uryga
23

La respuesta de Jared Forsyth es horriblemente incorrecta y anticuada. Sigue un conjunto completo de antipatrones, como el uso destopPropagation , el estado de inicialización de los accesorios , el uso de jQuery, los objetos anidados en el estado y tiene algunosdragging campo de estado . Si se reescribe, la solución será la siguiente, pero todavía fuerza la reconciliación del DOM virtual en cada tic de movimiento del mouse y no es muy eficaz.

UPD. Mi respuesta fue horriblemente incorrecta y anticuada. Ahora, el código alivia los problemas del ciclo de vida lento de los componentes de React mediante el uso de controladores de eventos nativos y actualizaciones de estilo, utiliza transformya que no conduce a reflujos y acelera los cambios de DOM requestAnimationFrame. Ahora es constantemente 60 FPS para mí en cada navegador que probé.

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

y un poco de 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;
}

Ejemplo en JSFiddle.

polkovnikov.ph
fuente
2
Gracias por esto, definitivamente esta no es la solución con mayor rendimiento, pero sigue las mejores prácticas de creación de aplicaciones en la actualidad.
Fecha de lanzamiento
1
@ryanj No, los valores predeterminados son malos, ese es el problema. ¿Cuál es la acción adecuada cuando cambian los accesorios? ¿Deberíamos restablecer el estado al nuevo predeterminado? ¿Deberíamos comparar el nuevo valor predeterminado con un valor predeterminado anterior para restablecer el estado a predeterminado solo cuando el valor predeterminado cambió? No hay forma de restringir al usuario para que use solo un valor constante y nada más. Por eso es un antipatrón. Los valores predeterminados deben crearse explícitamente a través de componentes de orden superior (es decir, para toda la clase, no para un objeto), y nunca deben establecerse a través de accesorios.
polkovnikov.ph
1
No estoy de acuerdo respetuosamente: el estado del componente es un lugar excelente para almacenar datos que son específicos de la interfaz de usuario de un componente, que no tienen relevancia para la aplicación en su conjunto, por ejemplo. Sin poder pasar potencialmente valores predeterminados como accesorios en algunos casos, las opciones para recuperar estos datos después del montaje son limitadas y en muchas (¿la mayoría?) Circunstancias menos deseables que los caprichos en torno a un componente que potencialmente se pasa a un valor diferente de valor predeterminado en un fecha posterior. No lo estoy defendiendo como una mejor práctica ni nada por el estilo, simplemente no es tan dañino como sugieres imo
ryan j
2
Una solución muy simple y elegante. Estoy feliz de ver que mi opinión fue similar. Tengo una pregunta: mencionas un rendimiento deficiente, ¿qué propondrías para lograr una función similar teniendo en cuenta el rendimiento?
Guillaume M
1
De todos modos, ahora tenemos ganchos y tengo que actualizar una respuesta una vez más pronto.
polkovnikov.ph
13

He actualizado la solución polkovnikov.ph a React 16 / ES6 con mejoras como el manejo táctil y el ajuste a una cuadrícula, que es lo que necesito para un juego. Ajustar a una cuadrícula alivia los problemas de rendimiento.

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;
cualquier país
fuente
hola @anyhotcountry ¿para qué usas el coeficiente gridX ?
AlexNikonov
1
@AlexNikonov es el tamaño de la cuadrícula (snap-to) en la dirección x. Se recomienda tener gridX y gridY> 1 para mejorar el rendimiento.
anyhotcountry
Esto funcionó bastante bien para mí. En el cambio que hice en la función onStart (): calculando relX y relY usé e.clienX-this.props.x. Esto me permitió colocar el componente arrastrable dentro de un contenedor principal en lugar de depender de que toda la página sea el área de arrastre. Sé que es un comentario tardío, pero solo quería dar las gracias.
Geoff
11

react-draggable también es fácil de usar. 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')
);

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

Necesita sus estilos, que es corto, o no obtiene el comportamiento esperado. Me gusta el comportamiento más que algunas de las otras opciones posibles, pero también hay algo llamado reaccionar-redimensionable-y-movible . Estoy tratando de hacer que el cambio de tamaño funcione con arrastrable, pero no me gusta hasta ahora.

Joseph Larson
fuente
8

Aquí está un simple enfoque moderno de esto con useState, useEffecty useRefen la 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
codigo con sentimiento
fuente
Esta parece ser la respuesta más actualizada aquí.
codyThompson
2

Me gustaría agregar un tercer escenario

La posición de movimiento no se guarda de ninguna manera. Piense en ello como un movimiento del mouse: su cursor no es un componente de React, ¿verdad?

Todo lo que hace es agregar un accesorio como "arrastrable" a su componente y una secuencia de eventos de arrastre que manipularán el 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
    };
},

En este caso, una manipulación DOM es algo elegante (nunca pensé que diría esto)

Thomas Deutsch
fuente
1
¿Podrías llenar la setXandYfunción con un componente imaginario?
Thellimist
0

Actualicé la clase usando referencias, ya que todas las soluciones que veo aquí tienen cosas que ya no son compatibles o que pronto se depreciarán como ReactDOM.findDOMNode. Puede ser padre de un componente hijo o un grupo de hijos :)

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

Aquí hay una respuesta de 2020 con un gancho:

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

Luego, un componente que usa el gancho:


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
fuente