Forma recomendada de tornar o componente / div do React arrastável

97

Quero fazer um componente React arrastável (isto é, reposicionável pelo mouse), que parece necessariamente envolver o estado global e manipuladores de eventos dispersos. Posso fazer isso da maneira suja, com uma variável global em meu arquivo JS, e provavelmente poderia até envolvê-lo em uma interface de encerramento agradável, mas quero saber se há uma maneira que mescla melhor com o React.

Além disso, como nunca fiz isso em JavaScript bruto antes, gostaria de ver como um especialista faz isso, para ter certeza de que tenho todos os casos menores tratados, especialmente no que se refere ao React.

Obrigado.

Andrew Fleenor
fonte
Na verdade, eu ficaria pelo menos tão feliz com uma explicação em prosa quanto com o código, ou mesmo apenas, "você está fazendo isso bem". Mas aqui está um JSFiddle do meu trabalho até agora: jsfiddle.net/Z2JtM
Andrew Fleenor
Concordo que esta é uma pergunta válida, visto que existem muito poucos exemplos de código de reação para olhar atualmente
Jared Forsyth
1
Encontrei uma solução HTML5 simples para meu caso de uso - youtu.be/z2nHLfiiKBA . Pode ajudar alguém !!
Prem
Experimente isso. É um HOC simples para transformar elementos agrupados
Shan

Respostas:

111

Eu provavelmente deveria transformar isso em um post de blog, mas aqui está um exemplo bastante sólido.

Os comentários devem explicar as coisas muito bem, mas entre em contato se tiver dúvidas.

E aqui está o violino para brincar: 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))
  }
})

Reflexões sobre propriedade estatal, etc.

"Quem deve ser o proprietário de qual estado" é uma pergunta importante a ser respondida, desde o início. No caso de um componente "arrastável", posso ver alguns cenários diferentes.

Cenário 1

o pai deve possuir a posição atual do arrastável. Nesse caso, o arrastável ainda teria o estado "estou arrastando", mas chamaria this.props.onChange(x, y)sempre que um evento mousemove ocorrer.

Cenário 2

o pai só precisa possuir a "posição de não movimento", e então o arrastável possuiria sua "posição de arrastar", mas no mouseup ele chamaria this.props.onChange(x, y)e adiaria a decisão final para o pai. Se o pai não gosta de onde o arrastável acabou, ele simplesmente não atualizaria seu estado, e o arrastável "voltaria" à sua posição inicial antes de arrastar.

Mixin ou componente?

@ssorallen apontou que, como "draggable" é mais um atributo do que uma coisa em si, pode servir melhor como um mixin. Minha experiência com mixins é limitada, então não vi como eles podem ajudar ou atrapalhar em situações complicadas. Essa pode ser a melhor opção.

Jared Forsyth
fonte
4
Ótimo exemplo. Isso parece mais apropriado do Mixinque uma classe completa, já que "Arrastável" não é realmente um objeto, é a habilidade de um objeto.
Ross Allen
2
Eu brinquei com ele um pouco, parece que arrastar para fora de seu pai não faz nada, mas todos os tipos de coisas estranhas acontecem quando é arrastado para outro componente de reação
Gorkem Yurtseven
11
Você pode remover a dependência jquery fazendo: var computedStyle = window.getComputedStyle(this.getDOMNode()); pos = { top: parseInt(computedStyle.top), left: parseInt(computedStyle.left) }; Se você estiver usando jquery com react, provavelmente está fazendo algo errado;) Se precisar de algum plugin jquery, acho que geralmente é mais fácil e menos código reescrevê-lo em react puro.
Matt Crinklaw-Vogt
7
Só queria acompanhar o comentário acima de @ MattCrinklaw-Vogt para dizer que uma solução mais à prova de balas deve ser usada this.getDOMNode().getBoundingClientRect()- getComputedStyle pode gerar qualquer propriedade CSS válida, incluindo autonesse caso o código acima resultará em a NaN. Consulte o artigo MDN: developer.mozilla.org/en-US/docs/Web/API/Element/…
Andru
2
E para acompanhar this.getDOMNode(), isso foi descontinuado. Use um ref para obter o nó dom. facebook.github.io/react/docs/…
Chris Sattinger
63

Implementei o react-dnd , um mixin de arrastar e soltar HTML5 flexível para o React com controle DOM completo.

As bibliotecas existentes de arrastar e soltar não se adequavam ao meu caso de uso, então escrevi o meu próprio. É semelhante ao código que executamos há cerca de um ano no Stampsy.com, mas reescrito para aproveitar as vantagens do React e do Flux.

Principais requisitos que eu tinha:

  • Emite zero DOM ou CSS próprio, deixando para os componentes consumidores;
  • Imponha o mínimo de estrutura possível aos componentes de consumo;
  • Use o recurso arrastar e soltar do HTML5 como back-end primário, mas torne possível adicionar back-ends diferentes no futuro;
  • Como a API HTML5 original, enfatize o arrastar de dados e não apenas “visualizações arrastáveis”;
  • Ocultar peculiaridades da API HTML5 do código de consumo;
  • Diferentes componentes podem ser “fontes de arrastar” ou “destinos de soltar” para diferentes tipos de dados;
  • Permita que um componente contenha várias fontes de arrastar e destinos de soltar quando necessário;
  • Facilite a alteração da aparência dos alvos de soltar se dados compatíveis estiverem sendo arrastados ou pairados;
  • Facilite o uso de imagens para arrastar miniaturas em vez de capturas de tela de elementos, evitando peculiaridades do navegador.

Se isso soa familiar para você, continue lendo.

Uso

Fonte de arrasto simples

Primeiro, declare os tipos de dados que podem ser arrastados.

Eles são usados ​​para verificar a “compatibilidade” de fontes de arrastar e destinos de soltar:

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

(Se você não tiver vários tipos de dados, esta biblioteca pode não ser para você.)

Então, vamos fazer um componente arrastável muito simples que, quando arrastado, 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)} />
    );
  }
);

Ao especificar configureDragDrop, informamos DragDropMixino comportamento de arrastar e soltar desse componente. Os componentes arrastáveis ​​e soltáveis ​​usam o mesmo mixin.

Por dentro configureDragDrop, precisamos chamar registerTypecada um de nossos custom ItemTypesque esse componente suporta. Por exemplo, pode haver várias representações de imagens em seu aplicativo e cada uma forneceria um dragSourcefor ItemTypes.IMAGE.

A dragSourceé apenas um objeto que especifica como a fonte de arrastar funciona. Você deve implementar beginDragpara retornar o item que representa os dados que você está arrastando e, opcionalmente, algumas opções que ajustam a IU de arrastar. Você pode, opcionalmente, implementar canDragpara proibir arrastar ou endDrag(didDrop)para executar alguma lógica quando o soltar ocorreu (ou não). E você pode compartilhar essa lógica entre os componentes, permitindo que um mixin compartilhado seja gerado dragSourcepara eles.

Finalmente, você deve usar {...this.dragSourceFor(itemType)}em alguns (um ou mais) elementos renderpara anexar manipuladores de arrasto. Isso significa que você pode ter várias “alças de arrastar” em um elemento e elas podem até corresponder a diferentes tipos de itens. (Se você não estiver familiarizado com os atributos de propagação JSX sintaxe de , verifique).

Alvo de queda simples

Digamos que queremos ImageBlockser um alvo de soltar para IMAGEs. É praticamente o mesmo, exceto que precisamos fornecer registerTypeuma dropTargetimplementação:

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

Arraste a fonte + solte o destino em um componente

Digamos que agora desejamos que o usuário seja capaz de arrastar uma imagem para fora de ImageBlock. Precisamos apenas adicionar apropriado dragSourcea ele e alguns manipuladores:

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

O que mais é possível?

Não abordei tudo, mas é possível usar esta API de mais algumas maneiras:

  • Use getDragState(type)egetDropState(type) para saber se arrastar está ativo e use-o para alternar classes ou atributos CSS;
  • Especifique dragPreviewpara Imageusar imagens como espaços reservados para arrastar (use ImagePreloaderMixinpara carregá-las);
  • Digamos, queremos torná-lo ImageBlocksreordenável. Só precisamos deles para implementar dropTargete dragSourcepara ItemTypes.BLOCK.
  • Suponha que adicionemos outros tipos de blocos. Podemos reutilizar sua lógica de reordenamento, colocando-a em um mixin.
  • dropTargetFor(...types) permite especificar vários tipos de uma vez, de modo que uma zona de lançamento pode capturar muitos tipos diferentes.
  • Quando você precisa de um controle mais refinado, a maioria dos métodos recebe o evento de arrastar que os causou como o último parâmetro.

Para obter a documentação atualizada e as instruções de instalação, acesse o reac-dnd repo no Github .

Dan Abramov
fonte
5
O que arrastar e soltar e arrastar com o mouse têm em comum além de usar um mouse? Sua resposta não está relacionada a nenhuma pergunta e claramente é um anúncio de biblioteca.
polkovnikov.ph
5
Parece que 29 pessoas acharam que isso estava relacionado à questão. O React DnD também permite que você implemente arrastar o mouse perfeitamente. Pensarei melhor do que compartilhar meu trabalho de graça e trabalhar em exemplos e documentação extensa da próxima vez, para não ter que perder tempo lendo comentários sarcásticos.
Dan Abramov
7
Sim, eu sei disso perfeitamente. O fato de você ter documentação em outro lugar não significa que esta seja uma resposta para a pergunta dada. Você poderia ter escrito "usar Google" para o mesmo resultado. 29 votos positivos são devidos a uma longa postagem de uma pessoa conhecida, não por causa da qualidade da resposta.
polkovnikov.ph
links atualizados para exemplos oficiais de coisas livremente arrastáveis ​​com
react
23

A resposta de Jared Forsyth está terrivelmente errada e desatualizada. Ele segue todo um conjunto de antipadrões, como o uso destopPropagation , inicializando estado a partir de props , uso de jQuery, objetos aninhados em estado e tem algum draggingcampo de estado estranho . Se estiver sendo reescrito, a solução será a seguinte, mas ainda força a reconciliação virtual do DOM em cada movimento do mouse e não tem muito desempenho.

UPD. Minha resposta estava terrivelmente errada e desatualizada. Agora, o código alivia os problemas do ciclo de vida lento do componente React usando manipuladores de eventos nativos e atualizações de estilo, usa transformporque não leva a refluxos e limita as alterações de DOM requestAnimationFrame. Agora são consistentemente 60 FPS para mim em todos os navegadores que experimentei.

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 um pouco 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;
}

Exemplo no JSFiddle.

polkovnikov.ph
fonte
2
Obrigado por isso, definitivamente não é a solução de melhor desempenho, mas segue as melhores práticas de construção de aplicativos hoje.
Spets
1
@ryanj Não, os valores padrão são ruins, esse é o problema. Qual é a ação adequada quando os adereços mudam? Devemos redefinir o estado para o novo padrão? Devemos comparar o novo valor padrão com um valor padrão antigo para redefinir o estado para o padrão apenas quando o padrão mudou? Não há como restringir o usuário a usar apenas um valor constante e nada mais. É por isso que é um antipadrão. Os valores padrão devem ser criados explicitamente por meio de componentes de alta ordem (ou seja, para toda a classe, não para um objeto) e nunca devem ser definidos por meio de props.
polkovnikov.ph
1
Eu respeitosamente discordo - o estado do componente é um excelente lugar para armazenar dados que são específicos para a IU de um componente, que não tem relevância para o aplicativo como um todo, por exemplo. Sem ser capaz de potencialmente passar valores padrão como props em alguns casos, as opções para recuperar esses dados pós-montagem são limitadas e em muitas (na maioria?) Circunstâncias menos desejáveis ​​do que os caprichos em torno de um componente que pode ser passado por um outro prop someDefaultValue diferente em um data posterior. Não estou defendendo isso como prática recomendada ou qualquer coisa do tipo, simplesmente não é tão prejudicial quanto você está sugerindo
ryan j
2
Solução muito simples e elegante. Estou feliz em ver que minha opinião sobre isso foi semelhante. Eu tenho uma pergunta: você mencionou baixo desempenho, o que você proporia para alcançar um recurso semelhante com desempenho em mente?
Guillaume M
1
De qualquer forma, temos ganchos agora e tenho que atualizar uma resposta novamente em breve.
polkovnikov.ph
13

Atualizei a solução polkovnikov.ph para React 16 / ES6 com melhorias como manipulação de toque e encaixe em uma grade, que é o que eu preciso para um jogo. O ajuste a uma grade alivia os problemas de desempenho.

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;
qualquer país quente
fonte
oi @anyhotcountry para que você usa o coeficiente gridX ?
AlexNikonov de
1
@AlexNikonov é o tamanho da grade (ajustada) na direção x. É recomendado ter gridX e gridY> 1 para melhorar o desempenho.
anyhotcountry
Isso funcionou muito bem para mim. Na mudança que fiz na função onStart (): calculando relX e relY, usei e.clienX-this.props.x. Isso me permitiu colocar o componente arrastável dentro de um contêiner pai em vez de depender de toda a página sendo a área de arrastar. Sei que é um comentário tardio, mas só queria agradecer.
Geoff
11

Reagir arrastável também é 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')
);

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

Você precisa dos estilos deles, que são curtos, ou não obtém o comportamento esperado. Gosto do comportamento mais do que de algumas das outras opções possíveis, mas também há algo chamado reajustável e móvel . Estou tentando fazer o redimensionamento funcionar com arrastável, mas nenhuma alegria até agora.

Joseph Larson
fonte
8

Aqui está uma abordagem simples e moderno a este com useState, useEffecte useRefem 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
código com sentimento
fonte
Esta parece ser a resposta mais atualizada aqui.
codyThompson,
2

Eu gostaria de adicionar um terceiro cenário

A posição de movimento não é salva de forma alguma. Pense nisso como um movimento do mouse - o cursor não é um componente do React, certo?

Tudo o que você faz é adicionar um objeto como "arrastável" ao seu componente e um fluxo de eventos de arrastamento que irão manipular o 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
    };
},

Nesse caso, a manipulação de DOM é uma coisa elegante (nunca pensei que diria isso)

Thomas Deutsch
fonte
1
você poderia preencher a setXandYfunção com um componente imaginário?
Thellimist
0

Atualizei a classe usando refs, pois todas as soluções que vejo aqui têm coisas que não são mais suportadas ou em breve serão depreciadas ReactDOM.findDOMNode. Pode ser pai de um componente filho ou um grupo de filhos :)

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

Aqui está uma resposta para 2020 com um 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];
}

Em seguida, um componente que usa o 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
fonte