Méthode recommandée pour rendre le composant / div React déplaçable


97

Je veux créer un composant React déplaçable (c'est-à-dire repositionnable à la souris), qui semble nécessairement impliquer des gestionnaires d'événements globaux et dispersés. Je peux le faire à la manière sale, avec une variable globale dans mon fichier JS, et pourrais probablement même l'envelopper dans une belle interface de fermeture, mais je veux savoir s'il existe un moyen qui se maille mieux avec React.

De plus, comme je n'ai jamais fait cela en JavaScript brut auparavant, j'aimerais voir comment un expert le fait, pour m'assurer que tous les cas secondaires sont traités, en particulier en ce qui concerne React.

Merci.


En fait, je serais au moins aussi heureux avec une explication en prose que du code, ou même simplement, "tu fais ça bien". Mais voici un JSFiddle de mon travail jusqu'à présent: jsfiddle.net/Z2JtM
Andrew Fleenor

Je conviens que c'est une question valide, étant donné qu'il existe très peu d'exemples de code de réaction à regarder actuellement
Jared Forsyth

1
J'ai trouvé une solution HTML5 simple pour mon cas d'utilisation - youtu.be/z2nHLfiiKBA . Pourrait aider quelqu'un !!
Prem le

Essaye ça. C'est un simple HOC pour transformer des éléments enveloppés pour être déplaçables npmjs.com/package/just-drag
Shan

Réponses:


111

Je devrais probablement en faire un article de blog, mais voici un exemple assez solide.

Les commentaires devraient bien expliquer les choses, mais faites-moi savoir si vous avez des questions.

Et voici le violon avec lequel jouer: 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))
  }
})

Réflexions sur la propriété de l'État, etc.

«À qui appartient quel État» est une question importante à laquelle il faut répondre, dès le départ. Dans le cas d'un composant "déplaçable", j'ai pu voir quelques scénarios différents.

Scénario 1

le parent doit posséder la position actuelle du déplaçable. Dans ce cas, le draggable posséderait toujours l'état «suis-je en train de glisser», mais serait appelé this.props.onChange(x, y)chaque fois qu'un événement mousemove se produit.

Scénario 2

le parent n'a besoin que de posséder la "position non mobile", et ainsi le draggable posséderait sa "position de glissement" mais une fois en haut, il appellerait this.props.onChange(x, y)et reporterait la décision finale au parent. Si le parent n'aime pas l'endroit où le déplaçable s'est retrouvé, il ne mettrait tout simplement pas à jour son état, et le déplaçable "reviendrait" à sa position initiale avant de le faire glisser.

Mixin ou composant?

@ssorallen a fait remarquer que, parce que "draggable" est plus un attribut qu'une chose en soi, il pourrait mieux servir de mixin. Mon expérience avec les mixins est limitée, donc je n'ai pas vu comment ils pourraient aider ou gêner dans des situations compliquées. Cela pourrait bien être la meilleure option.


4
Excellent exemple. Cela semble plus approprié qu'une Mixinclasse complète puisque "Draggable" n'est pas réellement un objet, c'est une capacité d'un objet.
Ross Allen

2
J'ai joué un peu avec lui, il semble que traîner en dehors de son parent ne fait rien, mais toutes sortes de choses étranges se produisent quand il est traîné dans un autre composant de réaction
Gorkem Yurtseven

11
Vous pouvez supprimer la dépendance jquery en faisant: var computedStyle = window.getComputedStyle(this.getDOMNode()); pos = { top: parseInt(computedStyle.top), left: parseInt(computedStyle.left) }; Si vous utilisez jquery avec react, vous faites probablement quelque chose de mal;) Si vous avez besoin d'un plugin jquery, je trouve qu'il est généralement plus facile et moins de code de le réécrire en pure react.
Matt Crinklaw-Vogt

7
Je voulais juste faire suite au commentaire ci-dessus de @ MattCrinklaw-Vogt pour dire qu'une solution plus à l'épreuve des balles est à utiliser this.getDOMNode().getBoundingClientRect()- getComputedStyle peut afficher n'importe quelle propriété CSS valide, y compris autodans ce cas, le code ci-dessus entraînera un fichier NaN. Voir l'article MDN: developer.mozilla.org/en-US/docs/Web/API/Element/…
Andru

2
Et pour suivre re this.getDOMNode(), c'est obsolète. Utilisez une référence pour obtenir le nœud dom. facebook.github.io/react/docs/…
Chris Sattinger

63

J'ai implémenté react-dnd , un mixin HTML5 flexible par glisser-déposer pour React avec un contrôle complet du DOM.

Les bibliothèques de glisser-déposer existantes ne correspondaient pas à mon cas d'utilisation, j'ai donc écrit la mienne. C'est similaire au code que nous utilisons depuis environ un an sur Stampsy.com, mais réécrit pour tirer parti de React et Flux.

Principales exigences que j'avais:

  • Émettre zéro DOM ou CSS propre, en le laissant aux composants consommateurs;
  • Imposer le moins de structure possible aux composants consommateurs;
  • Utilisez le glisser-déposer HTML5 comme backend principal, mais cela permet d'ajouter différents backends à l'avenir;
  • Comme l'API HTML5 d'origine, mettez l'accent sur le glissement des données et pas seulement sur les «vues déplaçables»;
  • Masquer les bizarreries de l'API HTML5 du code consommateur;
  • Différents composants peuvent être des «sources de glissement» ou des «cibles de dépôt» pour différents types de données;
  • Permettre à un composant de contenir plusieurs sources de glissement et de déposer des cibles si nécessaire;
  • Facilitez le changement d'apparence des cibles de dépôt si des données compatibles sont déplacées ou survolées;
  • Facilitez l'utilisation d'images pour faire glisser des miniatures au lieu de captures d'écran d'éléments, évitant ainsi les bizarreries du navigateur.

Si cela vous semble familier, lisez la suite.

Usage

Source de glissement simple

Tout d'abord, déclarez les types de données qui peuvent être glissés.

Ceux-ci sont utilisés pour vérifier la «compatibilité» des sources de glissement et des cibles de dépôt:

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

(Si vous ne disposez pas de plusieurs types de données, cette bibliothèque n'est peut-être pas faite pour vous.)

Ensuite, créons un composant déplaçable très simple qui, lorsqu'il est déplacé, représente 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)} />
    );
  }
);

En spécifiant configureDragDrop, nous indiquons DragDropMixinle comportement glisser-déposer de ce composant. Les composants traînables et déposables utilisent le même mixin.

À l'intérieur configureDragDrop, nous devons appeler registerTypepour chacun de nos produits personnalisés pris en charge par ItemTypesce composant. Par exemple, il pourrait y avoir plusieurs représentations d'images dans votre application, et chacune fournirait un dragSourcepour ItemTypes.IMAGE.

A dragSourceest juste un objet spécifiant le fonctionnement de la source de glissement. Vous devez implémenter beginDragpour renvoyer l'élément qui représente les données que vous faites glisser et, éventuellement, quelques options qui ajustent l'interface utilisateur de glissement. Vous pouvez éventuellement implémenter canDragpour interdire le glissement, ou endDrag(didDrop)pour exécuter une logique lorsque le dépôt s'est (ou n'a pas) eu lieu. Et vous pouvez partager cette logique entre les composants en laissant un mixin partagé générer dragSourcepour eux.

Enfin, vous devez utiliser {...this.dragSourceFor(itemType)}sur certains (un ou plusieurs) éléments dans renderpour attacher des gestionnaires de glissement. Cela signifie que vous pouvez avoir plusieurs «poignées de glissement» dans un élément, et elles peuvent même correspondre à différents types d'éléments. (Si vous n'êtes pas familier avec la syntaxe des attributs de propagation JSX , vérifiez-la).

Cible de dépôt simple

Disons que nous voulons ImageBlockêtre une cible de chute pour IMAGEs. C'est à peu près la même chose, sauf que nous devons donner registerTypeune dropTargetimplémentation:

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

Faites glisser la source + déposez la cible dans un composant

Disons que nous voulons maintenant que l'utilisateur puisse faire glisser une image hors de ImageBlock. Nous devons juste y ajouter les éléments appropriés dragSourceet quelques gestionnaires:

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

Quoi d'autre est possible?

Je n'ai pas tout couvert mais il est possible d'utiliser cette API de plusieurs manières supplémentaires:

  • Utilisez getDragState(type)et getDropState(type)pour savoir si le glissement est actif et utilisez-le pour basculer entre les classes CSS ou les attributs;
  • Spécifiez dragPreviewd' Imageutiliser les images comme espaces réservés de glissement (utilisez ImagePreloaderMixinpour les charger);
  • Dites, nous voulons rendre ImageBlocksréorganisable. Nous avons seulement besoin d'eux pour mettre en œuvre dropTargetet dragSourcepour ItemTypes.BLOCK.
  • Supposons que nous ajoutions d'autres types de blocs. Nous pouvons réutiliser leur logique de réorganisation en la plaçant dans un mixin.
  • dropTargetFor(...types) permet de spécifier plusieurs types à la fois, de sorte qu'une zone de dépôt peut capturer de nombreux types différents.
  • Lorsque vous avez besoin d'un contrôle plus fin, la plupart des méthodes reçoivent l'événement de glissement qui les a provoquées comme dernier paramètre.

Pour obtenir une documentation et des instructions d'installation à jour, rendez -vous sur react-dnd repo sur Github .


5
Qu'est-ce que le glisser-déposer et le glisser-déposer avec la souris ont en commun autre que l'utilisation d'une souris? Votre réponse n'est pas du tout liée à une question et est clairement une annonce de bibliothèque.
polkovnikov.ph

5
Il semble que 29 personnes aient trouvé que cela était lié à la question. React DnD vous permet également d'implémenter très bien le glissement de la souris . Je pense qu'il vaut mieux que je partage mon travail gratuitement et que je travaille sur des exemples et une documentation volumineuse la prochaine fois pour ne pas avoir à passer du temps à lire des commentaires sournois.
Dan Abramov

7
Oui, je le sais parfaitement. Le fait que vous ayez de la documentation ailleurs ne signifie pas que c'est une réponse à la question donnée. Vous auriez pu écrire "utiliser Google" pour obtenir le même résultat. 29 votes positifs sont dus à un long message d'une personne connue, et non à la qualité de la réponse.
polkovnikov.ph

liens mis à jour vers des exemples officiels de trucs librement déplaçables avec react-dnd: basique , avancé
uryga

23

La réponse de Jared Forsyth est horriblement fausse et dépassée. Il suit tout un ensemble d'anti-modèles tels que l' utilisation destopPropagation , l' initialisation de l'état à partir des accessoires , l'utilisation de jQuery, les objets imbriqués dans l'état et a un draggingchamp d'état impair . En cas de réécriture, la solution sera la suivante, mais elle force toujours la réconciliation DOM virtuelle à chaque tick de déplacement de la souris et n'est pas très performante.

UPD. Ma réponse était horriblement fausse et dépassée. Désormais, le code atténue les problèmes de lenteur du cycle de vie des composants React en utilisant des gestionnaires d'événements natifs et des mises à jour de style, utilise transformcar il ne conduit pas à des reflux et limite les modifications du DOM requestAnimationFrame. Maintenant, c'est toujours 60 FPS pour moi dans chaque navigateur que j'ai essayé.

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

et un peu 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;
}

Exemple sur JSFiddle.


2
Merci pour cela, ce n'est certainement pas la solution la plus performante mais elle suit les meilleures pratiques de création d'applications aujourd'hui.
Spets

1
@ryanj Non, les valeurs par défaut sont mauvaises, c'est le problème. Quelle est l'action appropriée lorsque les accessoires changent? Devrions-nous réinitialiser l'état à la nouvelle valeur par défaut? Devrions-nous comparer la nouvelle valeur par défaut avec une ancienne valeur par défaut pour réinitialiser l'état à la valeur par défaut uniquement lorsque la valeur par défaut a changé? Il n'y a aucun moyen de restreindre l'utilisateur à n'utiliser qu'une valeur constante, et rien d'autre. C'est pourquoi c'est un anti-modèle. Les valeurs par défaut doivent être créées explicitement via des composants d'ordre élevé (c'est-à-dire pour toute la classe, pas pour un objet), et ne doivent jamais être définies via des accessoires.
polkovnikov.ph

1
Je ne suis pas du tout d'accord - l'état du composant est un excellent endroit pour stocker des données spécifiques à l'interface utilisateur d'un composant, qui n'ont aucune pertinence pour l'application dans son ensemble, par exemple. Sans pouvoir potentiellement passer des valeurs par défaut comme accessoires dans certains cas, les options de récupération de ces données après le montage sont limitées et dans de nombreuses circonstances (la plupart?) Moins souhaitables que les caprices autour d'un composant potentiellement passé à un accessoire someDefaultValue différent à un date ultérieure. Je ne le préconise pas comme meilleure pratique ou quoi que ce soit du genre, ce n'est tout simplement pas aussi nocif que vous le suggérez imo
ryan j

2
Solution très simple et élégante en effet. Je suis heureux de voir que mon point de vue était similaire. J'ai une question: vous parlez de performances médiocres, que proposeriez-vous pour obtenir une fonctionnalité similaire en gardant à l'esprit les performances?
Guillaume M

1
Quoi qu'il en soit, nous avons des crochets maintenant, et je dois à nouveau mettre à jour une réponse bientôt.
polkovnikov.ph

13

J'ai mis à jour la solution polkovnikov.ph vers React 16 / ES6 avec des améliorations telles que la gestion tactile et l'alignement sur une grille, ce dont j'ai besoin pour un jeu. L'accrochage à une grille atténue les problèmes de performances.

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;

salut @anyhotcountry pour quoi utilisez-vous le coefficient gridX ?
AlexNikonov

1
@AlexNikonov c'est la taille de la grille (snap-to) dans la direction x. Il est recommandé d'avoir gridX et gridY> 1 pour améliorer les performances.
anyhotcountry

Cela a très bien fonctionné pour moi. Lors du changement que j'ai effectué dans la fonction onStart (): calcul de relX et relY j'ai utilisé e.clienX-this.props.x. Cela m'a permis de placer le composant déplaçable dans un conteneur parent plutôt que de dépendre de la page entière comme zone de glissement. Je sais que c'est un commentaire tardif mais je voulais juste dire merci.
Geoff

11

react-draggable est également facile à utiliser. 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')
);

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

Vous avez besoin de leurs styles, ce qui est court, ou vous n'obtenez pas tout à fait le comportement attendu. J'aime le comportement plus que certains des autres choix possibles, mais il y a aussi quelque chose qui s'appelle react-resizable-and-movable . J'essaie de faire travailler le redimensionnement avec draggable, mais pas de joie pour l'instant.


8

Voici une approche simple et moderne à cela avec useState, useEffectet useRefen 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

Cela semble être la réponse la plus à jour ici.
codyThompson

2

Je souhaite ajouter un 3ème scénario

La position de déplacement n'est en aucun cas enregistrée. Pensez-y comme un mouvement de souris - votre curseur n'est pas un composant React, non?

Tout ce que vous faites, c'est d'ajouter un accessoire comme "draggable" à votre composant et un flux des événements de glissement qui manipuleront le 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
    };
},

Dans ce cas, une manipulation DOM est une chose élégante (je n'aurais jamais pensé dire ça)


1
pourriez-vous remplir la setXandYfonction avec un composant imaginaire?
Thellimist

0

J'ai mis à jour la classe en utilisant des refs car toutes les solutions que je vois ici ont des choses qui ne sont plus prises en charge ou qui seront bientôt dépréciées comme ReactDOM.findDOMNode. Peut être parent d'un composant enfant ou d'un groupe d'enfants :)

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

Voici une réponse 2020 avec un crochet:

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

Puis un composant qui utilise le 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>
  );
}
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.